package livesampler_test import ( "context" "errors" "fmt" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "github.com/itworx/pulse/internal/live" "github.com/itworx/pulse/internal/livesampler" "github.com/itworx/pulse/internal/metriccatalog" "github.com/itworx/pulse/internal/prometheus" "github.com/itworx/pulse/internal/queryplan" ) func fixedNow() time.Time { return time.Date(2026, 8, 1, 12, 0, 0, 0, time.UTC) } func liveRequest() queryplan.Request { now := fixedNow() return queryplan.Request{ Metric: "container.cpu.utilization", Scope: map[string]string{"container": "media_server"}, Range: queryplan.Range{From: now.Add(-time.Minute), To: now, StepSeconds: 15}, Aggregation: "avg", MaxSeries: 2, MaxPoints: 60, } } func epoch(at time.Time) string { return fmt.Sprintf("%.3f", float64(at.UnixMilli())/1000) } type recorder struct { mu sync.Mutex queries []string } func (r *recorder) record(query string) { r.mu.Lock() defer r.mu.Unlock() r.queries = append(r.queries, query) } func (r *recorder) last() string { r.mu.Lock() defer r.mu.Unlock() if len(r.queries) == 0 { return "" } return r.queries[len(r.queries)-1] } func prometheusServer(t *testing.T, recorded *recorder, handler http.HandlerFunc) *httptest.Server { t.Helper() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/query" { t.Errorf("unexpected path: %s", r.URL.Path) } if recorded != nil { recorded.record(r.URL.Query().Get("query")) } handler(w, r) })) t.Cleanup(server.Close) return server } func jsonServer(t *testing.T, recorded *recorder, body string) *httptest.Server { t.Helper() return prometheusServer(t, recorded, func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(body)) }) } func testSampler(t *testing.T, server *httptest.Server, options livesampler.Options, limits prometheus.Limits) *livesampler.Sampler { t.Helper() if limits.Timeout == 0 { limits.Timeout = time.Second } client, err := prometheus.New(server.URL, nil, limits) if err != nil { t.Fatal(err) } registry, err := metriccatalog.DefaultRegistry() if err != nil { t.Fatal(err) } if options.Now == nil { options.Now = fixedNow } sampler, err := livesampler.New(registry, client, options) if err != nil { t.Fatal(err) } return sampler } func TestSamplerReturnsFreshSamplesFromApprovedTemplate(t *testing.T) { recorded := &recorder{} body := fmt.Sprintf(`{"status":"success","data":{"resultType":"vector","result":[ {"metric":{"__name__":"container_cpu","container":"media_server","instance":"server-2"},"value":[%s,"11"]}, {"metric":{"__name__":"container_cpu","container":"media_server","instance":"server-1"},"value":[%s,"42.5"]}]}}`, epoch(fixedNow().Add(-5*time.Second)), epoch(fixedNow().Add(-10*time.Second))) sampler := testSampler(t, jsonServer(t, recorded, body), livesampler.Options{}, prometheus.Limits{}) samples, err := sampler.Sample(context.Background(), liveRequest()) if err != nil { t.Fatal(err) } if len(samples) != 2 { t.Fatalf("samples=%+v", samples) } if samples[0].Series >= samples[1].Series { t.Fatalf("samples are not deterministically ordered: %+v", samples) } first := samples[0] if first.Freshness != "fresh" || first.Value == nil || *first.Value != 42.5 { t.Fatalf("first=%+v", first) } if first.Timestamp != fixedNow().Add(-10*time.Second) || first.Labels["instance"] != "server-1" { t.Fatalf("first=%+v", first) } if _, ok := first.Labels["__name__"]; ok { t.Fatalf("internal label leaked: %+v", first.Labels) } if query := recorded.last(); !strings.Contains(query, `name="media_server"`) || strings.Contains(query, `container="media_server"`) || !strings.Contains(query, "[1m]") { t.Fatalf("query=%s", query) } } func TestSamplerReportsEmptyResultAsUnknown(t *testing.T) { sampler := testSampler(t, jsonServer(t, nil, `{"status":"success","data":{"resultType":"vector","result":[]}}`), livesampler.Options{}, prometheus.Limits{}) samples, err := sampler.Sample(context.Background(), liveRequest()) if err != nil { t.Fatal(err) } if len(samples) != 1 { t.Fatalf("empty result was reported as %d samples", len(samples)) } if samples[0].Freshness != "unknown" || samples[0].Value != nil || samples[0].Labels["reason"] != livesampler.ReasonNoData { t.Fatalf("samples=%+v", samples) } if samples[0].Series != "container.cpu.utilization" || samples[0].Timestamp != fixedNow() { t.Fatalf("samples=%+v", samples) } } func TestSamplerReportsUpstreamFailureAsUnknown(t *testing.T) { server := prometheusServer(t, nil, func(w http.ResponseWriter, _ *http.Request) { http.Error(w, "unavailable", http.StatusServiceUnavailable) }) sampler := testSampler(t, server, livesampler.Options{}, prometheus.Limits{}) samples, err := sampler.Sample(context.Background(), liveRequest()) if err != nil { t.Fatal(err) } if len(samples) != 1 || samples[0].Freshness != "unknown" || samples[0].Labels["reason"] != livesampler.ReasonSourceError { t.Fatalf("samples=%+v", samples) } } func TestSamplerReportsTimeoutAsUnknownAndBoundsItsOwnDeadline(t *testing.T) { release := make(chan struct{}) t.Cleanup(func() { close(release) }) server := prometheusServer(t, nil, func(w http.ResponseWriter, r *http.Request) { select { case <-release: case <-r.Context().Done(): case <-time.After(5 * time.Second): } }) sampler := testSampler(t, server, livesampler.Options{Timeout: 25 * time.Millisecond}, prometheus.Limits{Timeout: 10 * time.Second}) started := time.Now() samples, err := sampler.Sample(context.Background(), liveRequest()) if err != nil { t.Fatal(err) } if elapsed := time.Since(started); elapsed > 2*time.Second { t.Fatalf("sampler did not bound its own deadline: %s", elapsed) } if len(samples) != 1 || samples[0].Freshness != "unknown" || samples[0].Labels["reason"] != livesampler.ReasonTimeout { t.Fatalf("samples=%+v", samples) } } func TestSamplerReportsStaleScrapeAsStaleWithLastKnownValue(t *testing.T) { body := fmt.Sprintf(`{"status":"success","data":{"resultType":"vector","result":[ {"metric":{"container":"media_server"},"value":[%s,"7"]}]}}`, epoch(fixedNow().Add(-5*time.Minute))) sampler := testSampler(t, jsonServer(t, nil, body), livesampler.Options{}, prometheus.Limits{}) samples, err := sampler.Sample(context.Background(), liveRequest()) if err != nil { t.Fatal(err) } if len(samples) != 1 { t.Fatalf("samples=%+v", samples) } if samples[0].Freshness != "stale" || samples[0].Labels["reason"] != livesampler.ReasonStale { t.Fatalf("stale scrape was not marked: %+v", samples[0]) } if samples[0].Value == nil || *samples[0].Value != 7 || samples[0].Timestamp != fixedNow().Add(-5*time.Minute) { t.Fatalf("last known value and age were lost: %+v", samples[0]) } } func TestSamplerRefusesResultsAboveTheSeriesBudget(t *testing.T) { at := epoch(fixedNow()) body := fmt.Sprintf(`{"status":"success","data":{"resultType":"vector","result":[ {"metric":{"container":"a"},"value":[%s,"1"]}, {"metric":{"container":"b"},"value":[%s,"2"]}, {"metric":{"container":"c"},"value":[%s,"3"]}]}}`, at, at, at) sampler := testSampler(t, jsonServer(t, nil, body), livesampler.Options{}, prometheus.Limits{}) if _, err := sampler.Sample(context.Background(), liveRequest()); err == nil { t.Fatal("expected the request series budget to be enforced") } else { var sampleError livesampler.Error if !errors.As(err, &sampleError) || sampleError.Code != "LIVE_SAMPLE_SERIES_LIMIT" { t.Fatalf("err=%v", err) } } request := liveRequest() request.MaxSeries = 0 samples, err := sampler.Sample(context.Background(), request) if err != nil { t.Fatal(err) } if len(samples) != 3 { t.Fatalf("samples=%+v", samples) } unbounded := testSampler(t, jsonServer(t, nil, body), livesampler.Options{MaxSeries: 2}, prometheus.Limits{}) if _, err := unbounded.Sample(context.Background(), request); err == nil { t.Fatal("expected the sampler series budget to bound an unbounded request") } } func TestSamplerKeepsOnlyTheNewestPointPerSeries(t *testing.T) { body := fmt.Sprintf(`{"status":"success","data":{"resultType":"matrix","result":[ {"metric":{"container":"media_server"},"values":[[%s,"1"],[%s,"2"],[%s,"3"]]}]}}`, epoch(fixedNow().Add(-30*time.Second)), epoch(fixedNow().Add(-20*time.Second)), epoch(fixedNow().Add(-10*time.Second))) sampler := testSampler(t, jsonServer(t, nil, body), livesampler.Options{}, prometheus.Limits{}) samples, err := sampler.Sample(context.Background(), liveRequest()) if err != nil { t.Fatal(err) } if len(samples) != 1 || samples[0].Value == nil || *samples[0].Value != 3 { t.Fatalf("samples=%+v", samples) } if samples[0].Timestamp != fixedNow().Add(-10*time.Second) || samples[0].Freshness != "fresh" { t.Fatalf("samples=%+v", samples) } } func TestSamplerRejectsUnknownMetricAndUnusableConstruction(t *testing.T) { sampler := testSampler(t, jsonServer(t, nil, `{"status":"success","data":{"resultType":"vector","result":[]}}`), livesampler.Options{}, prometheus.Limits{}) request := liveRequest() request.Metric = "container.cpu.unknown" var sampleError livesampler.Error if _, err := sampler.Sample(context.Background(), request); !errors.As(err, &sampleError) || sampleError.Code != "LIVE_SAMPLE_METRIC_UNKNOWN" { t.Fatalf("err=%v", err) } registry, err := metriccatalog.DefaultRegistry() if err != nil { t.Fatal(err) } if _, err := livesampler.New(registry, nil, livesampler.Options{}); !errors.Is(err, livesampler.ErrSourceRequired) { t.Fatalf("err=%v", err) } if _, err := livesampler.New(metriccatalog.Registry{}, stubSource{}, livesampler.Options{}); !errors.Is(err, livesampler.ErrCatalogRequired) { t.Fatalf("err=%v", err) } if _, err := livesampler.New(registry, stubSource{}, livesampler.Options{MaxSeries: 100000}); !errors.Is(err, livesampler.ErrOptionsInvalid) { t.Fatalf("err=%v", err) } if _, err := livesampler.New(registry, stubSource{}, livesampler.Options{Timeout: time.Hour}); !errors.Is(err, livesampler.ErrOptionsInvalid) { t.Fatalf("err=%v", err) } var missing *livesampler.Sampler if _, err := missing.Sample(context.Background(), liveRequest()); !errors.Is(err, livesampler.ErrSourceRequired) { t.Fatalf("err=%v", err) } } type stubSource struct{} func (stubSource) Query(context.Context, string, *time.Time) (prometheus.QueryResult, error) { return prometheus.QueryResult{}, nil } func TestSamplerHonorsCallerCancellation(t *testing.T) { sampler := testSampler(t, jsonServer(t, nil, `{"status":"success","data":{"resultType":"vector","result":[]}}`), livesampler.Options{}, prometheus.Limits{}) ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := sampler.Sample(ctx, liveRequest()); !errors.Is(err, context.Canceled) { t.Fatalf("err=%v", err) } } func TestSamplerIsSafeForConcurrentSubscriptions(t *testing.T) { body := fmt.Sprintf(`{"status":"success","data":{"resultType":"vector","result":[ {"metric":{"container":"media_server"},"value":[%s,"5"]}]}}`, epoch(fixedNow())) sampler := testSampler(t, jsonServer(t, &recorder{}, body), livesampler.Options{}, prometheus.Limits{MaxConcurrency: 4}) var group sync.WaitGroup failures := make(chan error, 32) for worker := 0; worker < 8; worker++ { group.Add(1) go func() { defer group.Done() for round := 0; round < 8; round++ { samples, err := sampler.Sample(context.Background(), liveRequest()) if err != nil { failures <- err return } if len(samples) != 1 || samples[0].Freshness != "fresh" || samples[0].Value == nil || *samples[0].Value != 5 { failures <- fmt.Errorf("unexpected samples: %+v", samples) return } } }() } group.Wait() close(failures) for err := range failures { t.Fatal(err) } } func TestSamplerSatisfiesLiveSampler(t *testing.T) { sampler := testSampler(t, jsonServer(t, nil, `{"status":"success","data":{"resultType":"vector","result":[]}}`), livesampler.Options{}, prometheus.Limits{}) var contract live.Sampler = sampler samples, err := contract.Sample(context.Background(), liveRequest()) if err != nil || len(samples) == 0 { t.Fatalf("samples=%+v err=%v", samples, err) } }