Loki: Add support for ad-hoc filtering in dashboard (#36393)

* Add support for ad-hoc filtering in dashboards

* Add tests

* Add documentation

* Update docs/sources/variables/variable-types/add-ad-hoc-filters.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Remove unused import

* Refactor and simplify

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
Ivana Huckova 2021-07-06 04:30:27 -04:00 committed by GitHub
parent 666656c925
commit b77f6d59bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 102 additions and 7 deletions

View File

@ -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" >}}).

View File

@ -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.
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.

View File

@ -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' }],

View File

@ -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<LokiQuery, LokiOptions> {
};
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<LokiQuery, LokiOptions> {
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<LokiQuery, LokiOptions> {
// 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 }) {