From ab676ce035cd22633292a17cb27b21249ed6086d Mon Sep 17 00:00:00 2001 From: Tobias Skarhed <1438972+tskarhed@users.noreply.github.com> Date: Fri, 4 Jul 2025 18:35:52 +0200 Subject: [PATCH] Loki: Add scope filters to queries on the frontend (#104638) * Add scopes to queries in DataSourceWithBackend * Remove Prometheus-specific solution * Readd prometheus support * move scopes reordering ti loki ds * Add tests and logQLScope feature flag * Move featureToggles to before/after each * Remove irrelevant file change --- .../src/utils/DataSourceWithBackend.ts | 1 + .../datasource/loki/datasource.test.ts | 156 ++++++++++++++++++ .../app/plugins/datasource/loki/datasource.ts | 9 +- 3 files changed, 165 insertions(+), 1 deletion(-) diff --git a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts index ccfe41b0c9d..ff95bc1c1e6 100644 --- a/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts +++ b/packages/grafana-runtime/src/utils/DataSourceWithBackend.ts @@ -181,6 +181,7 @@ class DataSourceWithBackend< if (datasource.uid?.length) { dsUIDs.add(datasource.uid); } + return { ...(shouldApplyTemplateVariables ? this.applyTemplateVariables(q, request.scopedVars, request.filters) : q), datasource, diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index c63332522cd..8d49fe6f27b 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -1809,6 +1809,162 @@ describe('LokiDatasource', () => { }); }); + describe('scopes application', () => { + let ds: LokiDatasource; + let origBackendSrv: BackendSrv; + + beforeEach(() => { + origBackendSrv = getBackendSrv(); + ds = createLokiDatasource(templateSrvStub); + // Enable the required feature toggles + config.featureToggles.scopeFilters = true; + config.featureToggles.logQLScope = true; + }); + + afterEach(() => { + setBackendSrv(origBackendSrv); + // Reset feature toggles to false + config.featureToggles.scopeFilters = false; + config.featureToggles.logQLScope = false; + }); + + it('should apply scopes to queries when feature toggles are enabled', async () => { + const mockScopes = [ + { + metadata: { name: 'test-scope' }, + spec: { + title: 'Test Scope', + type: 'test', + description: 'Test scope description', + category: 'test-category', + filters: [ + { key: 'environment', value: 'production', operator: 'equals' as const }, + { key: 'service', value: 'api', operator: 'equals' as const }, + ], + }, + }, + ]; + + const query: DataQueryRequest = { + ...baseRequestOptions, + targets: [{ expr: '{job="grafana"}', refId: 'A' }], + scopes: mockScopes, + }; + + const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse })); + setBackendSrv({ ...origBackendSrv, fetch: fetchMock }); + + await ds.query(query).pipe(take(1)).toPromise(); + + expect(fetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + queries: expect.arrayContaining([ + expect.objectContaining({ + scopes: [ + { key: 'environment', value: 'production', operator: 'equals' }, + { key: 'service', value: 'api', operator: 'equals' }, + ], + }), + ]), + }), + }) + ); + }); + + it('should not apply scopes when feature toggles are disabled', async () => { + // Disable the required feature toggles + config.featureToggles.scopeFilters = false; + config.featureToggles.logQLScope = false; + + const mockScopes = [ + { + metadata: { name: 'test-scope' }, + spec: { + title: 'Test Scope', + type: 'test', + description: 'Test scope description', + category: 'test-category', + filters: [{ key: 'environment', value: 'production', operator: 'equals' as const }], + }, + }, + ]; + + const query: DataQueryRequest = { + ...baseRequestOptions, + targets: [{ expr: '{job="grafana"}', refId: 'A' }], + scopes: mockScopes, + }; + + const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse })); + setBackendSrv({ ...origBackendSrv, fetch: fetchMock }); + + await ds.query(query).pipe(take(1)).toPromise(); + + expect(fetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + queries: expect.arrayContaining([ + expect.objectContaining({ + scopes: undefined, + }), + ]), + }), + }) + ); + }); + + it('should handle empty scopes array', async () => { + const query: DataQueryRequest = { + ...baseRequestOptions, + targets: [{ expr: '{job="grafana"}', refId: 'A' }], + scopes: [], + }; + + const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse })); + setBackendSrv({ ...origBackendSrv, fetch: fetchMock }); + + await ds.query(query).pipe(take(1)).toPromise(); + + expect(fetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + queries: expect.arrayContaining([ + expect.objectContaining({ + scopes: [], + }), + ]), + }), + }) + ); + }); + + it('should handle undefined scopes', async () => { + const query: DataQueryRequest = { + ...baseRequestOptions, + targets: [{ expr: '{job="grafana"}', refId: 'A' }], + scopes: undefined, + }; + + const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse })); + setBackendSrv({ ...origBackendSrv, fetch: fetchMock }); + + await ds.query(query).pipe(take(1)).toPromise(); + + expect(fetchMock).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ + queries: expect.arrayContaining([ + expect.objectContaining({ + scopes: undefined, + }), + ]), + }), + }) + ); + }); + }); + describe('getQueryStats', () => { let ds: LokiDatasource; let query: LokiQuery; diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 4ded88df124..468b52b005e 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -299,7 +299,14 @@ export class LokiDatasource query(request: DataQueryRequest): Observable { const queries = request.targets .map(getNormalizedLokiQuery) // used to "fix" the deprecated `.queryType` prop - .map((q) => ({ ...q, maxLines: q.maxLines ?? this.maxLines })); + .map((q) => ({ + ...q, + maxLines: q.maxLines ?? this.maxLines, + scopes: + config.featureToggles.scopeFilters && config.featureToggles.logQLScope + ? request.scopes?.flatMap((scope) => scope.spec.filters) + : undefined, + })); const fixedRequest: DataQueryRequest = { ...request,