diff --git a/docs/sources/datasources/loki.md b/docs/sources/datasources/loki.md index e30071c7e2b..d9167829151 100644 --- a/docs/sources/datasources/loki.md +++ b/docs/sources/datasources/loki.md @@ -154,6 +154,9 @@ provides the following functions you can use in the `Query` input field. | `label_values(label)` | Returns a list of label values for the `label`. | | `label_values(log stream selector, label)` | Returns a list of label values for the `label` in the specified `log stream selector`.| +### Ad hoc filters variable +Loki supports the special ad hoc filters variable type. It allows you to specify any number of label/value filters on the fly. These filters are automatically applied to all your Loki queries. + ### Using interval and range variables You can use some global built-in variables in query variables; `$__interval`, `$__interval_ms`, `$__range`, `$__range_s` and `$__range_ms`. For more information, refer to [Global built-in variables]({{< relref "../variables/variable-types/global-variables.md" >}}). diff --git a/docs/sources/variables/variable-types/add-ad-hoc-filters.md b/docs/sources/variables/variable-types/add-ad-hoc-filters.md index 074c494c044..4b3f92559a1 100644 --- a/docs/sources/variables/variable-types/add-ad-hoc-filters.md +++ b/docs/sources/variables/variable-types/add-ad-hoc-filters.md @@ -8,7 +8,7 @@ weight = 700 _Ad hoc filters_ allow you to add key/value filters that are automatically added to all metric queries that use the specified data source. Unlike other variables, you do not use ad hoc filters in queries. Instead, you use ad hoc filters to write filters for existing queries. -> **Note:** Ad hoc filter variables only work with InfluxDB, Prometheus, and Elasticsearch data sources. +> **Note:** Ad hoc filter variables only work with Prometheus, Loki, InfluxDB, and Elasticsearch data sources. ## Enter General options @@ -29,4 +29,4 @@ _Ad hoc filters_ allow you to add key/value filters that are automatically added ## Create ad hoc filters -Ad hoc filters are one of the most complex and flexible variable options available. Instead of a regular list of variable options, this variable allows you to build a dashboard-wide ad hoc query. Filters you apply in this manner are applied to all panels on the dashboard. \ No newline at end of file +Ad hoc filters are one of the most complex and flexible variable options available. Instead of a regular list of variable options, this variable allows you to build a dashboard-wide ad hoc query. Filters you apply in this manner are applied to all panels on the dashboard. diff --git a/public/app/plugins/datasource/loki/datasource.test.ts b/public/app/plugins/datasource/loki/datasource.test.ts index ffe64f18b50..e2fd3380d19 100644 --- a/public/app/plugins/datasource/loki/datasource.test.ts +++ b/public/app/plugins/datasource/loki/datasource.test.ts @@ -34,6 +34,7 @@ const timeSrvStub = ({ } as unknown) as TimeSrv; const templateSrvStub = { + getAdhocFilters: jest.fn(() => [] as any[]), replace: jest.fn((a: string, ...rest: any) => a), }; @@ -334,6 +335,68 @@ describe('LokiDatasource', () => { }); }); + describe('When using adhoc filters', () => { + const DEFAULT_EXPR = 'rate({bar="baz", job="foo"} |= "bar" [5m])'; + const options = { + targets: [{ expr: DEFAULT_EXPR }], + }; + const originalAdhocFiltersMock = templateSrvStub.getAdhocFilters(); + const ds = new LokiDatasource({} as any, templateSrvStub as any, timeSrvStub as any); + ds.runRangeQuery = jest.fn(() => of({ data: [] })); + + afterAll(() => { + templateSrvStub.getAdhocFilters.mockReturnValue(originalAdhocFiltersMock); + }); + + it('should not modify expression with no filters', async () => { + await ds.query(options as any).toPromise(); + expect(ds.runRangeQuery).toBeCalledWith({ expr: DEFAULT_EXPR }, expect.anything(), expect.anything()); + }); + + it('should add filters to expression', async () => { + templateSrvStub.getAdhocFilters.mockReturnValue([ + { + key: 'k1', + operator: '=', + value: 'v1', + }, + { + key: 'k2', + operator: '!=', + value: 'v2', + }, + ]); + + await ds.query(options as any).toPromise(); + expect(ds.runRangeQuery).toBeCalledWith( + { expr: 'rate({bar="baz",job="foo",k1="v1",k2!="v2"} |= "bar" [5m])' }, + expect.anything(), + expect.anything() + ); + }); + + it('should add escaping if needed to regex filter expressions', async () => { + templateSrvStub.getAdhocFilters.mockReturnValue([ + { + key: 'k1', + operator: '=~', + value: 'v.*', + }, + { + key: 'k2', + operator: '=~', + value: `v'.*`, + }, + ]); + await ds.query(options as any).toPromise(); + expect(ds.runRangeQuery).toBeCalledWith( + { expr: 'rate({bar="baz",job="foo",k1=~"v.*",k2=~"v\\\\\'.*"} |= "bar" [5m])' }, + expect.anything(), + expect.anything() + ); + }); + }); + describe('__range, __range_s and __range_ms variables', () => { const options = { targets: [{ expr: 'rate(process_cpu_seconds_total[$__range])', refId: 'A' }], diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 379a73a17e4..5e6e236e456 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -25,7 +25,8 @@ import { ScopedVars, TimeRange, } from '@grafana/data'; -import { getTemplateSrv, TemplateSrv, BackendSrvRequest, FetchError, getBackendSrv } from '@grafana/runtime'; +import { BackendSrvRequest, FetchError, getBackendSrv } from '@grafana/runtime'; +import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; import { addLabelToQuery } from 'app/plugins/datasource/prometheus/add_label_to_query'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { convertToWebSocketUrl } from 'app/core/utils/explore'; @@ -102,10 +103,13 @@ export class LokiDatasource extends DataSourceApi { }; const filteredTargets = options.targets .filter((target) => target.expr && !target.hide) - .map((target) => ({ - ...target, - expr: this.templateSrv.replace(target.expr, scopedVars, this.interpolateQueryExpr), - })); + .map((target) => { + const expr = this.addAdHocFilters(target.expr); + return { + ...target, + expr: this.templateSrv.replace(expr, scopedVars, this.interpolateQueryExpr), + }; + }); for (const target of filteredTargets) { if (target.instant) { @@ -379,6 +383,15 @@ export class LokiDatasource extends DataSourceApi { return Array.from(streams); } + // By implementing getTagKeys and getTagValues we add ad-hoc filtters functionality + async getTagKeys() { + return await this.labelNamesQuery(); + } + + async getTagValues(options: any = {}) { + return await this.labelValuesQuery(options.key); + } + interpolateQueryExpr(value: any, variable: any) { // if no multi or include all do not regexEscape if (!variable.multi && !variable.includeAll) { @@ -628,6 +641,22 @@ export class LokiDatasource extends DataSourceApi { // The min interval is set to 1ms return Math.max(interval, 1); } + + addAdHocFilters(queryExpr: string) { + const adhocFilters = this.templateSrv.getAdhocFilters(this.name); + let expr = queryExpr; + + expr = adhocFilters.reduce((acc: string, filter: { key?: any; operator?: any; value?: any }) => { + const { key, operator } = filter; + let { value } = filter; + if (operator === '=~' || operator === '!~') { + value = lokiRegularEscape(value); + } + return addLabelToQuery(acc, key, value, operator); + }, expr); + + return expr; + } } export function renderTemplate(aliasPattern: string, aliasData: { [key: string]: string }) {