feat: add Loki log query support to lab-monitoring
Add 3 opt-in Loki tools (query_logs, list_labels, list_label_values) that are registered when LOKI_URL is configured. Includes Loki HTTP client, CLI commands (logs, labels), NixOS module option, formatting, and tests. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
162
internal/monitoring/loki_test.go
Normal file
162
internal/monitoring/loki_test.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLokiClient_QueryRange(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/loki/api/v1/query_range" {
|
||||
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if r.URL.Query().Get("query") != `{job="varlogs"}` {
|
||||
t.Errorf("unexpected query param: %s", r.URL.Query().Get("query"))
|
||||
}
|
||||
if r.URL.Query().Get("direction") != "backward" {
|
||||
t.Errorf("unexpected direction: %s", r.URL.Query().Get("direction"))
|
||||
}
|
||||
if r.URL.Query().Get("limit") != "10" {
|
||||
t.Errorf("unexpected limit: %s", r.URL.Query().Get("limit"))
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"resultType": "streams",
|
||||
"result": [
|
||||
{
|
||||
"stream": {"job": "varlogs", "filename": "/var/log/syslog"},
|
||||
"values": [
|
||||
["1234567890000000000", "line 1"],
|
||||
["1234567891000000000", "line 2"]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewLokiClient(srv.URL)
|
||||
start := time.Unix(0, 1234567890000000000)
|
||||
end := time.Unix(0, 1234567899000000000)
|
||||
data, err := client.QueryRange(context.Background(), `{job="varlogs"}`, start, end, 10, "backward")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if data.ResultType != "streams" {
|
||||
t.Errorf("expected resultType=streams, got %s", data.ResultType)
|
||||
}
|
||||
if len(data.Result) != 1 {
|
||||
t.Fatalf("expected 1 stream, got %d", len(data.Result))
|
||||
}
|
||||
if data.Result[0].Stream["job"] != "varlogs" {
|
||||
t.Errorf("expected job=varlogs, got %s", data.Result[0].Stream["job"])
|
||||
}
|
||||
if len(data.Result[0].Values) != 2 {
|
||||
t.Fatalf("expected 2 entries, got %d", len(data.Result[0].Values))
|
||||
}
|
||||
if data.Result[0].Values[0][1] != "line 1" {
|
||||
t.Errorf("expected first line='line 1', got %s", data.Result[0].Values[0][1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLokiClient_QueryRangeError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"status": "error",
|
||||
"errorType": "bad_data",
|
||||
"error": "invalid LogQL query"
|
||||
}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewLokiClient(srv.URL)
|
||||
_, err := client.QueryRange(context.Background(), "invalid{", time.Now().Add(-time.Hour), time.Now(), 100, "backward")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !contains(err.Error(), "invalid LogQL query") {
|
||||
t.Errorf("expected error to contain 'invalid LogQL query', got: %s", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLokiClient_Labels(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/loki/api/v1/labels" {
|
||||
t.Errorf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"status": "success",
|
||||
"data": ["job", "instance", "filename"]
|
||||
}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewLokiClient(srv.URL)
|
||||
labels, err := client.Labels(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(labels) != 3 {
|
||||
t.Fatalf("expected 3 labels, got %d", len(labels))
|
||||
}
|
||||
if labels[0] != "job" {
|
||||
t.Errorf("expected first label=job, got %s", labels[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLokiClient_LabelValues(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/loki/api/v1/label/job/values" {
|
||||
t.Errorf("unexpected path: %s, expected /loki/api/v1/label/job/values", r.URL.Path)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{
|
||||
"status": "success",
|
||||
"data": ["varlogs", "nginx", "systemd"]
|
||||
}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewLokiClient(srv.URL)
|
||||
values, err := client.LabelValues(context.Background(), "job")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(values) != 3 {
|
||||
t.Fatalf("expected 3 values, got %d", len(values))
|
||||
}
|
||||
if values[0] != "varlogs" {
|
||||
t.Errorf("expected first value=varlogs, got %s", values[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLokiClient_HTTPError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte("internal error"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewLokiClient(srv.URL)
|
||||
_, err := client.QueryRange(context.Background(), `{job="test"}`, time.Now().Add(-time.Hour), time.Now(), 100, "backward")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !contains(err.Error(), "500") {
|
||||
t.Errorf("expected error to contain status code, got: %s", err.Error())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user