diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts index 3fbbe32a06c..fcb59be0bce 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts @@ -1307,8 +1307,14 @@ describe('queryHasFilter()', () => { describe('addAdhocFilters', () => { describe('with invalid filters', () => { + let ds: ElasticDatasource, templateSrv: TemplateSrv; + beforeEach(() => { + const context = getTestContext(); + ds = context.ds; + templateSrv = context.templateSrv; + }); + it('should filter out ad hoc filter without key', () => { - const { ds, templateSrv } = getTestContext(); jest.mocked(templateSrv.getAdhocFilters).mockReturnValue([{ key: '', operator: '=', value: 'a', condition: '' }]); const query = ds.addAdHocFilters('foo:"bar"'); @@ -1316,7 +1322,6 @@ describe('addAdhocFilters', () => { }); it('should filter out ad hoc filter without value', () => { - const { ds, templateSrv } = getTestContext(); jest.mocked(templateSrv.getAdhocFilters).mockReturnValue([{ key: 'a', operator: '=', value: '', condition: '' }]); const query = ds.addAdHocFilters('foo:"bar"'); @@ -1324,7 +1329,6 @@ describe('addAdhocFilters', () => { }); it('should filter out filter ad hoc filter with invalid operator', () => { - const { ds, templateSrv } = getTestContext(); jest.mocked(templateSrv.getAdhocFilters).mockReturnValue([{ key: 'a', operator: 'A', value: '', condition: '' }]); const query = ds.addAdHocFilters('foo:"bar"'); @@ -1349,10 +1353,50 @@ describe('addAdhocFilters', () => { }); it('should correctly add 1 ad hoc filter when query is empty', () => { - const query = ds.addAdHocFilters(''); - expect(query).toBe('test:"test1"'); + expect(ds.addAdHocFilters('')).toBe('test:"test1"'); + expect(ds.addAdHocFilters(' ')).toBe('test:"test1"'); + expect(ds.addAdHocFilters(' ')).toBe('test:"test1"'); }); + it('should not fail if the filter value is a number', () => { + jest + .mocked(templateSrvMock.getAdhocFilters) + // @ts-expect-error + .mockReturnValue([{ key: 'key', operator: '=', value: 1, condition: '' }]); + expect(ds.addAdHocFilters('')).toBe('key:"1"'); + }); + + it.each(['=', '!=', '=~', '!~', '>', '<', '', ''])( + `should properly build queries with '%s' filters`, + (operator: string) => { + jest + .mocked(templateSrvMock.getAdhocFilters) + .mockReturnValue([{ key: 'key', operator, value: 'value', condition: '' }]); + + const query = ds.addAdHocFilters('foo:"bar"'); + switch (operator) { + case '=': + expect(query).toBe('foo:"bar" AND key:"value"'); + break; + case '!=': + expect(query).toBe('foo:"bar" AND -key:"value"'); + break; + case '=~': + expect(query).toBe('foo:"bar" AND key:/value/'); + break; + case '!~': + expect(query).toBe('foo:"bar" AND -key:/value/'); + break; + case '>': + expect(query).toBe('foo:"bar" AND key:>value'); + break; + case '<': + expect(query).toBe('foo:"bar" AND key: { jest .mocked(templateSrvMock.getAdhocFilters) diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 3f49d181e77..513c5393446 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -56,13 +56,7 @@ import { } from './components/QueryEditor/MetricAggregationsEditor/aggregations'; import { metricAggregationConfig } from './components/QueryEditor/MetricAggregationsEditor/utils'; import { isMetricAggregationWithMeta } from './guards'; -import { - addFilterToQuery, - escapeFilter, - escapeFilterValue, - queryHasFilter, - removeFilterFromQuery, -} from './modifyQuery'; +import { addAddHocFilter, addFilterToQuery, queryHasFilter, removeFilterFromQuery } from './modifyQuery'; import { trackAnnotationQuery, trackQuery } from './tracking'; import { Logs, @@ -955,35 +949,11 @@ export class ElasticDatasource if (adhocFilters.length === 0) { return query; } - const esFilters = adhocFilters.map((filter) => { - let { key, operator, value } = filter; - if (!key || !value) { - return; - } - /** - * Keys and values in ad hoc filters may contain characters such as - * colons, which needs to be escaped. - */ - key = escapeFilter(key); - value = escapeFilterValue(value); - switch (operator) { - case '=': - return `${key}:"${value}"`; - case '!=': - return `-${key}:"${value}"`; - case '=~': - return `${key}:/${value}/`; - case '!~': - return `-${key}:/${value}/`; - case '>': - return `${key}:>${value}`; - case '<': - return `${key}:<${value}`; - } - return; + let finalQuery = query; + adhocFilters.forEach((filter) => { + finalQuery = addAddHocFilter(finalQuery, filter); }); - const finalQuery = [query, ...esFilters].filter((f) => f).join(' AND '); return finalQuery; } diff --git a/public/app/plugins/datasource/elasticsearch/modifyQuery.ts b/public/app/plugins/datasource/elasticsearch/modifyQuery.ts index 97c271ab05d..f4206041f98 100644 --- a/public/app/plugins/datasource/elasticsearch/modifyQuery.ts +++ b/public/app/plugins/datasource/elasticsearch/modifyQuery.ts @@ -1,6 +1,8 @@ import { isEqual } from 'lodash'; import lucene, { AST, BinaryAST, LeftOnlyAST, NodeTerm } from 'lucene'; +import { AdHocVariableFilter } from '@grafana/data'; + type ModifierType = '' | '-'; /** @@ -65,7 +67,59 @@ export function addFilterToQuery(query: string, key: string, value: string, modi value = lucene.phrase.escape(value); const filter = `${modifier}${key}:"${value}"`; - return query === '' ? filter : `${query} AND ${filter}`; + return concatenate(query, filter); +} + +/** + * Merge a query with a filter. + */ +function concatenate(query: string, filter: string, condition = 'AND'): string { + if (!filter) { + return query; + } + return query.trim() === '' ? filter : `${query} ${condition} ${filter}`; +} + +/** + * Adds a label:"value" expression to the query. + */ +export function addAddHocFilter(query: string, filter: AdHocVariableFilter): string { + if (!filter.key || !filter.value) { + return query; + } + + filter = { + ...filter, + // Type is defined as string, but it can be a number. + value: filter.value.toString(), + }; + + const equalityFilters = ['=', '!=']; + if (equalityFilters.includes(filter.operator)) { + return addFilterToQuery(query, filter.key, filter.value, filter.operator === '=' ? '' : '-'); + } + /** + * Keys and values in ad hoc filters may contain characters such as + * colons, which needs to be escaped. + */ + const key = escapeFilter(filter.key); + const value = escapeFilterValue(filter.value); + let addHocFilter = ''; + switch (filter.operator) { + case '=~': + addHocFilter = `${key}:/${value}/`; + break; + case '!~': + addHocFilter = `-${key}:/${value}/`; + break; + case '>': + addHocFilter = `${key}:>${value}`; + break; + case '<': + addHocFilter = `${key}:<${value}`; + break; + } + return concatenate(query, addHocFilter); } /**