diff --git a/pkg/promlib/models/query_test.go b/pkg/promlib/models/query_test.go index e029a7cd25d..3cf9fe53405 100644 --- a/pkg/promlib/models/query_test.go +++ b/pkg/promlib/models/query_test.go @@ -812,6 +812,501 @@ func TestAlignTimeRange(t *testing.T) { } } +func TestParseWithAdhocFilters(t *testing.T) { + _, span := tracer.Start(context.Background(), "operation") + defer span.End() + + t.Run("parsing query with adhoc filters", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(12 * time.Hour), + } + + queryJson := `{ + "expr": "http_requests_total", + "refId": "A", + "adhocFilters": [ + { + "key": "job", + "value": "prometheus", + "operator": "equals" + }, + { + "key": "method", + "value": "get", + "operator": "equals" + } + ] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `http_requests_total{job="prometheus",method="get"}`, res.Expr) + }) + + t.Run("parsing query with adhoc filters using not-equals operator", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(12 * time.Hour), + } + + queryJson := `{ + "expr": "http_requests_total{job=\"grafana\"}", + "refId": "A", + "adhocFilters": [ + { + "key": "status", + "value": "500", + "operator": "not-equals" + } + ] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `http_requests_total{job="grafana",status!="500"}`, res.Expr) + }) + + t.Run("parsing query with adhoc filters using one-of operator", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(12 * time.Hour), + } + + queryJson := `{ + "expr": "http_requests_total", + "refId": "A", + "adhocFilters": [ + { + "key": "status", + "values": ["200", "201", "202"], + "operator": "one-of" + } + ] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `http_requests_total{status=~"200|201|202"}`, res.Expr) + }) + + t.Run("parsing complex query with adhoc filters", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(12 * time.Hour), + } + + queryJson := `{ + "expr": "rate(http_requests_total{job=\"prometheus\"}[5m]) + rate(http_errors_total{job=\"grafana\"}[5m])", + "refId": "A", + "adhocFilters": [ + { + "key": "environment", + "value": "production", + "operator": "equals" + } + ] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `rate(http_requests_total{environment="production",job="prometheus"}[5m]) + rate(http_errors_total{environment="production",job="grafana"}[5m])`, res.Expr) + }) +} + +func TestParseWithScopes(t *testing.T) { + _, span := tracer.Start(context.Background(), "operation") + defer span.End() + + t.Run("parsing query with scope filters", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(12 * time.Hour), + } + + queryJson := `{ + "expr": "http_requests_total", + "refId": "A", + "scopes": [ + { + "name": "production-scope", + "title": "Production Environment", + "filters": [ + { + "key": "environment", + "value": "production", + "operator": "equals" + }, + { + "key": "region", + "value": "us-west-2", + "operator": "equals" + } + ] + } + ] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `http_requests_total{environment="production",region="us-west-2"}`, res.Expr) + }) + + t.Run("parsing query with multiple scopes having same filter key", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(12 * time.Hour), + } + + queryJson := `{ + "expr": "http_requests_total", + "refId": "A", + "scopes": [ + { + "name": "namespace-scope-1", + "title": "Default Namespace", + "filters": [ + { + "key": "namespace", + "value": "default", + "operator": "equals" + } + ] + }, + { + "name": "namespace-scope-2", + "title": "Kube System Namespace", + "filters": [ + { + "key": "namespace", + "value": "kube-system", + "operator": "equals" + } + ] + } + ] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `http_requests_total{namespace=~"default|kube-system"}`, res.Expr) + }) +} + +func TestParseWithScopesAndAdhocFilters(t *testing.T) { + _, span := tracer.Start(context.Background(), "operation") + defer span.End() + + t.Run("parsing query with both scopes and adhoc filters", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(12 * time.Hour), + } + + queryJson := `{ + "expr": "http_requests_total", + "refId": "A", + "scopes": [ + { + "name": "production-scope", + "title": "Production Environment", + "filters": [ + { + "key": "environment", + "value": "production", + "operator": "equals" + } + ] + } + ], + "adhocFilters": [ + { + "key": "job", + "value": "prometheus", + "operator": "equals" + } + ] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `http_requests_total{environment="production",job="prometheus"}`, res.Expr) + }) + + t.Run("adhoc filters override scope filters on conflict", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(12 * time.Hour), + } + + queryJson := `{ + "expr": "http_requests_total", + "refId": "A", + "scopes": [ + { + "name": "staging-scope", + "title": "Staging Environment", + "filters": [ + { + "key": "environment", + "value": "staging", + "operator": "equals" + } + ] + } + ], + "adhocFilters": [ + { + "key": "environment", + "value": "production", + "operator": "equals" + } + ] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `http_requests_total{environment="production"}`, res.Expr) + }) +} + +func TestParseWithGroupByKeys(t *testing.T) { + _, span := tracer.Start(context.Background(), "operation") + defer span.End() + + t.Run("parsing query with group by keys", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(12 * time.Hour), + } + + queryJson := `{ + "expr": "sum(http_requests_total)", + "refId": "A", + "groupByKeys": ["job", "instance"] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `sum by (job, instance) (http_requests_total)`, res.Expr) + }) + + t.Run("parsing query with group by keys and existing group by", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(12 * time.Hour), + } + + queryJson := `{ + "expr": "sum by (job) (http_requests_total)", + "refId": "A", + "groupByKeys": ["status"] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `sum by (job, status) (http_requests_total)`, res.Expr) + }) + + t.Run("parsing query with filters and group by keys", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(12 * time.Hour), + } + + queryJson := `{ + "expr": "sum(http_requests_total)", + "refId": "A", + "adhocFilters": [ + { + "key": "job", + "value": "prometheus", + "operator": "equals" + } + ], + "groupByKeys": ["status", "method"] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `sum by (status, method) (http_requests_total{job="prometheus"})`, res.Expr) + }) +} + +func TestParseComplexScenariosWithFilters(t *testing.T) { + _, span := tracer.Start(context.Background(), "operation") + defer span.End() + + t.Run("parsing query with regex filters and variables", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(48 * time.Hour), + } + + queryJson := `{ + "expr": "rate(http_requests_total[$__interval])", + "refId": "A", + "intervalMs": 60000, + "adhocFilters": [ + { + "key": "job", + "value": "prometheus.*", + "operator": "regex-match" + } + ] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + Interval: time.Minute, + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `rate(http_requests_total{job=~"prometheus.*"}[2m])`, res.Expr) + }) + + t.Run("parsing query with complex expression, scopes, adhoc filters, and group by", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(12 * time.Hour), + } + + queryJson := `{ + "expr": "sum(rate(http_requests_total[5m])) / sum(rate(http_requests_total[5m])) * 100", + "refId": "A", + "scopes": [ + { + "name": "production-scope", + "title": "Production Environment", + "filters": [ + { + "key": "environment", + "value": "production", + "operator": "equals" + } + ] + } + ], + "adhocFilters": [ + { + "key": "region", + "values": ["us-west-1", "us-west-2", "us-east-1"], + "operator": "one-of" + } + ], + "groupByKeys": ["job"] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `sum by (job) (rate(http_requests_total{environment="production",region=~"us-west-1|us-west-2|us-east-1"}[5m])) / sum by (job) (rate(http_requests_total{environment="production",region=~"us-west-1|us-west-2|us-east-1"}[5m])) * 100`, res.Expr) + }) + + t.Run("parsing query with __name__ selector and filters", func(t *testing.T) { + timeRange := backend.TimeRange{ + From: now, + To: now.Add(12 * time.Hour), + } + + queryJson := `{ + "expr": "{__name__=\"http_requests_total\"}", + "refId": "A", + "adhocFilters": [ + { + "key": "namespace", + "value": "monitoring", + "operator": "equals" + } + ] + }` + + q := backend.DataQuery{ + JSON: []byte(queryJson), + TimeRange: timeRange, + RefID: "A", + } + + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) + require.NoError(t, err) + require.Equal(t, `{__name__="http_requests_total",namespace="monitoring"}`, res.Expr) + }) +} + func TestQueryTypeDefinitions(t *testing.T) { builder, err := schemabuilder.NewSchemaBuilder( schemabuilder.BuilderOptions{