package prometheus import ( "context" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/itworx/pulse/internal/observability" ) func TestQueryNormalizesWarningsAndUsesFixedEndpoint(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/query" || r.URL.Query().Get("query") != "up" { t.Fatalf("unexpected request: %s", r.URL.String()) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"status":"success","data":{"resultType":"vector"},"warnings":["partial response"]}`)) })) defer server.Close() c, err := New(server.URL, nil, Limits{Timeout: time.Second}) if err != nil { t.Fatal(err) } result, err := c.Query(context.Background(), "up", nil) if err != nil { t.Fatal(err) } if len(result.Warnings) != 1 || c.Metrics().Requests != 1 { t.Fatalf("unexpected result/metrics: %+v %+v", result, c.Metrics()) } } func TestRejectsUserSuppliedEndpointAndOversizedQuery(t *testing.T) { if _, err := New("http://prometheus.local/api/v1/query?query=up", nil, Limits{}); err == nil { t.Fatal("expected URL rejection") } c, err := New("http://prometheus.local", nil, Limits{MaxQueryLength: 3}) if err != nil { t.Fatal(err) } if _, err := c.Query(context.Background(), "up + 1", nil); err == nil { t.Fatal("expected query bound rejection") } } func TestTimeoutIsCounted(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(100 * time.Millisecond) })) defer server.Close() c, err := New(server.URL, nil, Limits{Timeout: 10 * time.Millisecond}) if err != nil { t.Fatal(err) } _, err = c.Query(context.Background(), "up", nil) if err == nil || !strings.Contains(err.Error(), "context deadline") { t.Fatalf("unexpected timeout: %v", err) } if c.Metrics().Timeouts != 1 || c.Metrics().Errors != 1 { t.Fatalf("unexpected metrics: %+v", c.Metrics()) } } func TestRangeBounds(t *testing.T) { c, err := New("http://prometheus.local", nil, Limits{}) if err != nil { t.Fatal(err) } now := time.Now() if _, err := c.QueryRange(context.Background(), "up", now, now.Add(time.Minute), 0); err == nil { t.Fatal("expected step validation") } } func TestHealthMapsRequestFailureToUnknown(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "unavailable", http.StatusServiceUnavailable) })) defer server.Close() c, err := New(server.URL, nil, Limits{Timeout: time.Second}) if err != nil { t.Fatal(err) } health := c.Health(context.Background()) if health.State != "unknown" || health.ReasonCode != "PROMETHEUS_UNAVAILABLE" || health.Policy.MaxAge != 30*time.Second { t.Fatalf("unexpected health: %+v", health) } } func TestQueryLatencyIsMeasuredAndPublished(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { time.Sleep(5 * time.Millisecond) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"status":"success","data":{"resultType":"vector"}}`)) })) defer server.Close() c, err := New(server.URL, nil, Limits{Timeout: time.Second}) if err != nil { t.Fatal(err) } if metrics := c.Metrics(); metrics.TotalDuration != 0 || metrics.MaxDuration != 0 { t.Fatalf("latency counted before any request: %+v", metrics) } for index := 0; index < 2; index++ { if _, err := c.Query(context.Background(), "up", nil); err != nil { t.Fatal(err) } } metrics := c.Metrics() if metrics.Requests != 2 || metrics.TotalDuration < 10*time.Millisecond || metrics.MaxDuration < 5*time.Millisecond { t.Fatalf("query latency was not measured: %+v", metrics) } if metrics.MaxDuration > metrics.TotalDuration { t.Fatalf("max latency exceeds the total: %+v", metrics) } registry := observability.NewRegistry(time.Unix(100, 0)) c.PublishMetrics(registry) exposition := registry.Exposition(time.Unix(101, 0)) for _, name := range []string{"pulse_prometheus_requests_total 2", "pulse_prometheus_errors_total 0", "pulse_prometheus_timeouts_total 0", "pulse_prometheus_request_duration_seconds_sum ", "pulse_prometheus_request_duration_seconds_max "} { if !strings.Contains(exposition, name) { t.Fatalf("metric %q missing from exposition: %s", name, exposition) } } } func TestPublishMetricsToleratesMissingSink(t *testing.T) { c, err := New("http://prometheus.local", nil, Limits{}) if err != nil { t.Fatal(err) } c.PublishMetrics(nil) var registry *observability.Registry c.PublishMetrics(registry) }