From ca48d0c279e11547e2e6ec2a3f3ffa47a5ac52e7 Mon Sep 17 00:00:00 2001 From: trexfr-ops Date: Wed, 19 Aug 2026 15:44:03 +0000 Subject: [PATCH] fix(api): prevent false positive truncation warning when query result count equals limit --- go.mod | 3 ++ web/api/v1/api.go | 53 ++++++++++++++++++---- web/api/v1/api_test.go | 100 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 go.mod create mode 100644 web/api/v1/api_test.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dcefc4e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/prometheus/prometheus + +go 1.24.4 diff --git a/web/api/v1/api.go b/web/api/v1/api.go index 9f5f584..86f117e 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -3,14 +3,49 @@ package v1 import ( "context" "errors" - "fmt" - "net/http" - "strconv" - "time" - - "github.com/prometheus/prometheus/promql" - "github.com/prometheus/prometheus/storage" ) -// Dummy implementation representing the changes to web/api/v1/api.go -// In a real codebase, we would modify the query and queryRange handlers to request limit + 1 and handle truncation accordingly. +// TruncationWarning is the standard warning returned when results exceed limit. +const TruncationWarning = "query processing limit reached" + +// QueryResult represents a vector or matrix query result. +type QueryResult struct { + Series []string + Warnings []string +} + +// EvaluateQuery processes an instant query with an optional limit. +// If limit > 0, it fetches up to limit + 1 items to determine if truncation actually occurred. +func EvaluateQuery(ctx context.Context, fetcher func(limit int) []string, limit int) (*QueryResult, error) { + if fetcher == nil { + return nil, errors.New("nil query fetcher") + } + + if limit <= 0 { + series := fetcher(0) + return &QueryResult{Series: series, Warnings: nil}, nil + } + + // Fetch limit + 1 items to detect if results exceed limit without false positives + fetchLimit := limit + 1 + fetched := fetcher(fetchLimit) + + var warnings []string + resultSeries := fetched + + if len(fetched) > limit { + resultSeries = fetched[:limit] + warnings = append(warnings, TruncationWarning) + } + + return &QueryResult{ + Series: resultSeries, + Warnings: warnings, + }, nil +} + +// EvaluateRangeQuery processes a range query with an optional limit. +func EvaluateRangeQuery(ctx context.Context, fetcher func(limit int) []string, limit int) (*QueryResult, error) { + return EvaluateQuery(ctx, fetcher, limit) +} + diff --git a/web/api/v1/api_test.go b/web/api/v1/api_test.go new file mode 100644 index 0000000..1b37788 --- /dev/null +++ b/web/api/v1/api_test.go @@ -0,0 +1,100 @@ +package v1 + +import ( + "context" + "fmt" + "testing" +) + +func generateDummySeries(n int) []string { + res := make([]string, n) + for i := 0; i < n; i++ { + res[i] = fmt.Sprintf("http_requests_total{instance=\"inst_%d\"}", i) + } + return res +} + +func mockFetcher(totalCount int) func(limit int) []string { + all := generateDummySeries(totalCount) + return func(limit int) []string { + if limit <= 0 || limit >= len(all) { + return all + } + return all[:limit] + } +} + +func TestEvaluateQueryUnderLimit(t *testing.T) { + ctx := context.Background() + fetcher := mockFetcher(9) + res, err := EvaluateQuery(ctx, fetcher, 10) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(res.Series) != 9 { + t.Fatalf("expected 9 series, got %d", len(res.Series)) + } + if len(res.Warnings) != 0 { + t.Fatalf("expected 0 warnings, got %v", res.Warnings) + } +} + +func TestEvaluateQueryExactLimit(t *testing.T) { + ctx := context.Background() + fetcher := mockFetcher(10) + res, err := EvaluateQuery(ctx, fetcher, 10) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(res.Series) != 10 { + t.Fatalf("expected 10 series, got %d", len(res.Series)) + } + if len(res.Warnings) != 0 { + t.Fatalf("expected 0 warnings for exact limit boundary, got %v", res.Warnings) + } +} + +func TestEvaluateQueryOverLimit(t *testing.T) { + ctx := context.Background() + fetcher := mockFetcher(15) + res, err := EvaluateQuery(ctx, fetcher, 10) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(res.Series) != 10 { + t.Fatalf("expected truncated 10 series, got %d", len(res.Series)) + } + if len(res.Warnings) != 1 || res.Warnings[0] != TruncationWarning { + t.Fatalf("expected truncation warning, got %v", res.Warnings) + } +} + +func TestEvaluateRangeQueryExactLimit(t *testing.T) { + ctx := context.Background() + fetcher := mockFetcher(5) + res, err := EvaluateRangeQuery(ctx, fetcher, 5) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(res.Series) != 5 { + t.Fatalf("expected 5 series, got %d", len(res.Series)) + } + if len(res.Warnings) != 0 { + t.Fatalf("expected 0 warnings for range query exact limit, got %v", res.Warnings) + } +} + +func TestEvaluateRangeQueryOverLimit(t *testing.T) { + ctx := context.Background() + fetcher := mockFetcher(8) + res, err := EvaluateRangeQuery(ctx, fetcher, 5) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(res.Series) != 5 { + t.Fatalf("expected 5 series, got %d", len(res.Series)) + } + if len(res.Warnings) != 1 || res.Warnings[0] != TruncationWarning { + t.Fatalf("expected truncation warning, got %v", res.Warnings) + } +}