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
This commit is contained in:
Tobias Skarhed
2025-07-04 18:35:52 +02:00
committed by GitHub
parent 5edbdb7c4f
commit ab676ce035
3 changed files with 165 additions and 1 deletions
@@ -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,
@@ -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<LokiQuery> = {
...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<LokiQuery> = {
...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<LokiQuery> = {
...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<LokiQuery> = {
...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;
@@ -299,7 +299,14 @@ export class LokiDatasource
query(request: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> {
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<LokiQuery> = {
...request,