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
*/
export enum TraceqlSearchScope {
Event = 'event',
Instrumentation = 'instrumentation',
Intrinsic = 'intrinsic',
Link = 'link',
Resource = 'resource',
Span = 'span',
Unscoped = 'unscoped',

View File

@ -37,7 +37,10 @@ const (
// Defines values for TraceqlSearchScope.
const (
TraceqlSearchScopeEvent TraceqlSearchScope = "event"
TraceqlSearchScopeInstrumentation TraceqlSearchScope = "instrumentation"
TraceqlSearchScopeIntrinsic TraceqlSearchScope = "intrinsic"
TraceqlSearchScopeLink TraceqlSearchScope = "link"
TraceqlSearchScopeResource TraceqlSearchScope = "resource"
TraceqlSearchScopeSpan TraceqlSearchScope = "span"
TraceqlSearchScopeUnscoped TraceqlSearchScope = "unscoped"

View File

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

View File

@ -78,14 +78,19 @@ export const GroupByField = (props: Props) => {
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 (
<InlineSearchField label="Aggregate by" tooltip={`${notice} Select one or more tags to see the metrics summary.`}>
<>
{query.groupBy?.map((f, i) => {
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) => ({
label: 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) => {
return val;
});
@ -171,7 +171,7 @@ describe('SearchField', () => {
expect(await screen.findByText('resource')).toBeInTheDocument();
expect(await screen.findByText('span')).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('$templateVariable2')).toBeInTheDocument();
}
@ -188,6 +188,7 @@ describe('SearchField', () => {
},
]),
getIntrinsics: jest.fn().mockReturnValue(['duration']),
getTags: jest.fn().mockReturnValue(['cluster']),
} as unknown as TempoLanguageProvider;
const { container } = renderSearchField(jest.fn(), filter, [], false, lp);
@ -237,6 +238,7 @@ describe('SearchField', () => {
},
]),
getIntrinsics: jest.fn().mockReturnValue(['duration']),
getTags: jest.fn().mockReturnValue(['cluster']),
} as unknown as TempoLanguageProvider;
const { container } = renderSearchField(jest.fn(), filter, [], false, lp);
@ -283,6 +285,7 @@ const renderSearchField = (
},
]),
getIntrinsics: jest.fn().mockReturnValue(['duration']),
getTags: jest.fn().mockReturnValue(['cluster']),
} as unknown as TempoLanguageProvider);
const datasource: TempoDatasource = {

View File

@ -114,7 +114,10 @@ const SearchField = ({
}, [filter.value]);
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 }));
// 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`}
options={addVariablesToOptions ? withTemplateVariableOptions(scopeOptions) : scopeOptions}
value={filter.scope}
onChange={(v) => updateFilter({ ...filter, scope: v?.value })}
onChange={(v) => updateFilter({ ...filter, scope: v?.value, tag: undefined, value: [] })}
placeholder="Select scope"
aria-label={`select ${filter.id} scope`}
/>
@ -197,6 +200,7 @@ const SearchField = ({
onCloseMenu={() => setTagQuery('')}
onChange={(v) => updateFilter({ ...filter, tag: v?.value, value: [] })}
value={filter.tag}
key={filter.tag}
placeholder="Select tag"
isClearable
aria-label={`select ${filter.id} tag`}

View File

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

View File

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

View File

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

View File

@ -44,7 +44,13 @@ export const scopeHelper = (f: TraceqlFilter, lp: TempoLanguageProvider) => {
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) => {
return tag.replace(/^(event|link|resource|span)\./, '');
return tag.replace(/^(event|instrumentation|link|resource|span)\./, '');
};
export const filterScopedTag = (f: TraceqlFilter, lp: TempoLanguageProvider) => {
@ -96,12 +102,8 @@ export const filterTitle = (f: TraceqlFilter, lp: TempoLanguageProvider) => {
return startCase(filterScopedTag(f, lp));
};
export const getFilteredTags = (
tags: string[],
languageProvider: TempoLanguageProvider,
staticTags: Array<string | undefined>
) => {
return [...languageProvider.getIntrinsics(), ...tags].filter((t) => !staticTags.includes(t));
export const getFilteredTags = (tags: string[], staticTags: Array<string | undefined>) => {
return [...tags].filter((t) => !staticTags.includes(t));
};
export const getUnscopedTags = (scopes: Scope[]) => {

View File

@ -64,7 +64,7 @@ composableKinds: DataQuery: {
#SearchTableType: "traces" | "spans" | "raw" @cuetsy(kind="enum")
// 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: {
// Uniquely identify the filter, will not be used in the query generation
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
*/
export enum TraceqlSearchScope {
Event = 'event',
Instrumentation = 'instrumentation',
Intrinsic = 'intrinsic',
Link = 'link',
Resource = 'resource',
Span = 'span',
Unscoped = 'unscoped',

View File

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

View File

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

View File

@ -340,7 +340,9 @@ function resolveAttribute(node: SyntaxNode, text: string): SituationType {
const indexOfDot = attributeFieldParentText.indexOf('.');
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 {
type: 'SPANSET_IN_NAME_SCOPE',
scope: attributeFieldUpToDot,

View File

@ -56,7 +56,7 @@ export const intrinsics = intrinsicsV1.concat([
'trace:rootName',
'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 functions = aggregatorFunctions.concat([
@ -198,13 +198,14 @@ export const traceqlGrammar: Grammar = {
pattern: /\{[^}]*}/,
inside: {
filter: {
pattern: /([\w.\/-]+)?(\s*)(([!=+\-<>~]+)\s*("([^"\n&]+)?"?|([^"\n\s&|}]+))?)/g,
pattern:
/([\w:.\/-]+)\s*(=|!=|<=|>=|=~|!~|>|<)\s*("[^"]*"|[\w.\/-]+)(\s*(\&\&|\|\|)\s*([\w:.\/-]+)\s*(=|!=|<=|>=|=~|!~|>|<)\s*("[^"]*"|[\w.\/-]+))*/g,
inside: {
comment: {
pattern: /#.*/,
},
'label-key': {
pattern: /[a-z_.][\w./_-]*(?=\s*(=|!=|>|<|>=|<=|=~|!~))/,
pattern: /[a-z_.][\w./_-]*(:[\w./_-]+)?(?=\s*(=|!=|>|<|>=|<=|=~|!~))/,
alias: 'attr-name',
},
'label-value': {