Skip to content

Commit ab676ce

Browse files
authored
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
1 parent 5edbdb7 commit ab676ce

File tree

3 files changed

+165
-1
lines changed

3 files changed

+165
-1
lines changed

packages/grafana-runtime/src/utils/DataSourceWithBackend.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ class DataSourceWithBackend<
181181
if (datasource.uid?.length) {
182182
dsUIDs.add(datasource.uid);
183183
}
184+
184185
return {
185186
...(shouldApplyTemplateVariables ? this.applyTemplateVariables(q, request.scopedVars, request.filters) : q),
186187
datasource,

public/app/plugins/datasource/loki/datasource.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,6 +1809,162 @@ describe('LokiDatasource', () => {
18091809
});
18101810
});
18111811

1812+
describe('scopes application', () => {
1813+
let ds: LokiDatasource;
1814+
let origBackendSrv: BackendSrv;
1815+
1816+
beforeEach(() => {
1817+
origBackendSrv = getBackendSrv();
1818+
ds = createLokiDatasource(templateSrvStub);
1819+
// Enable the required feature toggles
1820+
config.featureToggles.scopeFilters = true;
1821+
config.featureToggles.logQLScope = true;
1822+
});
1823+
1824+
afterEach(() => {
1825+
setBackendSrv(origBackendSrv);
1826+
// Reset feature toggles to false
1827+
config.featureToggles.scopeFilters = false;
1828+
config.featureToggles.logQLScope = false;
1829+
});
1830+
1831+
it('should apply scopes to queries when feature toggles are enabled', async () => {
1832+
const mockScopes = [
1833+
{
1834+
metadata: { name: 'test-scope' },
1835+
spec: {
1836+
title: 'Test Scope',
1837+
type: 'test',
1838+
description: 'Test scope description',
1839+
category: 'test-category',
1840+
filters: [
1841+
{ key: 'environment', value: 'production', operator: 'equals' as const },
1842+
{ key: 'service', value: 'api', operator: 'equals' as const },
1843+
],
1844+
},
1845+
},
1846+
];
1847+
1848+
const query: DataQueryRequest<LokiQuery> = {
1849+
...baseRequestOptions,
1850+
targets: [{ expr: '{job="grafana"}', refId: 'A' }],
1851+
scopes: mockScopes,
1852+
};
1853+
1854+
const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse }));
1855+
setBackendSrv({ ...origBackendSrv, fetch: fetchMock });
1856+
1857+
await ds.query(query).pipe(take(1)).toPromise();
1858+
1859+
expect(fetchMock).toHaveBeenCalledWith(
1860+
expect.objectContaining({
1861+
data: expect.objectContaining({
1862+
queries: expect.arrayContaining([
1863+
expect.objectContaining({
1864+
scopes: [
1865+
{ key: 'environment', value: 'production', operator: 'equals' },
1866+
{ key: 'service', value: 'api', operator: 'equals' },
1867+
],
1868+
}),
1869+
]),
1870+
}),
1871+
})
1872+
);
1873+
});
1874+
1875+
it('should not apply scopes when feature toggles are disabled', async () => {
1876+
// Disable the required feature toggles
1877+
config.featureToggles.scopeFilters = false;
1878+
config.featureToggles.logQLScope = false;
1879+
1880+
const mockScopes = [
1881+
{
1882+
metadata: { name: 'test-scope' },
1883+
spec: {
1884+
title: 'Test Scope',
1885+
type: 'test',
1886+
description: 'Test scope description',
1887+
category: 'test-category',
1888+
filters: [{ key: 'environment', value: 'production', operator: 'equals' as const }],
1889+
},
1890+
},
1891+
];
1892+
1893+
const query: DataQueryRequest<LokiQuery> = {
1894+
...baseRequestOptions,
1895+
targets: [{ expr: '{job="grafana"}', refId: 'A' }],
1896+
scopes: mockScopes,
1897+
};
1898+
1899+
const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse }));
1900+
setBackendSrv({ ...origBackendSrv, fetch: fetchMock });
1901+
1902+
await ds.query(query).pipe(take(1)).toPromise();
1903+
1904+
expect(fetchMock).toHaveBeenCalledWith(
1905+
expect.objectContaining({
1906+
data: expect.objectContaining({
1907+
queries: expect.arrayContaining([
1908+
expect.objectContaining({
1909+
scopes: undefined,
1910+
}),
1911+
]),
1912+
}),
1913+
})
1914+
);
1915+
});
1916+
1917+
it('should handle empty scopes array', async () => {
1918+
const query: DataQueryRequest<LokiQuery> = {
1919+
...baseRequestOptions,
1920+
targets: [{ expr: '{job="grafana"}', refId: 'A' }],
1921+
scopes: [],
1922+
};
1923+
1924+
const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse }));
1925+
setBackendSrv({ ...origBackendSrv, fetch: fetchMock });
1926+
1927+
await ds.query(query).pipe(take(1)).toPromise();
1928+
1929+
expect(fetchMock).toHaveBeenCalledWith(
1930+
expect.objectContaining({
1931+
data: expect.objectContaining({
1932+
queries: expect.arrayContaining([
1933+
expect.objectContaining({
1934+
scopes: [],
1935+
}),
1936+
]),
1937+
}),
1938+
})
1939+
);
1940+
});
1941+
1942+
it('should handle undefined scopes', async () => {
1943+
const query: DataQueryRequest<LokiQuery> = {
1944+
...baseRequestOptions,
1945+
targets: [{ expr: '{job="grafana"}', refId: 'A' }],
1946+
scopes: undefined,
1947+
};
1948+
1949+
const fetchMock = jest.fn().mockReturnValue(of({ data: testLogsResponse }));
1950+
setBackendSrv({ ...origBackendSrv, fetch: fetchMock });
1951+
1952+
await ds.query(query).pipe(take(1)).toPromise();
1953+
1954+
expect(fetchMock).toHaveBeenCalledWith(
1955+
expect.objectContaining({
1956+
data: expect.objectContaining({
1957+
queries: expect.arrayContaining([
1958+
expect.objectContaining({
1959+
scopes: undefined,
1960+
}),
1961+
]),
1962+
}),
1963+
})
1964+
);
1965+
});
1966+
});
1967+
18121968
describe('getQueryStats', () => {
18131969
let ds: LokiDatasource;
18141970
let query: LokiQuery;

public/app/plugins/datasource/loki/datasource.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,14 @@ export class LokiDatasource
299299
query(request: DataQueryRequest<LokiQuery>): Observable<DataQueryResponse> {
300300
const queries = request.targets
301301
.map(getNormalizedLokiQuery) // used to "fix" the deprecated `.queryType` prop
302-
.map((q) => ({ ...q, maxLines: q.maxLines ?? this.maxLines }));
302+
.map((q) => ({
303+
...q,
304+
maxLines: q.maxLines ?? this.maxLines,
305+
scopes:
306+
config.featureToggles.scopeFilters && config.featureToggles.logQLScope
307+
? request.scopes?.flatMap((scope) => scope.spec.filters)
308+
: undefined,
309+
}));
303310

304311
const fixedRequest: DataQueryRequest<LokiQuery> = {
305312
...request,

0 commit comments

Comments
 (0)