From bd321216db5cd2059ea05450f0ee87d8ce9a9af4 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:08:02 +0100 Subject: [PATCH] Tempo: Support new traceql intrinsics (#94240) * Tidy and update types * Tidy up comments * More tidy up * Simplify logic and reduce repitition in autocomplete * Simplify logic * Add and support new intrinsics * Only retrieve intrinsics from language provider * Move generateQueryFromFilters to languageProvider --- .../SearchTraceQLEditor/SearchField.test.tsx | 3 + .../tempo/SearchTraceQLEditor/SearchField.tsx | 5 +- .../SearchTraceQLEditor/TagsInput.test.tsx | 2 +- .../tempo/SearchTraceQLEditor/TagsInput.tsx | 2 +- .../TraceQLSearch.test.tsx | 10 +- .../SearchTraceQLEditor/TraceQLSearch.tsx | 17 +- .../tempo/SearchTraceQLEditor/utils.test.ts | 247 ++++-------------- .../tempo/SearchTraceQLEditor/utils.ts | 61 +++-- .../plugins/datasource/tempo/datasource.ts | 24 +- .../tempo/language_provider.test.ts | 132 ++++++++++ .../datasource/tempo/language_provider.ts | 31 ++- .../datasource/tempo/traceql/QueryEditor.tsx | 5 +- .../tempo/traceql/TraceQLEditor.tsx | 8 +- .../tempo/traceql/autocomplete.test.ts | 16 +- .../datasource/tempo/traceql/autocomplete.ts | 95 +++---- .../datasource/tempo/traceql/highlighting.ts | 2 +- .../datasource/tempo/traceql/situation.ts | 18 +- .../datasource/tempo/traceql/traceql.ts | 20 +- 18 files changed, 357 insertions(+), 341 deletions(-) diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx index 7330956c63d..461adc2f1c3 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.test.tsx @@ -187,6 +187,7 @@ describe('SearchField', () => { type: 'keyword', }, ]), + getIntrinsics: jest.fn().mockReturnValue(['duration']), } as unknown as TempoLanguageProvider; const { container } = renderSearchField(jest.fn(), filter, [], false, lp); @@ -235,6 +236,7 @@ describe('SearchField', () => { type: 'int', }, ]), + getIntrinsics: jest.fn().mockReturnValue(['duration']), } as unknown as TempoLanguageProvider; const { container } = renderSearchField(jest.fn(), filter, [], false, lp); @@ -280,6 +282,7 @@ const renderSearchField = ( type: 'string', }, ]), + getIntrinsics: jest.fn().mockReturnValue(['duration']), } as unknown as TempoLanguageProvider); const datasource: TempoDatasource = { diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx index bb56da5cdb8..67fce3b2364 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/SearchField.tsx @@ -53,7 +53,10 @@ const SearchField = ({ }: Props) => { const styles = useStyles2(getStyles); const [alertText, setAlertText] = useState(); - const scopedTag = useMemo(() => filterScopedTag(filter), [filter]); + const scopedTag = useMemo( + () => filterScopedTag(filter, datasource.languageProvider), + [datasource.languageProvider, filter] + ); // We automatically change the operator to the regex op when users select 2 or more values // However, they expect this to be automatically rolled back to the previous operator once // there's only one value selected, so we store the previous operator and value diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.test.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.test.tsx index b7834c57774..22a91ac5b37 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.test.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.test.tsx @@ -43,7 +43,7 @@ describe('TagsInput', () => { jest.advanceTimersByTime(1000); }); await waitFor(() => { - expect(screen.getByText('foo')).toBeInTheDocument(); + expect(screen.getByText('rootServiceName')).toBeInTheDocument(); expect(screen.getByText('bar')).toBeInTheDocument(); }); }); diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.tsx index 31afd82f7c6..c0765ed280c 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TagsInput.tsx @@ -69,7 +69,7 @@ const TagsInput = ({ const getTags = (f: TraceqlFilter) => { const tags = datasource.languageProvider.getTags(f.scope); - return getFilteredTags(tags, staticTags); + return getFilteredTags(tags, datasource.languageProvider, staticTags); }; const validInput = (f: TraceqlFilter) => { diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.test.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.test.tsx index ae2e26ed67f..73ac6675e9a 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.test.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.test.tsx @@ -65,7 +65,10 @@ describe('TraceQLSearch', () => { }, } as TempoDatasource; datasource.isStreamingSearchEnabled = () => false; - datasource.languageProvider = new TempoLanguageProvider(datasource); + const lp = new TempoLanguageProvider(datasource); + lp.getIntrinsics = () => ['duration']; + lp.generateQueryFromFilters = () => '{}'; + datasource.languageProvider = lp; let query: TempoQuery = { refId: 'A', queryType: 'traceqlSearch', @@ -218,7 +221,10 @@ describe('TraceQLSearch', () => { }, } as TempoDatasource; datasource.isStreamingSearchEnabled = () => false; - datasource.languageProvider = new TempoLanguageProvider(datasource); + const lp = new TempoLanguageProvider(datasource); + lp.getIntrinsics = () => ['duration']; + lp.generateQueryFromFilters = () => '{}'; + datasource.languageProvider = lp; await act(async () => { const { container } = render( diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.tsx b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.tsx index 5c886b87c12..63755df6110 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.tsx +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/TraceQLSearch.tsx @@ -18,7 +18,7 @@ import { GroupByField } from './GroupByField'; import InlineSearchField from './InlineSearchField'; import SearchField from './SearchField'; import TagsInput from './TagsInput'; -import { filterScopedTag, filterTitle, generateQueryFromFilters, interpolateFilters, replaceAt } from './utils'; +import { filterScopedTag, filterTitle, interpolateFilters, replaceAt } from './utils'; interface Props { datasource: TempoDatasource; @@ -64,8 +64,8 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVa const templateVariables = getTemplateSrv().getVariables(); useEffect(() => { - setTraceQlQuery(generateQueryFromFilters(interpolateFilters(query.filters || []))); - }, [query, templateVariables]); + setTraceQlQuery(datasource.languageProvider.generateQueryFromFilters(interpolateFilters(query.filters || []))); + }, [datasource.languageProvider, query, templateVariables]); const findFilter = useCallback((id: string) => query.filters?.find((f) => f.id === id), [query.filters]); @@ -99,6 +99,10 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVa const staticTags = datasource.search?.filters?.map((f) => f.tag) || []; staticTags.push('duration'); staticTags.push('traceDuration'); + staticTags.push('span:duration'); + staticTags.push('trace:duration'); + staticTags.push('status'); + staticTags.push('span:status'); // Dynamic filters are all filters that don't match the ID of a filter in the datasource configuration // The duration and status fields are a special case since its selector is hard-coded @@ -118,9 +122,10 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVa f.tag && ( { - it('an empty array', () => { - expect(generateQueryFromFilters([])).toBe('{}'); - }); - - it('a field without value', () => { - expect(generateQueryFromFilters([{ id: 'foo', tag: 'footag', operator: '=' }])).toBe('{}'); - }); - - it('a field with value but without tag', () => { - expect(generateQueryFromFilters([{ id: 'foo', value: 'foovalue', operator: '=' }])).toBe('{}'); - }); - - it('a field with value and tag but without operator', () => { - expect(generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: 'foovalue' }])).toBe('{}'); - }); - - describe('generates correct query for duration when duration type', () => { - it('not set', () => { - expect( - generateQueryFromFilters([ - { id: 'min-duration', operator: '>', valueType: 'duration', tag: 'duration', value: '100ms' }, - ]) - ).toBe('{duration>100ms}'); - }); - it('set to span', () => { - expect( - generateQueryFromFilters([ - { id: 'min-duration', operator: '>', valueType: 'duration', tag: 'duration', value: '100ms' }, - { id: 'duration-type', value: 'span' }, - ]) - ).toBe('{duration>100ms}'); - }); - it('set to trace', () => { - expect( - generateQueryFromFilters([ - { 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(generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: 'foovalue', operator: '=' }])).toBe( - '{.footag=foovalue}' - ); - expect( - generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: 'foovalue', operator: '=', valueType: 'string' }]) - ).toBe('{.footag="foovalue"}'); - }); - - it('a field with valueType as integer', () => { - expect( - generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: '1234', operator: '>', valueType: 'integer' }]) - ).toBe('{.footag>1234}'); - }); - it('two fields with everything filled in', () => { - expect( - generateQueryFromFilters([ - { 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( - generateQueryFromFilters([ - { 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( - generateQueryFromFilters([ - { id: 'foo', value: '1234', operator: '>=', valueType: 'integer' }, - { id: 'bar', tag: 'bartag', operator: '=', valueType: 'string' }, - ]) - ).toBe('{}'); - }); - it('scope is unscoped', () => { - expect( - generateQueryFromFilters([ - { - id: 'foo', - tag: 'footag', - value: '1234', - operator: '>=', - scope: TraceqlSearchScope.Unscoped, - valueType: 'integer', - }, - ]) - ).toBe('{.footag>=1234}'); - }); - it('scope is span', () => { - expect( - generateQueryFromFilters([ - { - id: 'foo', - tag: 'footag', - value: '1234', - operator: '>=', - scope: TraceqlSearchScope.Span, - valueType: 'integer', - }, - ]) - ).toBe('{span.footag>=1234}'); - }); - it('scope is resource', () => { - expect( - generateQueryFromFilters([ - { - id: 'foo', - tag: 'footag', - value: '1234', - operator: '>=', - scope: TraceqlSearchScope.Resource, - valueType: 'integer', - }, - ]) - ).toBe('{resource.footag>=1234}'); - }); -}); +const datasource: TempoDatasource = { + search: { + filters: [], + }, +} as unknown as TempoDatasource; +const lp = new TempoLanguageProvider(datasource); describe('generateQueryFromAdHocFilters generates the correct query for', () => { it('an empty array', () => { - expect(generateQueryFromAdHocFilters([])).toBe('{}'); + expect(generateQueryFromAdHocFilters([], lp)).toBe('{}'); }); it('a filter with values', () => { - expect(generateQueryFromAdHocFilters([{ key: 'footag', operator: '=', value: 'foovalue' }])).toBe( + 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' }, - ]) + 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' }])).toBe('{kind=server}'); + expect(generateQueryFromAdHocFilters([{ key: 'kind', operator: '=', value: 'server' }], lp)).toBe('{kind=server}'); }); }); describe('gets correct tags', () => { + const datasource: TempoDatasource = { + search: { + filters: [], + }, + } as unknown as TempoDatasource; + const lp = new TempoLanguageProvider(datasource); + it('for filtered tags when no tags supplied', () => { - const tags = getFilteredTags(emptyTags, []); - expect(tags).toEqual([ - 'duration', - 'kind', - 'name', - 'rootName', - 'rootServiceName', - 'status', - 'statusMessage', - 'traceDuration', - ]); + const tags = getFilteredTags(emptyTags, lp, []); + expect(tags).toEqual(intrinsicsV1); }); it('for filtered tags when API v1 tags supplied', () => { - const tags = getFilteredTags(v1Tags, []); - expect(tags).toEqual([ - 'duration', - 'kind', - 'name', - 'rootName', - 'rootServiceName', - 'status', - 'statusMessage', - 'traceDuration', - 'bar', - 'foo', - ]); + const tags = getFilteredTags(v1Tags, lp, []); + expect(tags).toEqual(intrinsicsV1.concat(['bar', 'foo'])); }); it('for filtered tags when API v1 tags supplied with tags to filter out', () => { - const tags = getFilteredTags(v1Tags, ['duration']); - expect(tags).toEqual([ - 'kind', - 'name', - 'rootName', - 'rootServiceName', - 'status', - 'statusMessage', - 'traceDuration', - 'bar', - 'foo', - ]); + const tags = getFilteredTags(v1Tags, lp, ['duration']); + expect(tags).toEqual(intrinsicsV1.filter((x) => x !== 'duration').concat(['bar', 'foo'])); }); it('for filtered tags when API v2 tags supplied', () => { - const tags = getFilteredTags(uniq(getUnscopedTags(v2Tags)), []); - expect(tags).toEqual([ - 'duration', - 'kind', - 'name', - 'rootName', - 'rootServiceName', - 'status', - 'statusMessage', - 'traceDuration', - 'cluster', - 'container', - 'db', - ]); + const tags = getFilteredTags(uniq(getUnscopedTags(v2Tags)), lp, []); + expect(tags).toEqual(intrinsicsV1.concat(['cluster', 'container', 'db'])); }); it('for filtered tags when API v2 tags supplied with tags to filter out', () => { - const tags = getFilteredTags(getUnscopedTags(v2Tags), ['duration', 'cluster']); - expect(tags).toEqual([ - 'kind', - 'name', - 'rootName', - 'rootServiceName', - 'status', - 'statusMessage', - 'traceDuration', - 'container', - 'db', - ]); + const tags = getFilteredTags(getUnscopedTags(v2Tags), lp, ['duration', 'cluster']); + expect(tags).toEqual(intrinsicsV1.filter((x) => x !== 'duration').concat(['container', 'db'])); + }); + + it('for filtered tags when API v2 tags set', () => { + lp.setV2Tags(v2Tags); + const tags = getFilteredTags(uniq(getUnscopedTags(v2Tags)), lp, []); + expect(tags).toEqual(testIntrinsics.concat(['cluster', 'container', 'db'])); }); it('for unscoped tags', () => { @@ -261,6 +103,7 @@ describe('gets correct tags', () => { }); export const emptyTags = []; +export const testIntrinsics = ['duration', 'kind', 'name', 'status']; export const v1Tags = ['bar', 'foo']; export const v2Tags = [ { @@ -273,6 +116,6 @@ export const v2Tags = [ }, { name: 'intrinsic', - tags: ['duration', 'kind', 'name', 'status'], + tags: testIntrinsics, }, ]; diff --git a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts index 82078699814..267893b508f 100644 --- a/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts +++ b/public/app/plugins/datasource/tempo/SearchTraceQLEditor/utils.ts @@ -5,7 +5,7 @@ import { getTemplateSrv } from '@grafana/runtime'; import { VariableFormatID } from '@grafana/schema'; import { TraceqlFilter, TraceqlSearchScope } from '../dataquery.gen'; -import { intrinsics } from '../traceql/traceql'; +import TempoLanguageProvider from '../language_provider'; import { Scope } from '../types'; export const interpolateFilters = (filters: TraceqlFilter[], scopedVars?: ScopedVars) => { @@ -28,18 +28,7 @@ export const interpolateFilters = (filters: TraceqlFilter[], scopedVars?: Scoped return interpolatedFilters; }; -export const generateQueryFromFilters = (filters: TraceqlFilter[]) => { - if (!filters) { - return ''; - } - - return `{${filters - .filter((f) => f.tag && f.operator && f.value?.length) - .map((f) => `${scopeHelper(f)}${tagHelper(f, filters)}${f.operator}${valueHelper(f)}`) - .join(' && ')}}`; -}; - -const valueHelper = (f: TraceqlFilter) => { +export const valueHelper = (f: TraceqlFilter) => { if (Array.isArray(f.value) && f.value.length > 1) { return `"${f.value.join('|')}"`; } @@ -49,9 +38,9 @@ const valueHelper = (f: TraceqlFilter) => { return f.value; }; -const scopeHelper = (f: TraceqlFilter) => { +export const scopeHelper = (f: TraceqlFilter, lp: TempoLanguageProvider) => { // Intrinsic fields don't have a scope - if (intrinsics.find((t) => t === f.tag)) { + if (lp.getIntrinsics().find((t) => t === f.tag)) { return ''; } return ( @@ -59,7 +48,7 @@ const scopeHelper = (f: TraceqlFilter) => { ); }; -const tagHelper = (f: TraceqlFilter, filters: TraceqlFilter[]) => { +export const tagHelper = (f: TraceqlFilter, filters: TraceqlFilter[]) => { if (f.tag === 'duration') { const durationType = filters.find((f) => f.id === 'duration-type'); if (durationType) { @@ -70,15 +59,15 @@ const tagHelper = (f: TraceqlFilter, filters: TraceqlFilter[]) => { return f.tag; }; -export const generateQueryFromAdHocFilters = (filters: AdHocVariableFilter[]) => { +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)}`) + .map((f) => `${f.key}${f.operator}${adHocValueHelper(f, lp)}`) .join(' && ')}}`; }; -const adHocValueHelper = (f: AdHocVariableFilter) => { - if (intrinsics.find((t) => t === f.key)) { +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) { @@ -91,11 +80,11 @@ export const getTagWithoutScope = (tag: string) => { return tag.replace(/^(event|link|resource|span)\./, ''); }; -export const filterScopedTag = (f: TraceqlFilter) => { - return scopeHelper(f) + f.tag; +export const filterScopedTag = (f: TraceqlFilter, lp: TempoLanguageProvider) => { + return scopeHelper(f, lp) + f.tag; }; -export const filterTitle = (f: TraceqlFilter) => { +export const filterTitle = (f: TraceqlFilter, lp: TempoLanguageProvider) => { // Special case for the intrinsic "name" since a label called "Name" isn't explicit if (f.tag === 'name') { return 'Span Name'; @@ -104,16 +93,34 @@ export const filterTitle = (f: TraceqlFilter) => { if (f.tag === 'service.name' && f.scope === TraceqlSearchScope.Resource) { return 'Service Name'; } - return startCase(filterScopedTag(f)); + return startCase(filterScopedTag(f, lp)); }; -export const getFilteredTags = (tags: string[], staticTags: Array) => { - return [...intrinsics, ...tags].filter((t) => !staticTags.includes(t)); +export const getFilteredTags = ( + tags: string[], + languageProvider: TempoLanguageProvider, + staticTags: Array +) => { + return [...languageProvider.getIntrinsics(), ...tags].filter((t) => !staticTags.includes(t)); }; export const getUnscopedTags = (scopes: Scope[]) => { return uniq( - scopes.map((scope: Scope) => (scope.name && scope.name !== 'intrinsic' && scope.tags ? scope.tags : [])).flat() + scopes + .map((scope: Scope) => + scope.name && scope.name !== TraceqlSearchScope.Intrinsic && scope.tags ? scope.tags : [] + ) + .flat() + ); +}; + +export const getIntrinsicTags = (scopes: Scope[]) => { + return uniq( + scopes + .map((scope: Scope) => + scope.name && scope.name === TraceqlSearchScope.Intrinsic && scope.tags ? scope.tags : [] + ) + .flat() ); }; diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index 43a09ce53f1..ffaee3bd7cc 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -34,12 +34,7 @@ import { } from '@grafana/runtime'; import { BarGaugeDisplayMode, TableCellDisplayMode, VariableFormatID } from '@grafana/schema'; -import { - generateQueryFromAdHocFilters, - generateQueryFromFilters, - getTagWithoutScope, - interpolateFilters, -} from './SearchTraceQLEditor/utils'; +import { generateQueryFromAdHocFilters, getTagWithoutScope, interpolateFilters } from './SearchTraceQLEditor/utils'; import { TempoVariableQuery, TempoVariableQueryType } from './VariableQueryEditor'; import { PrometheusDatasource, PromQuery } from './_importedDependencies/datasources/prometheus/types'; import { SearchTableType, TraceqlFilter, TraceqlSearchScope } from './dataquery.gen'; @@ -228,7 +223,7 @@ export class TempoDatasource extends DataSourceWithBackend): Promise> { - const query = generateQueryFromAdHocFilters(options.filters); + const query = generateQueryFromAdHocFilters(options.filters, this.languageProvider); return this.tagValuesQuery(options.key, query); } @@ -382,9 +377,8 @@ export class TempoDatasource extends DataSourceWithBackend this.hasGroupBy(t)); if (target) { const appliedQuery = this.applyVariables(target, options.scopedVars); - subQueries.push( - this.handleMetricsSummaryQuery(appliedQuery, generateQueryFromFilters(appliedQuery.filters), options) - ); + const queryFromFilters = this.languageProvider.generateQueryFromFilters(appliedQuery.filters); + subQueries.push(this.handleMetricsSummaryQuery(appliedQuery, queryFromFilters, options)); } } @@ -393,22 +387,22 @@ export class TempoDatasource extends DataSourceWithBackend 0) { const appliedQuery = this.applyVariables(traceqlSearchTargets[0], options.scopedVars); - const queryValueFromFilters = generateQueryFromFilters(appliedQuery.filters); + const queryFromFilters = this.languageProvider.generateQueryFromFilters(appliedQuery.filters); reportInteraction('grafana_traces_traceql_search_queried', { datasourceType: 'tempo', app: options.app ?? '', grafana_version: config.buildInfo.version, - query: queryValueFromFilters ?? '', + query: queryFromFilters ?? '', streaming: this.isStreamingSearchEnabled(), }); if (this.isStreamingSearchEnabled()) { - subQueries.push(this.handleStreamingQuery(options, traceqlSearchTargets, queryValueFromFilters)); + subQueries.push(this.handleStreamingQuery(options, traceqlSearchTargets, queryFromFilters)); } else { subQueries.push( this._request('/api/search', { - q: queryValueFromFilters, + q: queryFromFilters, limit: options.targets[0].limit ?? DEFAULT_LIMIT, spss: options.targets[0].spss ?? DEFAULT_SPSS, start: options.range.from.unix(), @@ -831,7 +825,7 @@ export class TempoDatasource extends DataSourceWithBackend { }); }); + describe('generateQueryFromFilters generates the correct query for', () => { + let lp: TempoLanguageProvider; + beforeEach(() => { + lp = setup(v1Tags); + }); + + it('an empty array', () => { + expect(lp.generateQueryFromFilters([])).toBe('{}'); + }); + + it('a field without value', () => { + expect(lp.generateQueryFromFilters([{ id: 'foo', tag: 'footag', operator: '=' }])).toBe('{}'); + }); + + it('a field with value but without tag', () => { + expect(lp.generateQueryFromFilters([{ 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('{}'); + }); + + 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' }, + ]) + ).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' }, + ]) + ).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' }, + ]) + ).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' }, + ]) + ).toBe('{.footag="foovalue"}'); + }); + + it('a field with valueType as integer', () => { + expect( + lp.generateQueryFromFilters([{ id: 'foo', tag: 'footag', value: '1234', operator: '>', valueType: 'integer' }]) + ).toBe('{.footag>1234}'); + }); + 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' }, + ]) + ).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' }, + ]) + ).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' }, + ]) + ).toBe('{}'); + }); + it('scope is unscoped', () => { + expect( + lp.generateQueryFromFilters([ + { + 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', + }, + ]) + ).toBe('{span.footag>=1234}'); + }); + it('scope is resource', () => { + expect( + lp.generateQueryFromFilters([ + { + id: 'foo', + tag: 'footag', + value: '1234', + operator: '>=', + scope: TraceqlSearchScope.Resource, + valueType: 'integer', + }, + ]) + ).toBe('{resource.footag>=1234}'); + }); + }); + const setup = (tagsV1?: string[], tagsV2?: Scope[]) => { const datasource: TempoDatasource = { search: { diff --git a/public/app/plugins/datasource/tempo/language_provider.ts b/public/app/plugins/datasource/tempo/language_provider.ts index a91cd899d06..0adf9a02f60 100644 --- a/public/app/plugins/datasource/tempo/language_provider.ts +++ b/public/app/plugins/datasource/tempo/language_provider.ts @@ -2,9 +2,18 @@ import { LanguageProvider, SelectableValue } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; import { VariableFormatID } from '@grafana/schema'; -import { getAllTags, getTagsByScope, getUnscopedTags } from './SearchTraceQLEditor/utils'; -import { TraceqlSearchScope } from './dataquery.gen'; +import { + getAllTags, + getIntrinsicTags, + getTagsByScope, + getUnscopedTags, + scopeHelper, + tagHelper, + valueHelper, +} from './SearchTraceQLEditor/utils'; +import { TraceqlFilter, TraceqlSearchScope } from './dataquery.gen'; import { TempoDatasource } from './datasource'; +import { intrinsicsV1 } from './traceql/traceql'; import { Scope } from './types'; export default class TempoLanguageProvider extends LanguageProvider { @@ -56,6 +65,13 @@ export default class TempoLanguageProvider extends LanguageProvider { this.tagsV2 = tags; }; + getIntrinsics = () => { + if (this.tagsV2) { + return getIntrinsicTags(this.tagsV2); + } + return intrinsicsV1; + }; + getTags = (scope?: TraceqlSearchScope) => { if (this.tagsV2 && scope) { if (scope === TraceqlSearchScope.Unscoped) { @@ -164,4 +180,15 @@ export default class TempoLanguageProvider extends LanguageProvider { // Reference: https://stackoverflow.com/a/37456192 return encodeURIComponent(encodeURIComponent(tag)); }; + + generateQueryFromFilters(filters: TraceqlFilter[]) { + if (!filters) { + return ''; + } + + return `{${filters + .filter((f) => f.tag && f.operator && f.value?.length) + .map((f) => `${scopeHelper(f, this)}${tagHelper(f, filters)}${f.operator}${valueHelper(f)}`) + .join(' && ')}}`; + } } diff --git a/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx b/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx index 1684ffd61bb..b64aa5865e9 100644 --- a/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx +++ b/public/app/plugins/datasource/tempo/traceql/QueryEditor.tsx @@ -6,7 +6,6 @@ import { GrafanaTheme2, QueryEditorProps } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; import { Button, InlineLabel, useStyles2 } from '@grafana/ui'; -import { generateQueryFromFilters } from '../SearchTraceQLEditor/utils'; import { TempoDatasource } from '../datasource'; import { defaultQuery, MyDataSourceOptions, TempoQuery } from '../types'; @@ -23,7 +22,7 @@ export function QueryEditor(props: Props) { const styles = useStyles2(getStyles); const query = defaults(props.query, defaultQuery); const [showCopyFromSearchButton, setShowCopyFromSearchButton] = useState(() => { - const genQuery = generateQueryFromFilters(query.filters || []); + const genQuery = props.datasource.languageProvider.generateQueryFromFilters(query.filters || []); return genQuery === query.query || genQuery === '{}'; }); @@ -51,7 +50,7 @@ export function QueryEditor(props: Props) { props.onClearResults(); props.onChange({ ...query, - query: generateQueryFromFilters(query.filters || []), + query: props.datasource.languageProvider.generateQueryFromFilters(query.filters || []), }); setShowCopyFromSearchButton(true); }} diff --git a/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.tsx b/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.tsx index 18ccc82b0dc..d9be413a2d8 100644 --- a/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.tsx +++ b/public/app/plugins/datasource/tempo/traceql/TraceQLEditor.tsx @@ -9,7 +9,7 @@ import { CodeEditor, Monaco, monacoTypes, useTheme2 } from '@grafana/ui'; import { TempoDatasource } from '../datasource'; import { TempoQuery } from '../types'; -import { CompletionProvider, CompletionType } from './autocomplete'; +import { CompletionProvider, CompletionItemType } from './autocomplete'; import { getErrorNodes, setMarkers } from './highlighting'; import { languageDefinition } from './traceql'; @@ -109,7 +109,7 @@ export function TraceQLEditor(props: Props) { errorNodes.filter((errorNode) => !(errorNode.from <= cursorPosition && cursorPosition <= errorNode.to)) ); - // Later on, show all errors + // Show all errors after a short delay, to avoid flickering errorTimeoutId.current = window.setTimeout(() => { setMarkers(monaco, model, errorNodes); }, 500); @@ -126,7 +126,7 @@ function setupPlaceholder(editor: monacoTypes.editor.IStandaloneCodeEditor, mona { range: new monaco.Range(1, 1, 1, 1), options: { - className: styles.placeholder, // The placeholder text is in styles.placeholder + className: styles.placeholder, isWholeLine: true, }, }, @@ -163,7 +163,7 @@ function setupActions(editor: monacoTypes.editor.IStandaloneCodeEditor, monaco: } function setupRegisterInteractionCommand(editor: monacoTypes.editor.IStandaloneCodeEditor): string | null { - return editor.addCommand(0, function (_, label, type: CompletionType) { + return editor.addCommand(0, function (_, label, type: CompletionItemType) { const properties: Record = { datasourceType: 'tempo', type }; // Filter out the label for TAG_VALUE completions to avoid potentially exposing sensitive data if (type !== 'TAG_VALUE') { diff --git a/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts b/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts index fafc6ccf361..d659ebc30e8 100644 --- a/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts +++ b/public/app/plugins/datasource/tempo/traceql/autocomplete.test.ts @@ -1,13 +1,13 @@ import { DataSourceInstanceSettings, PluginMetaInfo, PluginType } from '@grafana/data'; import { monacoTypes } from '@grafana/ui'; -import { emptyTags, v1Tags, v2Tags } from '../SearchTraceQLEditor/utils.test'; +import { emptyTags, testIntrinsics, v1Tags, v2Tags } from '../SearchTraceQLEditor/utils.test'; import { TempoDatasource } from '../datasource'; import TempoLanguageProvider from '../language_provider'; import { Scope, TempoJsonData } from '../types'; import { CompletionProvider } from './autocomplete'; -import { intrinsics, scopes } from './traceql'; +import { intrinsicsV1, scopes } from './traceql'; const emptyPosition = {} as monacoTypes.Position; @@ -21,7 +21,7 @@ describe('CompletionProvider', () => { const result = await provider.provideCompletionItems(model, emptyPosition); expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([ ...scopes.map((s) => expect.objectContaining({ label: s, insertText: s })), - ...intrinsics.map((s) => expect.objectContaining({ label: s, insertText: s })), + ...intrinsicsV1.map((s) => expect.objectContaining({ label: s, insertText: s })), expect.objectContaining({ label: 'bar', insertText: '.bar' }), expect.objectContaining({ label: 'foo', insertText: '.foo' }), expect.objectContaining({ label: 'status', insertText: '.status' }), @@ -33,7 +33,7 @@ describe('CompletionProvider', () => { const result = await provider.provideCompletionItems(model, emptyPosition); expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([ ...scopes.map((s) => expect.objectContaining({ label: s, insertText: s })), - ...intrinsics.map((s) => expect.objectContaining({ label: s, insertText: s })), + ...testIntrinsics.map((s) => expect.objectContaining({ label: s, insertText: s })), expect.objectContaining({ label: 'cluster', insertText: '.cluster' }), expect.objectContaining({ label: 'container', insertText: '.container' }), expect.objectContaining({ label: 'db', insertText: '.db' }), @@ -138,7 +138,7 @@ describe('CompletionProvider', () => { const result = await provider.provideCompletionItems(model, emptyPosition); expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([ ...scopes.map((s) => expect.objectContaining({ label: s, insertText: `{ ${s}$0 }` })), - ...intrinsics.map((s) => expect.objectContaining({ label: s, insertText: `{ ${s}$0 }` })), + ...intrinsicsV1.map((s) => expect.objectContaining({ label: s, insertText: `{ ${s}$0 }` })), expect.objectContaining({ label: 'bar', insertText: '{ .bar' }), expect.objectContaining({ label: 'foo', insertText: '{ .foo' }), expect.objectContaining({ label: 'status', insertText: '{ .status' }), @@ -150,7 +150,7 @@ describe('CompletionProvider', () => { const result = await provider.provideCompletionItems(model, emptyPosition); expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual([ ...scopes.map((s) => expect.objectContaining({ label: s, insertText: `{ ${s}$0 }` })), - ...intrinsics.map((s) => expect.objectContaining({ label: s, insertText: `{ ${s}$0 }` })), + ...testIntrinsics.map((s) => expect.objectContaining({ label: s, insertText: `{ ${s}$0 }` })), expect.objectContaining({ label: 'cluster', insertText: '{ .cluster' }), expect.objectContaining({ label: 'container', insertText: '{ .container' }), expect.objectContaining({ label: 'db', insertText: '{ .db' }), @@ -229,7 +229,7 @@ describe('CompletionProvider', () => { expect.objectContaining({ label: s.label, insertText: s.insertText, documentation: s.documentation }) ), ...scopes.map((s) => expect.objectContaining({ label: s, insertText: s })), - ...intrinsics.map((s) => expect.objectContaining({ label: s, insertText: s })), + ...testIntrinsics.map((s) => expect.objectContaining({ label: s, insertText: s })), expect.objectContaining({ label: 'cluster', insertText: '.cluster' }), expect.objectContaining({ label: 'container', insertText: '.container' }), expect.objectContaining({ label: 'db', insertText: '.db' }), @@ -361,7 +361,7 @@ describe('CompletionProvider', () => { const { provider, model } = setup(input, offset); const result = await provider.provideCompletionItems(model, emptyPosition); expect((result! as monacoTypes.languages.CompletionList).suggestions).toEqual( - [...scopes, ...intrinsics].map((s) => expect.objectContaining({ label: s })) + [...scopes, ...intrinsicsV1].map((s) => expect.objectContaining({ label: s })) ); }); diff --git a/public/app/plugins/datasource/tempo/traceql/autocomplete.ts b/public/app/plugins/datasource/tempo/traceql/autocomplete.ts index 63335ce4f81..f6db4d1e557 100644 --- a/public/app/plugins/datasource/tempo/traceql/autocomplete.ts +++ b/public/app/plugins/datasource/tempo/traceql/autocomplete.ts @@ -7,12 +7,7 @@ import type { Monaco, monacoTypes } from '@grafana/ui'; import TempoLanguageProvider from '../language_provider'; import { getSituation, Situation } from './situation'; -import { intrinsics, scopes } from './traceql'; - -interface Props { - languageProvider: TempoLanguageProvider; - setAlertText: (text?: string) => void; -} +import { scopes } from './traceql'; type MinimalCompletionItem = { label: string; @@ -21,6 +16,17 @@ type MinimalCompletionItem = { documentation?: string | IMarkdownString; }; +export type CompletionItemType = 'TAG_NAME' | 'TAG_VALUE' | 'KEYWORD' | 'OPERATOR' | 'SCOPE' | 'FUNCTION'; +type CompletionItem = MinimalCompletionItem & { + type: CompletionItemType; + insertTextRules?: monacoTypes.languages.CompletionItemInsertTextRule; // we used it to position the cursor +}; + +interface Props { + languageProvider: TempoLanguageProvider; + setAlertText: (text?: string) => void; +} + /** * Class that implements CompletionItemProvider interface and allows us to provide suggestion for the Monaco * autocomplete system. @@ -322,7 +328,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP * @param situation * @private */ - private async getCompletions(situation: Situation, setAlertText: (text?: string) => void): Promise { + private async getCompletions(situation: Situation, setAlertText: (text?: string) => void): Promise { switch (situation.type) { // This should only happen for cases that we do not support yet case 'UNKNOWN': { @@ -339,42 +345,26 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP return this.getTagsCompletions(); } case 'SPANSET_IN_THE_MIDDLE': - return [...CompletionProvider.comparisonOps, ...CompletionProvider.logicalOps].map((key) => ({ - ...key, - type: 'OPERATOR', - })); case 'SPANSET_EXPRESSION_OPERATORS_WITH_MISSING_CLOSED_BRACE': - return [...CompletionProvider.comparisonOps, ...CompletionProvider.logicalOps].map((key) => ({ - ...key, - type: 'OPERATOR', - })); + return this.getOperatorsCompletions([...CompletionProvider.comparisonOps, ...CompletionProvider.logicalOps]); case 'SPANSET_IN_NAME': return this.getScopesCompletions().concat(this.getIntrinsicsCompletions()).concat(this.getTagsCompletions()); case 'SPANSET_IN_NAME_SCOPE': return this.getTagsCompletions(undefined, situation.scope); case 'SPANSET_EXPRESSION_OPERATORS': - return [ + return this.getOperatorsCompletions([ ...CompletionProvider.comparisonOps, ...CompletionProvider.logicalOps, ...CompletionProvider.arithmeticOps, - ].map((key) => ({ - ...key, - type: 'OPERATOR', - })); + ]); case 'SPANFIELD_COMBINING_OPERATORS': - return [ + return this.getOperatorsCompletions([ ...CompletionProvider.logicalOps, ...CompletionProvider.arithmeticOps, ...CompletionProvider.comparisonOps, - ].map((key) => ({ - ...key, - type: 'OPERATOR', - })); + ]); case 'SPANSET_COMBINING_OPERATORS': - return CompletionProvider.spansetOps.map((key) => ({ - ...key, - type: 'OPERATOR', - })); + return this.getOperatorsCompletions(CompletionProvider.spansetOps); case 'SPANSET_PIPELINE_AFTER_OPERATOR': const functions = CompletionProvider.functions.map((key) => ({ ...key, @@ -386,10 +376,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP .concat(this.getTagsCompletions('.')); return [...functions, ...tags]; case 'SPANSET_COMPARISON_OPERATORS': - return CompletionProvider.comparisonOps.map((key) => ({ - ...key, - type: 'OPERATOR', - })); + return this.getOperatorsCompletions(CompletionProvider.comparisonOps); case 'SPANSET_IN_VALUE': let tagValues; try { @@ -403,8 +390,6 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP } } - const items: Completion[] = []; - const getInsertionText = (val: SelectableValue): string => { if (situation.betweenQuotes) { return val.label!; @@ -412,6 +397,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP return val.type === 'string' ? `"${val.label}"` : val.label!; }; + const items: CompletionItem[] = []; tagValues?.forEach((val) => { if (val?.label) { items.push({ @@ -439,7 +425,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP } } - private getTagsCompletions(prepend?: string, scope?: string): Completion[] { + private getTagsCompletions(prepend?: string, scope?: string): CompletionItem[] { const tags = this.languageProvider.getTraceqlAutocompleteTags(scope); return tags .sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'accent' })) @@ -450,8 +436,8 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP })); } - private getIntrinsicsCompletions(prepend?: string, append?: string): Completion[] { - return intrinsics.map((key) => ({ + private getIntrinsicsCompletions(prepend?: string, append?: string): CompletionItem[] { + return this.languageProvider.getIntrinsics().map((key) => ({ label: key, insertText: (prepend || '') + key + (append || ''), type: 'KEYWORD', @@ -459,7 +445,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP })); } - private getScopesCompletions(prepend?: string, append?: string): Completion[] { + private getScopesCompletions(prepend?: string, append?: string): CompletionItem[] { return scopes.map((key) => ({ label: key, insertText: (prepend || '') + key + (append || ''), @@ -467,6 +453,13 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP insertTextRules: this.monaco?.languages.CompletionItemInsertTextRule?.InsertAsSnippet, })); } + + private getOperatorsCompletions(ops: MinimalCompletionItem[]): CompletionItem[] { + return ops.map((key) => ({ + ...key, + type: 'OPERATOR', + })); + } } /** @@ -474,7 +467,10 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP * @param type * @param monaco */ -function getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): monacoTypes.languages.CompletionItemKind { +function getMonacoCompletionItemKind( + type: CompletionItemType, + monaco: Monaco +): monacoTypes.languages.CompletionItemKind { switch (type) { case 'TAG_NAME': return monaco.languages.CompletionItemKind.Enum; @@ -489,25 +485,10 @@ function getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): mona case 'FUNCTION': return monaco.languages.CompletionItemKind.Function; default: - throw new Error(`Unexpected CompletionType: ${type}`); + throw new Error(`Unexpected CompletionItemType: ${type}`); } } -export type CompletionType = 'TAG_NAME' | 'TAG_VALUE' | 'KEYWORD' | 'OPERATOR' | 'SCOPE' | 'FUNCTION'; -type Completion = { - type: CompletionType; - label: string; - insertText: string; - insertTextRules?: monacoTypes.languages.CompletionItemInsertTextRule; // we used it to position the cursor - documentation?: string | IMarkdownString; - detail?: string; -}; - -export type Tag = { - name: string; - value: string; -}; - function getRangeAndOffset(monaco: Monaco, model: monacoTypes.editor.ITextModel, position: monacoTypes.Position) { const word = model.getWordAtPosition(position); const range = @@ -539,7 +520,7 @@ function getRangeAndOffset(monaco: Monaco, model: monacoTypes.editor.ITextModel, */ function fixSuggestion( suggestion: monacoTypes.languages.CompletionItem, - itemType: CompletionType, + itemType: CompletionItemType, model: monacoTypes.editor.ITextModel, offset: number ) { diff --git a/public/app/plugins/datasource/tempo/traceql/highlighting.ts b/public/app/plugins/datasource/tempo/traceql/highlighting.ts index 21e17c9aba8..7eef867863a 100644 --- a/public/app/plugins/datasource/tempo/traceql/highlighting.ts +++ b/public/app/plugins/datasource/tempo/traceql/highlighting.ts @@ -116,7 +116,7 @@ export const getErrorNodes = (query: string): SyntaxNode[] => { }; /** - * Use red markers (squiggles) to highlight syntax errors in queries. + * Use markers (squiggles) to highlight syntax errors or warnings in queries. * */ export const setMarkers = ( diff --git a/public/app/plugins/datasource/tempo/traceql/situation.ts b/public/app/plugins/datasource/tempo/traceql/situation.ts index 0e60d065f6b..e97af2f2f19 100644 --- a/public/app/plugins/datasource/tempo/traceql/situation.ts +++ b/public/app/plugins/datasource/tempo/traceql/situation.ts @@ -161,18 +161,16 @@ export function getSituation(text: string, offset: number): Situation | null { shiftedOffset -= 1; } - // if the tree contains error, it is very probable that - // our node is one of those error nodes. - // also, if there are errors, the node lezer finds us, - // might not be the best node. - // so first we check if there is an error node at the cursor position - let maybeErrorNode = getErrorNode(tree, shiftedOffset); - if (!maybeErrorNode) { - // try again with the previous character - maybeErrorNode = getErrorNode(tree, shiftedOffset - 1); + // If the tree contains error, it's probable that our node is one of those error nodes. + // If there are errors, the node lezer finds us might not be the best node. + // So, first we check if there is an error node at the cursor position. + let errorNode = getErrorNode(tree, shiftedOffset); + if (!errorNode) { + // Try again with the previous character. + errorNode = getErrorNode(tree, shiftedOffset - 1); } - const cur = maybeErrorNode != null ? maybeErrorNode.cursor() : tree.cursorAt(shiftedOffset); + const cur = errorNode != null ? errorNode.cursor() : tree.cursorAt(shiftedOffset); const currentNode = cur.node; const ids = [cur.type.id]; diff --git a/public/app/plugins/datasource/tempo/traceql/traceql.ts b/public/app/plugins/datasource/tempo/traceql/traceql.ts index 5d8dc077584..6eb2a2c6e3d 100644 --- a/public/app/plugins/datasource/tempo/traceql/traceql.ts +++ b/public/app/plugins/datasource/tempo/traceql/traceql.ts @@ -28,7 +28,7 @@ export const keywordOperators = ['=', '!=']; export const stringOperators = ['=', '!=', '=~', '!~']; export const numberOperators = ['=', '!=', '>', '<', '>=', '<=']; -export const intrinsics = [ +export const intrinsicsV1 = [ 'duration', 'kind', 'name', @@ -38,6 +38,24 @@ export const intrinsics = [ 'statusMessage', 'traceDuration', ]; +export const intrinsics = intrinsicsV1.concat([ + 'event:name', + 'event:timeSinceStart', + 'instrumentation:name', + 'instrumentation:version', + 'link:spanID', + 'link:traceID', + 'span:duration', + 'span:id', + 'span:kind', + 'span:name', + 'span:status', + 'span:statusMessage', + 'trace:duration', + 'trace:id', + 'trace:rootName', + 'trace:rootService', +]); export const scopes: string[] = ['resource', 'span']; const aggregatorFunctions = ['avg', 'count', 'max', 'min', 'sum'];