diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 6994d45fb40..9440b7b1187 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -492,11 +492,6 @@ export interface FeatureToggles { */ useMultipleScopeNodesEndpoint?: boolean; /** - * In-development feature that will allow injection of labels into prometheus queries. - * @default true - */ - promQLScope?: boolean; - /** * In-development feature that will allow injection of labels into loki queries. * @default false */ diff --git a/packages/grafana-prometheus/src/datasource.test.ts b/packages/grafana-prometheus/src/datasource.test.ts index bc30a4befff..ac5e2f59623 100644 --- a/packages/grafana-prometheus/src/datasource.test.ts +++ b/packages/grafana-prometheus/src/datasource.test.ts @@ -664,10 +664,6 @@ describe('PrometheusDatasource', () => { }); describe('interpolateVariablesInQueries', () => { - afterEach(() => { - config.featureToggles.promQLScope = undefined; - }); - it('should call replace function 3 times', () => { const query: PromQuery = { expr: 'test{job="testjob"}', @@ -695,17 +691,32 @@ describe('PrometheusDatasource', () => { expect(ds.enhanceExprWithAdHocFilters).toHaveBeenCalled(); }); - it('should not apply adhoc filters when promQLScope is enabled', () => { - config.featureToggles.promQLScope = true; + it('should not apply adhoc filters when scopes with filters are present', () => { ds.enhanceExprWithAdHocFilters = jest.fn(); ds.generateScopeFilters = jest.fn(); - const queries = [ + // Fix: Use correct types for scopes and filters + const typedScopes = [ + { + name: 'test-scope', + title: 'Test Scope', + type: 'test', + filters: [ + { + key: 'bar', + operator: 'equals' as const, + value: 'baz', + }, + ], + }, + ]; + const typedQueries: PromQuery[] = [ { refId: 'A', expr: 'rate({bar="baz", job="foo"} [5m]', + scopes: typedScopes, }, ]; - ds.interpolateVariablesInQueries(queries, {}); + ds.interpolateVariablesInQueries(typedQueries, {}); expect(ds.enhanceExprWithAdHocFilters).not.toHaveBeenCalled(); expect(ds.generateScopeFilters).toHaveBeenCalled(); }); @@ -716,10 +727,6 @@ describe('PrometheusDatasource', () => { replaceMock.mockImplementation((a: string, ...rest: unknown[]) => a); }); - afterEach(() => { - config.featureToggles.promQLScope = false; - }); - it('should call replace function for legendFormat', () => { const query = { expr: 'test{job="bar"}', @@ -783,7 +790,21 @@ describe('PrometheusDatasource', () => { }); it('should generate scope filters and **not** apply ad-hoc filters to expr', () => { - config.featureToggles.promQLScope = true; + const scopes = [ + { + name: 'test-scope', + title: 'Test Scope', + type: 'test', + filters: [ + { + key: 'bar', + operator: 'equals' as const, + value: 'baz', + }, + ], + }, + ]; + replaceMock.mockImplementation((a: string) => a); const filters = [ { @@ -801,17 +822,18 @@ describe('PrometheusDatasource', () => { const query = { expr: 'test{job="bar"}', refId: 'A', + scopes: scopes, }; const expectedScopeFilters: ScopeSpecFilter[] = [ { key: 'k1', - operator: 'equals', + operator: 'equals' as const, value: 'v1', }, { key: 'k2', - operator: 'not-equals', + operator: 'not-equals' as const, value: 'v2', }, ]; @@ -891,13 +913,27 @@ describe('PrometheusDatasource', () => { }); it('should replace variables in adhoc filters on backend when promQLScope is enabled', () => { - config.featureToggles.promQLScope = true; + const scopes = [ + { + name: 'test-scope', + title: 'Test Scope', + type: 'test', + filters: [ + { + key: 'bar', + operator: 'equals' as const, + value: 'baz', + }, + ], + }, + ]; const searchPattern = /\$A/g; replaceMock.mockImplementation((a: string) => a?.replace(searchPattern, '99') ?? a); const query = { expr: 'test', refId: 'A', + scopes: scopes, }; const filters = [ { @@ -906,6 +942,7 @@ describe('PrometheusDatasource', () => { value: '$A', }, ]; + const result = ds.applyTemplateVariables(query, {}, filters); expect(result).toMatchObject({ expr: 'test', diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index 9755325ff78..4f2beb195d1 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -430,7 +430,7 @@ export class PrometheusDatasource utcOffsetSec: utcOffset * 60, }; - if (config.featureToggles.promQLScope) { + if (request.scopes) { processedTarget.scopes = (request.scopes ?? []).map((scope) => ({ name: scope.metadata.name, ...scope.spec, @@ -543,7 +543,7 @@ export class PrometheusDatasource options.timeRange = getDefaultTimeRange(); } - if (config.featureToggles.promQLScope && (options?.scopes?.length ?? 0) > 0) { + if ((options?.scopes?.length ?? 0) > 0) { const suggestions = await this.languageProvider.fetchSuggestions( options.timeRange, options.queries, @@ -574,7 +574,7 @@ export class PrometheusDatasource } const requestId = `[${this.uid}][${options.key}]`; - if (config.featureToggles.promQLScope && (options?.scopes?.length ?? 0) > 0) { + if ((options?.scopes?.length ?? 0) > 0) { return ( await this.languageProvider.fetchSuggestions( options.timeRange, @@ -609,7 +609,7 @@ export class PrometheusDatasource scopedVars, this.interpolateExploreMetrics(query.fromExploreMetrics) ); - const replacedInterpolatedQuery = config.featureToggles.promQLScope + const replacedInterpolatedQuery = targetHasScopes(query) ? interpolatedQuery : this.templateSrv.replace( this.enhanceExprWithAdHocFilters(filters, interpolatedQuery), @@ -619,7 +619,7 @@ export class PrometheusDatasource const expandedQuery = { ...query, - ...(config.featureToggles.promQLScope ? { adhocFilters: this.generateScopeFilters(filters) } : {}), + ...(query.scopes && query.scopes.length > 0 ? { adhocFilters: this.generateScopeFilters(filters) } : {}), datasource: this.getRef(), expr: replacedInterpolatedQuery, interval: this.templateSrv.replace(query.interval, scopedVars), @@ -791,13 +791,13 @@ export class PrometheusDatasource // Apply ad-hoc filters // When ad-hoc filters are applied, we replace again the variables in case the ad-hoc filters also reference a variable - const exprWithAdhoc = config.featureToggles.promQLScope + const exprWithAdhoc = targetHasScopes(target) ? expr : this.templateSrv.replace(this.enhanceExprWithAdHocFilters(filters, expr), variables, this.interpolateQueryExpr); return { ...target, - ...(config.featureToggles.promQLScope ? { adhocFilters: this.generateScopeFilters(filters) } : {}), + ...(targetHasScopes(target) ? { adhocFilters: this.generateScopeFilters(filters) } : {}), expr: exprWithAdhoc, interval: this.templateSrv.replace(target.interval, variables), legendFormat: this.templateSrv.replace(target.legendFormat, variables), @@ -862,6 +862,10 @@ export class PrometheusDatasource } } +function targetHasScopes(target: PromQuery): boolean { + return !!(target.scopes && target.scopes.length > 0); +} + export function extractRuleMappingFromGroups(groups: RawRecordingRules[]): RuleQueryMapping { return groups.reduce( (mapping, group) => diff --git a/pkg/promlib/models/query.go b/pkg/promlib/models/query.go index a34c1b6aea9..c2ea3305d0b 100644 --- a/pkg/promlib/models/query.go +++ b/pkg/promlib/models/query.go @@ -161,7 +161,7 @@ type internalQueryModel struct { Interval string `json:"interval,omitempty"` } -func Parse(ctx context.Context, log glog.Logger, span trace.Span, query backend.DataQuery, dsScrapeInterval string, intervalCalculator intervalv2.Calculator, fromAlert bool, enableScope bool) (*Query, error) { +func Parse(ctx context.Context, log glog.Logger, span trace.Span, query backend.DataQuery, dsScrapeInterval string, intervalCalculator intervalv2.Calculator, fromAlert bool) (*Query, error) { model := &internalQueryModel{} if err := json.Unmarshal(query.JSON, model); err != nil { return nil, err @@ -185,38 +185,36 @@ func Parse(ctx context.Context, log glog.Logger, span trace.Span, query backend. timeRange, ) - if enableScope { - var scopeFilters []scope.ScopeFilter - for _, scope := range model.Scopes { - scopeFilters = append(scopeFilters, scope.Filters...) - } + var scopeFilters []scope.ScopeFilter + for _, scope := range model.Scopes { + scopeFilters = append(scopeFilters, scope.Filters...) + } - if len(scopeFilters) > 0 { - span.SetAttributes(attribute.StringSlice("scopeFilters", func() []string { - var filters []string - for _, f := range scopeFilters { - filters = append(filters, fmt.Sprintf("%q %q %q", f.Key, f.Operator, f.Value)) - } - return filters - }())) - } - - if len(model.AdhocFilters) > 0 { - span.SetAttributes(attribute.StringSlice("adhocFilters", func() []string { - var filters []string - for _, f := range model.AdhocFilters { - filters = append(filters, fmt.Sprintf("%q %q %q", f.Key, f.Operator, f.Value)) - } - return filters - }())) - } - - if len(scopeFilters) > 0 || len(model.AdhocFilters) > 0 || len(model.GroupByKeys) > 0 { - log.Info("Applying scope filters", "scopeFiltersCount", len(scopeFilters), "adhocFiltersCount", len(model.AdhocFilters), "groupByKeysCount", len(model.GroupByKeys)) - expr, err = ApplyFiltersAndGroupBy(expr, scopeFilters, model.AdhocFilters, model.GroupByKeys) - if err != nil { - return nil, err + if len(scopeFilters) > 0 { + span.SetAttributes(attribute.StringSlice("scopeFilters", func() []string { + var filters []string + for _, f := range scopeFilters { + filters = append(filters, fmt.Sprintf("%q %q %q", f.Key, f.Operator, f.Value)) } + return filters + }())) + } + + if len(model.AdhocFilters) > 0 { + span.SetAttributes(attribute.StringSlice("adhocFilters", func() []string { + var filters []string + for _, f := range model.AdhocFilters { + filters = append(filters, fmt.Sprintf("%q %q %q", f.Key, f.Operator, f.Value)) + } + return filters + }())) + } + + if len(scopeFilters) > 0 || len(model.AdhocFilters) > 0 || len(model.GroupByKeys) > 0 { + log.Info("Applying scope filters", "scopeFiltersCount", len(scopeFilters), "adhocFiltersCount", len(model.AdhocFilters), "groupByKeysCount", len(model.GroupByKeys)) + expr, err = ApplyFiltersAndGroupBy(expr, scopeFilters, model.AdhocFilters, model.GroupByKeys) + if err != nil { + return nil, err } } diff --git a/pkg/promlib/models/query_test.go b/pkg/promlib/models/query_test.go index cc2b2637442..3cf9fe53405 100644 --- a/pkg/promlib/models/query_test.go +++ b/pkg/promlib/models/query_test.go @@ -45,7 +45,7 @@ func TestParse(t *testing.T) { RefID: "A", } - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, true, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, true) require.NoError(t, err) require.Equal(t, false, res.ExemplarQuery) }) @@ -62,7 +62,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, time.Second*30, res.Step) }) @@ -80,7 +80,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, time.Second*15, res.Step) }) @@ -98,7 +98,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, time.Minute*20, res.Step) }) @@ -116,7 +116,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, time.Minute*2, res.Step) }) @@ -134,7 +134,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "240s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "240s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, time.Minute*4, res.Step) }) @@ -153,7 +153,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [2m]})", res.Expr) require.Equal(t, 120*time.Second, res.Step) @@ -174,7 +174,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [2m]})", res.Expr) }) @@ -193,7 +193,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [120000]})", res.Expr) }) @@ -212,7 +212,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [120000]}) + rate(ALERTS{job=\"test\" [2m]})", res.Expr) }) @@ -231,7 +231,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [120000]}) + rate(ALERTS{job=\"test\" [2m]})", res.Expr) }) @@ -249,7 +249,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [172800s]})", res.Expr) }) @@ -267,7 +267,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [172800]})", res.Expr) }) @@ -285,7 +285,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [172800s]})", res.Expr) }) @@ -303,7 +303,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [0]})", res.Expr) }) @@ -321,7 +321,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [1]})", res.Expr) }) @@ -339,7 +339,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [172800000]})", res.Expr) }) @@ -357,7 +357,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [20]})", res.Expr) }) @@ -376,7 +376,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [20m0s]})", res.Expr) }) @@ -395,7 +395,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, 1*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [1m0s]})", res.Expr) require.Equal(t, 1*time.Minute, res.Step) @@ -414,7 +414,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, 2*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [135000]})", res.Expr) }) @@ -432,7 +432,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, 2*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [135000]}) + rate(ALERTS{job=\"test\" [2m15s]})", res.Expr) }) @@ -451,7 +451,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, 2*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "A", res.RefId) }) @@ -469,7 +469,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, 2*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "rate(ALERTS{job=\"test\" [135000]}) + rate(ALERTS{job=\"test\" [2m15s]})", res.Expr) }) @@ -488,7 +488,7 @@ func TestParse(t *testing.T) { "range": true }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, true, res.RangeQuery) }) @@ -508,7 +508,7 @@ func TestParse(t *testing.T) { "instant": true }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, true, res.RangeQuery) require.Equal(t, true, res.InstantQuery) @@ -527,7 +527,7 @@ func TestParse(t *testing.T) { "refId": "A" }`, timeRange, time.Duration(1)*time.Minute) - res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, true, res.RangeQuery) }) @@ -660,7 +660,7 @@ func TestRateInterval(t *testing.T) { t.Run(tt.name, func(t *testing.T) { q := mockQuery(tt.args.expr, tt.args.interval, tt.args.intervalMs, tt.args.timeRange) q.MaxDataPoints = 12384 - res, err := models.Parse(context.Background(), log.New(), span, q, tt.args.dsScrapeInterval, intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, q, tt.args.dsScrapeInterval, intervalCalculator, false) require.NoError(t, err) require.Equal(t, tt.want.Expr, res.Expr) require.Equal(t, tt.want.Step, res.Step) @@ -695,7 +695,7 @@ func TestRateInterval(t *testing.T) { "utcOffsetSec":3600 }`), } - res, err := models.Parse(context.Background(), log.New(), span, query, "30s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, query, "30s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "sum(rate(process_cpu_seconds_total[2m0s]))", res.Expr) require.Equal(t, 30*time.Second, res.Step) @@ -730,7 +730,7 @@ func TestRateInterval(t *testing.T) { "maxDataPoints": 1055 }`), } - res, err := models.Parse(context.Background(), log.New(), span, query, "15s", intervalCalculator, false, false) + res, err := models.Parse(context.Background(), log.New(), span, query, "15s", intervalCalculator, false) require.NoError(t, err) require.Equal(t, "sum(rate(cache_requests_total[1m0s]))", res.Expr) require.Equal(t, 15*time.Second, res.Step) @@ -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{ diff --git a/pkg/promlib/querydata/request.go b/pkg/promlib/querydata/request.go index ff3cefc4458..a721907640b 100644 --- a/pkg/promlib/querydata/request.go +++ b/pkg/promlib/querydata/request.go @@ -100,10 +100,7 @@ func (s *QueryData) Execute(ctx context.Context, req *backend.QueryDataRequest) Responses: backend.Responses{}, } - var ( - hasPromQLScopeFeatureFlag = s.featureToggles.IsEnabled("promQLScope") - m sync.Mutex - ) + var m sync.Mutex concurrentQueryCount, err := req.PluginContext.GrafanaConfig.ConcurrentQueryCount() if err != nil { @@ -113,7 +110,7 @@ func (s *QueryData) Execute(ctx context.Context, req *backend.QueryDataRequest) _ = concurrency.ForEachJob(ctx, len(req.Queries), concurrentQueryCount, func(ctx context.Context, idx int) error { query := req.Queries[idx] - r := s.handleQuery(ctx, query, fromAlert, hasPromQLScopeFeatureFlag) + r := s.handleQuery(ctx, query, fromAlert) if r != nil { m.Lock() result.Responses[query.RefID] = *r @@ -125,11 +122,10 @@ func (s *QueryData) Execute(ctx context.Context, req *backend.QueryDataRequest) return &result, nil } -func (s *QueryData) handleQuery(ctx context.Context, bq backend.DataQuery, fromAlert, - hasPromQLScopeFeatureFlag bool) *backend.DataResponse { +func (s *QueryData) handleQuery(ctx context.Context, bq backend.DataQuery, fromAlert bool) *backend.DataResponse { traceCtx, span := s.tracer.Start(ctx, "datasource.prometheus") defer span.End() - query, err := models.Parse(ctx, s.log, span, bq, s.TimeInterval, s.intervalCalculator, fromAlert, hasPromQLScopeFeatureFlag) + query, err := models.Parse(ctx, s.log, span, bq, s.TimeInterval, s.intervalCalculator, fromAlert) if err != nil { return &backend.DataResponse{ Error: err, diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 01b37ad9386..cdd14fc665e 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -830,15 +830,6 @@ var ( HideFromDocs: true, HideFromAdminPage: true, }, - { - Name: "promQLScope", - Description: "In-development feature that will allow injection of labels into prometheus queries.", - Stage: FeatureStageGeneralAvailability, - Owner: grafanaOSSBigTent, - Expression: "true", - HideFromDocs: true, - HideFromAdminPage: true, - }, { Name: "logQLScope", Description: "In-development feature that will allow injection of labels into loki queries.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 88af45a4d15..2a20f2e8a0b 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -109,7 +109,6 @@ alertingSaveStateCompressed,preview,@grafana/alerting-squad,false,false,false scopeApi,experimental,@grafana/grafana-app-platform-squad,false,false,false useScopeSingleNodeEndpoint,experimental,@grafana/grafana-operator-experience-squad,false,false,true useMultipleScopeNodesEndpoint,experimental,@grafana/grafana-operator-experience-squad,false,false,true -promQLScope,GA,@grafana/oss-big-tent,false,false,false logQLScope,privatePreview,@grafana/observability-logs,false,false,false sqlExpressions,preview,@grafana/grafana-datasources-core-services,false,false,false sqlExpressionsColumnAutoComplete,experimental,@grafana/datapro,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index f26a754d574..ea44f2f188c 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -447,10 +447,6 @@ const ( // Makes the frontend use the 'names' param for fetching multiple scope nodes at once FlagUseMultipleScopeNodesEndpoint = "useMultipleScopeNodesEndpoint" - // FlagPromQLScope - // In-development feature that will allow injection of labels into prometheus queries. - FlagPromQLScope = "promQLScope" - // FlagLogQLScope // In-development feature that will allow injection of labels into loki queries. FlagLogQLScope = "logQLScope" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index caf8239adce..003da45e0f8 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2960,7 +2960,8 @@ "metadata": { "name": "promQLScope", "resourceVersion": "1753448760331", - "creationTimestamp": "2024-01-29T20:22:17Z" + "creationTimestamp": "2024-01-29T20:22:17Z", + "deletionTimestamp": "2025-10-05T00:24:17Z" }, "spec": { "description": "In-development feature that will allow injection of labels into prometheus queries.",