diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.tsx index fee68d5c771..1391f620691 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.tsx @@ -64,7 +64,9 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVa const templateVariables = getTemplateSrv().getVariables(); useEffect(() => { - setTraceQlQuery(datasource.languageProvider.generateQueryFromFilters(interpolateFilters(query.filters || []))); + setTraceQlQuery( + datasource.languageProvider.generateQueryFromFilters({ traceqlFilters: interpolateFilters(query.filters || []) }) + ); }, [datasource.languageProvider, query, templateVariables]); const findFilter = useCallback((id: string) => query.filters?.find((f) => f.id === id), [query.filters]); @@ -123,7 +125,9 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVa return traceQlQuery; } const filtersAfterRemoval = query.filters?.filter((f) => f.id !== filter.id) || []; - return datasource.languageProvider.generateQueryFromFilters(interpolateFilters(filtersAfterRemoval || [])); + return datasource.languageProvider.generateQueryFromFilters({ + traceqlFilters: interpolateFilters(filtersAfterRemoval || []), + }); }; return ( @@ -260,7 +264,9 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVa }); onClearResults(); - const traceQlQuery = datasource.languageProvider.generateQueryFromFilters(query.filters || []); + const traceQlQuery = datasource.languageProvider.generateQueryFromFilters({ + traceqlFilters: query.filters || [], + }); onChange({ ...query, query: traceQlQuery, diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.test.ts b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.test.ts index 57c2dadc2b1..df4974921aa 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.test.ts +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.test.ts @@ -7,7 +7,6 @@ import { intrinsics } from '../traceql/traceql'; import { filterToQuerySection, - generateQueryFromAdHocFilters, getAllTags, getFilteredTags, getIntrinsicTags, @@ -22,34 +21,6 @@ const datasource: TempoDatasource = { } as unknown as TempoDatasource; const lp = new TempoLanguageProvider(datasource); -describe('generateQueryFromAdHocFilters generates the correct query for', () => { - it('an empty array', () => { - expect(generateQueryFromAdHocFilters([], lp)).toBe('{}'); - }); - - it('a filter with values', () => { - expect(generateQueryFromAdHocFilters([{ key: 'footag', operator: '=', value: 'foovalue' }], lp)).toBe( - '{footag="foovalue"}' - ); - }); - - it('two filters with values', () => { - expect( - generateQueryFromAdHocFilters( - [ - { key: 'footag', operator: '=', value: 'foovalue' }, - { key: 'bartag', operator: '=', value: '0' }, - ], - lp - ) - ).toBe('{footag="foovalue" && bartag=0}'); - }); - - it('a filter with intrinsic values', () => { - expect(generateQueryFromAdHocFilters([{ key: 'kind', operator: '=', value: 'server' }], lp)).toBe('{kind=server}'); - }); -}); - describe('gets correct tags', () => { const datasource: TempoDatasource = { search: { diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts index 5b4313ce6f2..1a588b4a717 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts @@ -1,6 +1,6 @@ import { startCase, uniq } from 'lodash'; -import { AdHocVariableFilter, ScopedVars, SelectableValue } from '@grafana/data'; +import { ScopedVars, SelectableValue } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { VariableFormatID } from '@grafana/schema'; @@ -81,23 +81,6 @@ export const filterToQuerySection = (f: TraceqlFilter, filters: TraceqlFilter[], return `${scopeHelper(f, lp)}${tagHelper(f, filters)}${f.operator}${valueHelper(f)}`; }; -export const generateQueryFromAdHocFilters = (filters: AdHocVariableFilter[], lp: TempoLanguageProvider) => { - return `{${filters - .filter((f) => f.key && f.operator && f.value) - .map((f) => `${f.key}${f.operator}${adHocValueHelper(f, lp)}`) - .join(' && ')}}`; -}; - -const adHocValueHelper = (f: AdHocVariableFilter, lp: TempoLanguageProvider) => { - if (lp.getIntrinsics().find((t) => t === f.key)) { - return f.value; - } - if (parseInt(f.value, 10).toString() === f.value) { - return f.value; - } - return `"${f.value}"`; -}; - export const getTagWithoutScope = (tag: string) => { return tag.replace(/^(event|instrumentation|link|resource|span)\./, ''); }; diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 0791334f1c1..2f60c94b354 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -36,7 +36,7 @@ import { } from '@grafana/runtime'; import { BarGaugeDisplayMode, TableCellDisplayMode, VariableFormatID } from '@grafana/schema'; -import { generateQueryFromAdHocFilters, getTagWithoutScope, interpolateFilters } from './SearchTraceQLEditor/utils'; +import { getTagWithoutScope, interpolateFilters } from './SearchTraceQLEditor/utils'; import { TempoVariableQuery, TempoVariableQueryType } from './VariableQueryEditor'; import { PrometheusDatasource, PromQuery } from './_importedDependencies/datasources/prometheus/types'; import { TagLimitOptions } from './configuration/TagLimitSettings'; @@ -239,7 +239,7 @@ export class TempoDatasource extends DataSourceWithBackend): Promise> { - const query = generateQueryFromAdHocFilters(options.filters, this.languageProvider); + const query = this.languageProvider.generateQueryFromFilters({ adhocFilters: options.filters }); return this.tagValuesQuery(options.key, query); } @@ -418,7 +418,10 @@ export class TempoDatasource extends DataSourceWithBackend 0) { const appliedQuery = this.applyVariables(traceqlSearchTargets[0], options.scopedVars); - const queryFromFilters = this.languageProvider.generateQueryFromFilters(appliedQuery.filters); + const queryFromFilters = this.languageProvider.generateQueryFromFilters({ + traceqlFilters: appliedQuery.filters, + adhocFilters: options.filters, + }); reportInteraction('grafana_traces_traceql_search_queried', { datasourceType: 'tempo', @@ -918,7 +921,7 @@ export class TempoDatasource extends DataSourceWithBackend { }); it('an empty array', () => { - expect(lp.generateQueryFromFilters([])).toBe('{}'); + expect(lp.generateQueryFromFilters({ traceqlFilters: [] })).toBe('{}'); }); it('a field without value', () => { - expect(lp.generateQueryFromFilters([{ id: 'foo', tag: 'footag', operator: '=' }])).toBe('{}'); + expect(lp.generateQueryFromFilters({ traceqlFilters: [{ id: 'foo', tag: 'footag', operator: '=' }] })).toBe('{}'); }); it('a field with value but without tag', () => { - expect(lp.generateQueryFromFilters([{ id: 'foo', value: 'foovalue', operator: '=' }])).toBe('{}'); + expect(lp.generateQueryFromFilters({ traceqlFilters: [{ id: 'foo', value: 'foovalue', operator: '=' }] })).toBe( + '{}' + ); }); it('a field with value and tag but without operator', () => { - expect(lp.generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: 'foovalue' }])).toBe('{}'); + expect(lp.generateQueryFromFilters({ traceqlFilters: [{ id: 'foo', tag: 'footag', value: 'foovalue' }] })).toBe( + '{}' + ); }); describe('generates correct query for duration when duration type', () => { it('not set', () => { expect( - lp.generateQueryFromFilters([ - { id: 'min-duration', operator: '>', valueType: 'duration', tag: 'duration', value: '100ms' }, - ]) + lp.generateQueryFromFilters({ + traceqlFilters: [ + { id: 'min-duration', operator: '>', valueType: 'duration', tag: 'duration', value: '100ms' }, + ], + }) ).toBe('{duration>100ms}'); }); it('set to span', () => { expect( - lp.generateQueryFromFilters([ - { id: 'min-duration', operator: '>', valueType: 'duration', tag: 'duration', value: '100ms' }, - { id: 'duration-type', value: 'span' }, - ]) + lp.generateQueryFromFilters({ + traceqlFilters: [ + { id: 'min-duration', operator: '>', valueType: 'duration', tag: 'duration', value: '100ms' }, + { id: 'duration-type', value: 'span' }, + ], + }) ).toBe('{duration>100ms}'); }); it('set to trace', () => { expect( - lp.generateQueryFromFilters([ - { id: 'min-duration', operator: '>', valueType: 'duration', tag: 'duration', value: '100ms' }, - { id: 'duration-type', value: 'trace' }, - ]) + lp.generateQueryFromFilters({ + traceqlFilters: [ + { id: 'min-duration', operator: '>', valueType: 'duration', tag: 'duration', value: '100ms' }, + { id: 'duration-type', value: 'trace' }, + ], + }) ).toBe('{traceDuration>100ms}'); }); }); it('a field with tag, operator and tag', () => { - expect(lp.generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: 'foovalue', operator: '=' }])).toBe( - '{.footag=foovalue}' - ); expect( - lp.generateQueryFromFilters([ - { id: 'foo', tag: 'footag', value: 'foovalue', operator: '=', valueType: 'string' }, - ]) + lp.generateQueryFromFilters({ + traceqlFilters: [{ id: 'foo', tag: 'footag', value: 'foovalue', operator: '=' }], + }) + ).toBe('{.footag=foovalue}'); + expect( + lp.generateQueryFromFilters({ + traceqlFilters: [{ id: 'foo', tag: 'footag', value: 'foovalue', operator: '=', valueType: 'string' }], + }) ).toBe('{.footag="foovalue"}'); }); it('a field with valueType as integer', () => { expect( - lp.generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: '1234', operator: '>', valueType: 'integer' }]) + lp.generateQueryFromFilters({ + traceqlFilters: [{ id: 'foo', tag: 'footag', value: '1234', operator: '>', valueType: 'integer' }], + }) ).toBe('{.footag>1234}'); }); it.each([['=~'], ['!~']])('a field with a regexp operator (%s)', (operator) => { expect( - lp.generateQueryFromFilters([ - { - id: 'span-name', - tag: 'name', - operator, - scope: TraceqlSearchScope.Span, - value: ['api/v2/variants/by-upc/(?P[\\s\\S]*)/$'], - valueType: 'string', - }, - ]) + lp.generateQueryFromFilters({ + traceqlFilters: [ + { + id: 'span-name', + tag: 'name', + operator, + scope: TraceqlSearchScope.Span, + value: ['api/v2/variants/by-upc/(?P[\\s\\S]*)/$'], + valueType: 'string', + }, + ], + }) ).toBe(`{name${operator}"api/v2/variants/by-upc/\\\\(\\\\?P\\\\[\\\\\\\\s\\\\\\\\S\\\\]\\\\*\\\\)/\\\\$"}`); }); it('two fields with everything filled in', () => { expect( - lp.generateQueryFromFilters([ - { id: 'foo', tag: 'footag', value: '1234', operator: '>=', valueType: 'integer' }, - { id: 'bar', tag: 'bartag', value: 'barvalue', operator: '=', valueType: 'string' }, - ]) + lp.generateQueryFromFilters({ + traceqlFilters: [ + { id: 'foo', tag: 'footag', value: '1234', operator: '>=', valueType: 'integer' }, + { id: 'bar', tag: 'bartag', value: 'barvalue', operator: '=', valueType: 'string' }, + ], + }) ).toBe('{.footag>=1234 && .bartag="barvalue"}'); }); it('two fields but one is missing a value', () => { expect( - lp.generateQueryFromFilters([ - { id: 'foo', tag: 'footag', value: '1234', operator: '>=', valueType: 'integer' }, - { id: 'bar', tag: 'bartag', operator: '=', valueType: 'string' }, - ]) + lp.generateQueryFromFilters({ + traceqlFilters: [ + { id: 'foo', tag: 'footag', value: '1234', operator: '>=', valueType: 'integer' }, + { id: 'bar', tag: 'bartag', operator: '=', valueType: 'string' }, + ], + }) ).toBe('{.footag>=1234}'); }); it('two fields but one is missing a value and the other a tag', () => { expect( - lp.generateQueryFromFilters([ - { id: 'foo', value: '1234', operator: '>=', valueType: 'integer' }, - { id: 'bar', tag: 'bartag', operator: '=', valueType: 'string' }, - ]) + lp.generateQueryFromFilters({ + traceqlFilters: [ + { id: 'foo', value: '1234', operator: '>=', valueType: 'integer' }, + { id: 'bar', tag: 'bartag', operator: '=', valueType: 'string' }, + ], + }) ).toBe('{}'); }); it('scope is unscoped', () => { expect( - lp.generateQueryFromFilters([ - { - id: 'foo', - tag: 'footag', - value: '1234', - operator: '>=', - scope: TraceqlSearchScope.Unscoped, - valueType: 'integer', - }, - ]) + lp.generateQueryFromFilters({ + traceqlFilters: [ + { + id: 'foo', + tag: 'footag', + value: '1234', + operator: '>=', + scope: TraceqlSearchScope.Unscoped, + valueType: 'integer', + }, + ], + }) ).toBe('{.footag>=1234}'); }); it('scope is span', () => { expect( - lp.generateQueryFromFilters([ - { - id: 'foo', - tag: 'footag', - value: '1234', - operator: '>=', - scope: TraceqlSearchScope.Span, - valueType: 'integer', - }, - ]) + lp.generateQueryFromFilters({ + traceqlFilters: [ + { + id: 'foo', + tag: 'footag', + value: '1234', + operator: '>=', + scope: TraceqlSearchScope.Span, + valueType: 'integer', + }, + ], + }) ).toBe('{span.footag>=1234}'); }); it('scope is resource', () => { expect( - lp.generateQueryFromFilters([ - { - id: 'foo', - tag: 'footag', - value: '1234', - operator: '>=', - scope: TraceqlSearchScope.Resource, - valueType: 'integer', - }, - ]) + lp.generateQueryFromFilters({ + traceqlFilters: [ + { + id: 'foo', + tag: 'footag', + value: '1234', + operator: '>=', + scope: TraceqlSearchScope.Resource, + valueType: 'integer', + }, + ], + }) ).toBe('{resource.footag>=1234}'); }); + describe('adhoc filters', () => { + it('mixes adhoc filters with trace ql', () => { + expect( + lp.generateQueryFromFilters({ + traceqlFilters: [ + { + id: 'foo', + tag: 'footag', + value: '1234', + operator: '>=', + scope: TraceqlSearchScope.Resource, + valueType: 'integer', + }, + ], + adhocFilters: [ + { + key: 'resource.name', + operator: '=', + value: 'foo', + }, + ], + }) + ).toBe('{resource.footag>=1234 && resource.name="foo"}'); + }); + it('an empty array', () => { + expect(lp.generateQueryFromFilters({ adhocFilters: [] })).toBe('{}'); + }); + + it('a filter with values', () => { + expect( + lp.generateQueryFromFilters({ adhocFilters: [{ key: 'footag', operator: '=', value: 'foovalue' }] }) + ).toBe('{footag="foovalue"}'); + }); + + it('two filters with values', () => { + expect( + lp.generateQueryFromFilters({ + adhocFilters: [ + { key: 'footag', operator: '=', value: 'foovalue' }, + { key: 'bartag', operator: '=', value: '0' }, + ], + }) + ).toBe('{footag="foovalue" && bartag=0}'); + }); + + it('a filter with enum intrinsic values', () => { + expect(lp.generateQueryFromFilters({ adhocFilters: [{ key: 'kind', operator: '=', value: 'server' }] })).toBe( + '{kind=server}' + ); + }); + + it('a filter with non-enum intrinsic values', () => { + expect( + lp.generateQueryFromFilters({ adhocFilters: [{ key: 'name', operator: '=', value: 'my-server' }] }) + ).toBe('{name="my-server"}'); + }); + }); }); const setup = (tagsV1?: string[], tagsV2?: Scope[]) => { diff --git a/public/app/plugins/datasource/tempo/language_provider.ts b/public/app/plugins/datasource/tempo/language_provider.ts index e8c4d202dde..6aa67bb750e 100644 --- a/public/app/plugins/datasource/tempo/language_provider.ts +++ b/public/app/plugins/datasource/tempo/language_provider.ts @@ -1,4 +1,4 @@ -import { LanguageProvider, SelectableValue } from '@grafana/data'; +import { AdHocVariableFilter, LanguageProvider, SelectableValue } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { VariableFormatID } from '@grafana/schema'; @@ -11,7 +11,7 @@ import { } from './SearchTraceQLEditor/utils'; import { TraceqlFilter, TraceqlSearchScope } from './dataquery.gen'; import { TempoDatasource } from './datasource'; -import { intrinsicsV1 } from './traceql/traceql'; +import { enumIntrinsics, intrinsicsV1 } from './traceql/traceql'; import { Scope } from './types'; // Limit maximum tags retrieved from the backend @@ -179,14 +179,48 @@ export default class TempoLanguageProvider extends LanguageProvider { return encodeURIComponent(encodeURIComponent(tag)); }; - generateQueryFromFilters(filters: TraceqlFilter[]) { + generateQueryFromFilters({ + traceqlFilters, + adhocFilters, + }: { + traceqlFilters?: TraceqlFilter[]; + adhocFilters?: AdHocVariableFilter[]; + }) { + if (!traceqlFilters && !adhocFilters) { + return ''; + } + + const allFilters = [ + ...this.generateQueryFromTraceQlFilters(traceqlFilters || []), + ...this.generateQueryFromAdHocFilters(adhocFilters || []), + ]; + + return `{${allFilters.join(' && ')}}`; + } + + private generateQueryFromTraceQlFilters(filters: TraceqlFilter[]) { if (!filters) { return ''; } - return `{${filters + return filters .filter((f) => f.tag && f.operator && f.value?.length) - .map((f) => filterToQuerySection(f, filters, this)) - .join(' && ')}}`; + .map((f) => filterToQuerySection(f, filters, this)); } + + private generateQueryFromAdHocFilters = (filters: AdHocVariableFilter[]) => { + return filters + .filter((f) => f.key && f.operator && f.value) + .map((f) => `${f.key}${f.operator}${this.adHocValueHelper(f)}`); + }; + + adHocValueHelper = (f: AdHocVariableFilter) => { + if (this.getIntrinsics().find((t) => t === f.key) && enumIntrinsics.includes(f.key)) { + return f.value; + } + if (parseInt(f.value, 10).toString() === f.value) { + return f.value; + } + return `"${f.value}"`; + }; } diff --git a/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx b/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx index c4c90c78041..f80f9986f93 100644 --- a/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx +++ b/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx @@ -22,7 +22,9 @@ export function QueryEditor(props: Props) { const styles = useStyles2(getStyles); const query = defaults(props.query, defaultQuery); const [showCopyFromSearchButton, setShowCopyFromSearchButton] = useState(() => { - const genQuery = props.datasource.languageProvider.generateQueryFromFilters(query.filters || []); + const genQuery = props.datasource.languageProvider.generateQueryFromFilters({ + traceqlFilters: query.filters || [], + }); return genQuery === query.query || genQuery === '{}'; }); @@ -50,7 +52,9 @@ export function QueryEditor(props: Props) { props.onClearResults(); props.onChange({ ...query, - query: props.datasource.languageProvider.generateQueryFromFilters(query.filters || []), + query: props.datasource.languageProvider.generateQueryFromFilters({ + traceqlFilters: query.filters || [], + }), }); setShowCopyFromSearchButton(true); }} diff --git a/public/app/plugins/datasource/tempo/traceql/traceql.ts b/public/app/plugins/datasource/tempo/traceql/traceql.ts index 1dea15994dd..00f54a0d25f 100644 --- a/public/app/plugins/datasource/tempo/traceql/traceql.ts +++ b/public/app/plugins/datasource/tempo/traceql/traceql.ts @@ -58,6 +58,8 @@ export const intrinsics = intrinsicsV1.concat([ ]); export const scopes: string[] = ['event', 'instrumentation', 'link', 'resource', 'span']; +export const enumIntrinsics = ['kind', 'span:kind', 'status', 'span:status']; + const aggregatorFunctions = ['avg', 'count', 'max', 'min', 'sum']; const functions = aggregatorFunctions.concat([ 'by',