Tempo: Add support for ad-hoc filters (#102448)

* Add support for ad-hoc filters

* Handle enum tags when using ad hoc filters
pull/102707/head
Piotr Jamróz 3 months ago committed by GitHub
parent f0db0c4f0d
commit 82ea562b75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 12
      public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.tsx
  2. 29
      public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.test.ts
  3. 19
      public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts
  4. 11
      public/app/plugins/datasource/tempo/datasource.ts
  5. 233
      public/app/plugins/datasource/tempo/language_provider.test.ts
  6. 46
      public/app/plugins/datasource/tempo/language_provider.ts
  7. 8
      public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx
  8. 2
      public/app/plugins/datasource/tempo/traceql/traceql.ts

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

@ -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: {

@ -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)\./, '');
};

@ -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<TempoQuery, TempoJson
// Allows to retrieve the list of tag values for ad-hoc filters
getTagValues(options: DataSourceGetTagValuesOptions<TempoQuery>): Promise<Array<{ text: string }>> {
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<TempoQuery, TempoJson
const traceqlSearchTargets = targets.traceqlSearch;
if (traceqlSearchTargets.length > 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<TempoQuery, TempoJson
}
const appliedQuery = this.applyVariables(query, {});
return this.languageProvider.generateQueryFromFilters(appliedQuery.filters);
return this.languageProvider.generateQueryFromFilters({ traceqlFilters: appliedQuery.filters });
}
}

@ -89,143 +89,228 @@ describe('Language_provider', () => {
});
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<upc>[\\s\\S]*)/$'],
valueType: 'string',
},
])
lp.generateQueryFromFilters({
traceqlFilters: [
{
id: 'span-name',
tag: 'name',
operator,
scope: TraceqlSearchScope.Span,
value: ['api/v2/variants/by-upc/(?P<upc>[\\s\\S]*)/$'],
valueType: 'string',
},
],
})
).toBe(`{name${operator}"api/v2/variants/by-upc/\\\\(\\\\?P<upc>\\\\[\\\\\\\\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[]) => {

@ -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}"`;
};
}

@ -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);
}}

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

Loading…
Cancel
Save