Tempo: Support new TraceQL scopes (#94858)

* Support new TraceQL scopes

* Update TraceQL lezer version
This commit is contained in:
Joey 2024-10-24 09:17:40 +01:00 committed by GitHub
parent 7fe710b141
commit 5a9de531d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 70 additions and 39 deletions

View File

@ -98,7 +98,10 @@ export enum SearchTableType {
* static fields are pre-set in the UI, dynamic fields are added by the user * static fields are pre-set in the UI, dynamic fields are added by the user
*/ */
export enum TraceqlSearchScope { export enum TraceqlSearchScope {
Event = 'event',
Instrumentation = 'instrumentation',
Intrinsic = 'intrinsic', Intrinsic = 'intrinsic',
Link = 'link',
Resource = 'resource', Resource = 'resource',
Span = 'span', Span = 'span',
Unscoped = 'unscoped', Unscoped = 'unscoped',

View File

@ -37,10 +37,13 @@ const (
// Defines values for TraceqlSearchScope. // Defines values for TraceqlSearchScope.
const ( const (
TraceqlSearchScopeIntrinsic TraceqlSearchScope = "intrinsic" TraceqlSearchScopeEvent TraceqlSearchScope = "event"
TraceqlSearchScopeResource TraceqlSearchScope = "resource" TraceqlSearchScopeInstrumentation TraceqlSearchScope = "instrumentation"
TraceqlSearchScopeSpan TraceqlSearchScope = "span" TraceqlSearchScopeIntrinsic TraceqlSearchScope = "intrinsic"
TraceqlSearchScopeUnscoped TraceqlSearchScope = "unscoped" TraceqlSearchScopeLink TraceqlSearchScope = "link"
TraceqlSearchScopeResource TraceqlSearchScope = "resource"
TraceqlSearchScopeSpan TraceqlSearchScope = "span"
TraceqlSearchScopeUnscoped TraceqlSearchScope = "unscoped"
) )
// These are the common properties available to all queries in all datasources. // These are the common properties available to all queries in all datasources.

View File

@ -35,6 +35,7 @@ describe('GroupByField', () => {
}; };
jest.spyOn(lp, 'getMetricsSummaryTags').mockReturnValue(['component', 'http.method', 'http.status_code']); jest.spyOn(lp, 'getMetricsSummaryTags').mockReturnValue(['component', 'http.method', 'http.status_code']);
jest.spyOn(lp, 'getTags').mockReturnValue(['component', 'http.method', 'http.status_code']);
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); jest.useFakeTimers();

View File

@ -78,14 +78,19 @@ export const GroupByField = (props: Props) => {
onChange(copy); onChange(copy);
}; };
const scopeOptions = Object.values(TraceqlSearchScope).map((t) => ({ label: t, value: t })); const scopeOptions = Object.values(TraceqlSearchScope)
.filter((s) => {
// only add scope if it has tags
return datasource.languageProvider.getTags(s).length > 0;
})
.map((t) => ({ label: t, value: t }));
return ( return (
<InlineSearchField label="Aggregate by" tooltip={`${notice} Select one or more tags to see the metrics summary.`}> <InlineSearchField label="Aggregate by" tooltip={`${notice} Select one or more tags to see the metrics summary.`}>
<> <>
{query.groupBy?.map((f, i) => { {query.groupBy?.map((f, i) => {
const tags = tagOptions(f) const tags = tagOptions(f)
?.concat(f.tag !== undefined && !tagOptions(f)?.includes(f.tag) ? [f.tag] : []) ?.concat(f.tag !== undefined && f.tag !== '' && !tagOptions(f)?.includes(f.tag) ? [f.tag] : [])
.map((t) => ({ .map((t) => ({
label: t, label: t,
value: t, value: t,

View File

@ -153,7 +153,7 @@ describe('SearchField', () => {
} }
}); });
it('should not provide intrinsic as a selectable scope', async () => { it('should provide intrinsic as a selectable scope', async () => {
const updateFilter = jest.fn((val) => { const updateFilter = jest.fn((val) => {
return val; return val;
}); });
@ -171,7 +171,7 @@ describe('SearchField', () => {
expect(await screen.findByText('resource')).toBeInTheDocument(); expect(await screen.findByText('resource')).toBeInTheDocument();
expect(await screen.findByText('span')).toBeInTheDocument(); expect(await screen.findByText('span')).toBeInTheDocument();
expect(await screen.findByText('unscoped')).toBeInTheDocument(); expect(await screen.findByText('unscoped')).toBeInTheDocument();
expect(screen.queryByText('intrinsic')).not.toBeInTheDocument(); expect(await screen.findByText('intrinsic')).toBeInTheDocument();
expect(await screen.findByText('$templateVariable1')).toBeInTheDocument(); expect(await screen.findByText('$templateVariable1')).toBeInTheDocument();
expect(await screen.findByText('$templateVariable2')).toBeInTheDocument(); expect(await screen.findByText('$templateVariable2')).toBeInTheDocument();
} }
@ -188,6 +188,7 @@ describe('SearchField', () => {
}, },
]), ]),
getIntrinsics: jest.fn().mockReturnValue(['duration']), getIntrinsics: jest.fn().mockReturnValue(['duration']),
getTags: jest.fn().mockReturnValue(['cluster']),
} as unknown as TempoLanguageProvider; } as unknown as TempoLanguageProvider;
const { container } = renderSearchField(jest.fn(), filter, [], false, lp); const { container } = renderSearchField(jest.fn(), filter, [], false, lp);
@ -237,6 +238,7 @@ describe('SearchField', () => {
}, },
]), ]),
getIntrinsics: jest.fn().mockReturnValue(['duration']), getIntrinsics: jest.fn().mockReturnValue(['duration']),
getTags: jest.fn().mockReturnValue(['cluster']),
} as unknown as TempoLanguageProvider; } as unknown as TempoLanguageProvider;
const { container } = renderSearchField(jest.fn(), filter, [], false, lp); const { container } = renderSearchField(jest.fn(), filter, [], false, lp);
@ -283,6 +285,7 @@ const renderSearchField = (
}, },
]), ]),
getIntrinsics: jest.fn().mockReturnValue(['duration']), getIntrinsics: jest.fn().mockReturnValue(['duration']),
getTags: jest.fn().mockReturnValue(['cluster']),
} as unknown as TempoLanguageProvider); } as unknown as TempoLanguageProvider);
const datasource: TempoDatasource = { const datasource: TempoDatasource = {

View File

@ -114,7 +114,10 @@ const SearchField = ({
}, [filter.value]); }, [filter.value]);
const scopeOptions = Object.values(TraceqlSearchScope) const scopeOptions = Object.values(TraceqlSearchScope)
.filter((s) => s !== TraceqlSearchScope.Intrinsic) .filter((s) => {
// only add scope if it has tags
return datasource.languageProvider.getTags(s).length > 0;
})
.map((t) => ({ label: t, value: t })); .map((t) => ({ label: t, value: t }));
// If all values have type string or int/float use a focused list of operators instead of all operators // If all values have type string or int/float use a focused list of operators instead of all operators
@ -177,7 +180,7 @@ const SearchField = ({
inputId={`${filter.id}-scope`} inputId={`${filter.id}-scope`}
options={addVariablesToOptions ? withTemplateVariableOptions(scopeOptions) : scopeOptions} options={addVariablesToOptions ? withTemplateVariableOptions(scopeOptions) : scopeOptions}
value={filter.scope} value={filter.scope}
onChange={(v) => updateFilter({ ...filter, scope: v?.value })} onChange={(v) => updateFilter({ ...filter, scope: v?.value, tag: undefined, value: [] })}
placeholder="Select scope" placeholder="Select scope"
aria-label={`select ${filter.id} scope`} aria-label={`select ${filter.id} scope`}
/> />
@ -197,6 +200,7 @@ const SearchField = ({
onCloseMenu={() => setTagQuery('')} onCloseMenu={() => setTagQuery('')}
onChange={(v) => updateFilter({ ...filter, tag: v?.value, value: [] })} onChange={(v) => updateFilter({ ...filter, tag: v?.value, value: [] })}
value={filter.tag} value={filter.tag}
key={filter.tag}
placeholder="Select tag" placeholder="Select tag"
isClearable isClearable
aria-label={`select ${filter.id} tag`} aria-label={`select ${filter.id} tag`}

View File

@ -43,7 +43,6 @@ describe('TagsInput', () => {
jest.advanceTimersByTime(1000); jest.advanceTimersByTime(1000);
}); });
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('rootServiceName')).toBeInTheDocument();
expect(screen.getByText('bar')).toBeInTheDocument(); expect(screen.getByText('bar')).toBeInTheDocument();
}); });
}); });

View File

@ -69,7 +69,7 @@ const TagsInput = ({
const getTags = (f: TraceqlFilter) => { const getTags = (f: TraceqlFilter) => {
const tags = datasource.languageProvider.getTags(f.scope); const tags = datasource.languageProvider.getTags(f.scope);
return getFilteredTags(tags, datasource.languageProvider, staticTags); return getFilteredTags(tags, staticTags);
}; };
const validInput = (f: TraceqlFilter) => { const validInput = (f: TraceqlFilter) => {

View File

@ -3,7 +3,6 @@ import { uniq } from 'lodash';
import { TraceqlSearchScope } from '../dataquery.gen'; import { TraceqlSearchScope } from '../dataquery.gen';
import { TempoDatasource } from '../datasource'; import { TempoDatasource } from '../datasource';
import TempoLanguageProvider from '../language_provider'; import TempoLanguageProvider from '../language_provider';
import { intrinsicsV1 } from '../traceql/traceql';
import { getUnscopedTags, getFilteredTags, getAllTags, getTagsByScope, generateQueryFromAdHocFilters } from './utils'; import { getUnscopedTags, getFilteredTags, getAllTags, getTagsByScope, generateQueryFromAdHocFilters } from './utils';
@ -51,34 +50,34 @@ describe('gets correct tags', () => {
const lp = new TempoLanguageProvider(datasource); const lp = new TempoLanguageProvider(datasource);
it('for filtered tags when no tags supplied', () => { it('for filtered tags when no tags supplied', () => {
const tags = getFilteredTags(emptyTags, lp, []); const tags = getFilteredTags(emptyTags, []);
expect(tags).toEqual(intrinsicsV1); expect(tags).toEqual([]);
}); });
it('for filtered tags when API v1 tags supplied', () => { it('for filtered tags when API v1 tags supplied', () => {
const tags = getFilteredTags(v1Tags, lp, []); const tags = getFilteredTags(v1Tags, []);
expect(tags).toEqual(intrinsicsV1.concat(['bar', 'foo'])); expect(tags).toEqual(['bar', 'foo']);
}); });
it('for filtered tags when API v1 tags supplied with tags to filter out', () => { it('for filtered tags when API v1 tags supplied with tags to filter out', () => {
const tags = getFilteredTags(v1Tags, lp, ['duration']); const tags = getFilteredTags(v1Tags, ['foo']);
expect(tags).toEqual(intrinsicsV1.filter((x) => x !== 'duration').concat(['bar', 'foo'])); expect(tags).toEqual(['bar']);
}); });
it('for filtered tags when API v2 tags supplied', () => { it('for filtered tags when API v2 tags supplied', () => {
const tags = getFilteredTags(uniq(getUnscopedTags(v2Tags)), lp, []); const tags = getFilteredTags(uniq(getUnscopedTags(v2Tags)), []);
expect(tags).toEqual(intrinsicsV1.concat(['cluster', 'container', 'db'])); expect(tags).toEqual(['cluster', 'container', 'db']);
}); });
it('for filtered tags when API v2 tags supplied with tags to filter out', () => { it('for filtered tags when API v2 tags supplied with tags to filter out', () => {
const tags = getFilteredTags(getUnscopedTags(v2Tags), lp, ['duration', 'cluster']); const tags = getFilteredTags(getUnscopedTags(v2Tags), ['cluster']);
expect(tags).toEqual(intrinsicsV1.filter((x) => x !== 'duration').concat(['container', 'db'])); expect(tags).toEqual(['container', 'db']);
}); });
it('for filtered tags when API v2 tags set', () => { it('for filtered tags when API v2 tags set', () => {
lp.setV2Tags(v2Tags); lp.setV2Tags(v2Tags);
const tags = getFilteredTags(uniq(getUnscopedTags(v2Tags)), lp, []); const tags = getFilteredTags(uniq(getUnscopedTags(v2Tags)), []);
expect(tags).toEqual(testIntrinsics.concat(['cluster', 'container', 'db'])); expect(tags).toEqual(['cluster', 'container', 'db']);
}); });
it('for unscoped tags', () => { it('for unscoped tags', () => {

View File

@ -44,7 +44,13 @@ export const scopeHelper = (f: TraceqlFilter, lp: TempoLanguageProvider) => {
return ''; return '';
} }
return ( return (
(f.scope === TraceqlSearchScope.Resource || f.scope === TraceqlSearchScope.Span ? f.scope?.toLowerCase() : '') + '.' (f.scope === TraceqlSearchScope.Event ||
f.scope === TraceqlSearchScope.Instrumentation ||
f.scope === TraceqlSearchScope.Link ||
f.scope === TraceqlSearchScope.Resource ||
f.scope === TraceqlSearchScope.Span
? f.scope?.toLowerCase()
: '') + '.'
); );
}; };
@ -77,7 +83,7 @@ const adHocValueHelper = (f: AdHocVariableFilter, lp: TempoLanguageProvider) =>
}; };
export const getTagWithoutScope = (tag: string) => { export const getTagWithoutScope = (tag: string) => {
return tag.replace(/^(event|link|resource|span)\./, ''); return tag.replace(/^(event|instrumentation|link|resource|span)\./, '');
}; };
export const filterScopedTag = (f: TraceqlFilter, lp: TempoLanguageProvider) => { export const filterScopedTag = (f: TraceqlFilter, lp: TempoLanguageProvider) => {
@ -96,12 +102,8 @@ export const filterTitle = (f: TraceqlFilter, lp: TempoLanguageProvider) => {
return startCase(filterScopedTag(f, lp)); return startCase(filterScopedTag(f, lp));
}; };
export const getFilteredTags = ( export const getFilteredTags = (tags: string[], staticTags: Array<string | undefined>) => {
tags: string[], return [...tags].filter((t) => !staticTags.includes(t));
languageProvider: TempoLanguageProvider,
staticTags: Array<string | undefined>
) => {
return [...languageProvider.getIntrinsics(), ...tags].filter((t) => !staticTags.includes(t));
}; };
export const getUnscopedTags = (scopes: Scope[]) => { export const getUnscopedTags = (scopes: Scope[]) => {

View File

@ -64,7 +64,7 @@ composableKinds: DataQuery: {
#SearchTableType: "traces" | "spans" | "raw" @cuetsy(kind="enum") #SearchTableType: "traces" | "spans" | "raw" @cuetsy(kind="enum")
// static fields are pre-set in the UI, dynamic fields are added by the user // static fields are pre-set in the UI, dynamic fields are added by the user
#TraceqlSearchScope: "intrinsic" | "unscoped" | "resource" | "span" @cuetsy(kind="enum") #TraceqlSearchScope: "intrinsic" | "unscoped" | "event" | "instrumentation" | "link" | "resource" | "span" @cuetsy(kind="enum")
#TraceqlFilter: { #TraceqlFilter: {
// Uniquely identify the filter, will not be used in the query generation // Uniquely identify the filter, will not be used in the query generation
id: string id: string

View File

@ -96,7 +96,10 @@ export enum SearchTableType {
* static fields are pre-set in the UI, dynamic fields are added by the user * static fields are pre-set in the UI, dynamic fields are added by the user
*/ */
export enum TraceqlSearchScope { export enum TraceqlSearchScope {
Event = 'event',
Instrumentation = 'instrumentation',
Intrinsic = 'intrinsic', Intrinsic = 'intrinsic',
Link = 'link',
Resource = 'resource', Resource = 'resource',
Span = 'span', Span = 'span',
Unscoped = 'unscoped', Unscoped = 'unscoped',

View File

@ -528,7 +528,7 @@ function fixSuggestion(
const match = model const match = model
.getValue() .getValue()
.substring(0, offset) .substring(0, offset)
.match(/(span\.|resource\.|\.)?([\w./-]*)$/); .match(/(event\.|instrumentation\.|link\.|resource\.|span\.|\.)?([\w./-]*)$/);
if (match) { if (match) {
const scope = match[1]; const scope = match[1];

View File

@ -5,11 +5,14 @@ import {
And, And,
AttributeField, AttributeField,
ComparisonOp, ComparisonOp,
Event,
FieldExpression, FieldExpression,
FieldOp, FieldOp,
GroupOperation, GroupOperation,
Identifier, Identifier,
Instrumentation,
IntrinsicField, IntrinsicField,
Link,
Or, Or,
Parent, Parent,
parser, parser,
@ -157,6 +160,9 @@ export const getWarningMarkers = (severity: number, model: monacoTypes.editor.IT
// Make sure prevSibling is using the proper scope // Make sure prevSibling is using the proper scope
if ( if (
node.prevSibling?.type.id !== Parent && node.prevSibling?.type.id !== Parent &&
node.prevSibling?.type.id !== Event &&
node.prevSibling?.type.id !== Instrumentation &&
node.prevSibling?.type.id !== Link &&
node.prevSibling?.type.id !== Resource && node.prevSibling?.type.id !== Resource &&
node.prevSibling?.type.id !== Span node.prevSibling?.type.id !== Span
) { ) {

View File

@ -340,7 +340,9 @@ function resolveAttribute(node: SyntaxNode, text: string): SituationType {
const indexOfDot = attributeFieldParentText.indexOf('.'); const indexOfDot = attributeFieldParentText.indexOf('.');
const attributeFieldUpToDot = attributeFieldParentText.slice(0, indexOfDot); const attributeFieldUpToDot = attributeFieldParentText.slice(0, indexOfDot);
if (['span', 'resource', 'parent'].find((item) => item === attributeFieldUpToDot)) { if (
['event', 'instrumentation', 'link', 'resource', 'span', 'parent'].find((item) => item === attributeFieldUpToDot)
) {
return { return {
type: 'SPANSET_IN_NAME_SCOPE', type: 'SPANSET_IN_NAME_SCOPE',
scope: attributeFieldUpToDot, scope: attributeFieldUpToDot,

View File

@ -56,7 +56,7 @@ export const intrinsics = intrinsicsV1.concat([
'trace:rootName', 'trace:rootName',
'trace:rootService', 'trace:rootService',
]); ]);
export const scopes: string[] = ['resource', 'span']; export const scopes: string[] = ['event', 'instrumentation', 'link', 'resource', 'span'];
const aggregatorFunctions = ['avg', 'count', 'max', 'min', 'sum']; const aggregatorFunctions = ['avg', 'count', 'max', 'min', 'sum'];
const functions = aggregatorFunctions.concat([ const functions = aggregatorFunctions.concat([
@ -198,13 +198,14 @@ export const traceqlGrammar: Grammar = {
pattern: /\{[^}]*}/, pattern: /\{[^}]*}/,
inside: { inside: {
filter: { filter: {
pattern: /([\w.\/-]+)?(\s*)(([!=+\-<>~]+)\s*("([^"\n&]+)?"?|([^"\n\s&|}]+))?)/g, pattern:
/([\w:.\/-]+)\s*(=|!=|<=|>=|=~|!~|>|<)\s*("[^"]*"|[\w.\/-]+)(\s*(\&\&|\|\|)\s*([\w:.\/-]+)\s*(=|!=|<=|>=|=~|!~|>|<)\s*("[^"]*"|[\w.\/-]+))*/g,
inside: { inside: {
comment: { comment: {
pattern: /#.*/, pattern: /#.*/,
}, },
'label-key': { 'label-key': {
pattern: /[a-z_.][\w./_-]*(?=\s*(=|!=|>|<|>=|<=|=~|!~))/, pattern: /[a-z_.][\w./_-]*(:[\w./_-]+)?(?=\s*(=|!=|>|<|>=|<=|=~|!~))/,
alias: 'attr-name', alias: 'attr-name',
}, },
'label-value': { 'label-value': {