mirror of https://github.com/grafana/grafana
Tempo: New features for TraceQL grammar (#107000)
Support the following features in the TraceQL syntax grammar: * New instrinsic field: `span:parentID` * `with(most_recent=true)` Fixes: https://github.com/grafana/grafana/issues/106978 Fixes: https://github.com/grafana/grafana/issues/104159 Signed-off-by: Alex Bikfalvi <alex.bikfalvi@grafana.com>pull/108313/head
parent
8eec858054
commit
28109ee192
@ -0,0 +1,444 @@ |
|||||||
|
import { |
||||||
|
languageDefinition, |
||||||
|
traceqlGrammar, |
||||||
|
operators, |
||||||
|
keywordOperators, |
||||||
|
stringOperators, |
||||||
|
numberOperators, |
||||||
|
intrinsics, |
||||||
|
scopes, |
||||||
|
enumIntrinsics, |
||||||
|
} from './traceql'; |
||||||
|
|
||||||
|
describe('TraceQL grammar', () => { |
||||||
|
describe('Language definition', () => { |
||||||
|
it('should include all required keywords', () => { |
||||||
|
const { keywords } = languageDefinition.def.language; |
||||||
|
expect(keywords).toContain('with'); |
||||||
|
expect(keywords).toContain('span'); |
||||||
|
expect(keywords).toContain('resource'); |
||||||
|
expect(keywords).toContain('duration'); |
||||||
|
expect(keywords).toContain('status'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should include with clause keywords and parameters', () => { |
||||||
|
const { withClauseKeywords, withParameters } = languageDefinition.def.language; |
||||||
|
expect(withClauseKeywords).toContain('with'); |
||||||
|
expect(withParameters).toContain('most_recent'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('Operators', () => { |
||||||
|
it('should include all comparison operators', () => { |
||||||
|
expect(operators).toContain('='); |
||||||
|
expect(operators).toContain('!='); |
||||||
|
expect(operators).toContain('>'); |
||||||
|
expect(operators).toContain('<'); |
||||||
|
expect(operators).toContain('>='); |
||||||
|
expect(operators).toContain('<='); |
||||||
|
expect(operators).toContain('=~'); |
||||||
|
expect(operators).toContain('!~'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should categorize operators correctly', () => { |
||||||
|
expect(keywordOperators).toEqual(['=', '!=']); |
||||||
|
expect(stringOperators).toEqual(['=', '!=', '=~', '!~']); |
||||||
|
expect(numberOperators).toEqual(['=', '!=', '>', '<', '>=', '<=']); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('Intrinsics and scopes', () => { |
||||||
|
it('should include all intrinsics', () => { |
||||||
|
expect(intrinsics).toContain('duration'); |
||||||
|
expect(intrinsics).toContain('name'); |
||||||
|
expect(intrinsics).toContain('status'); |
||||||
|
expect(intrinsics).toContain('span:duration'); |
||||||
|
expect(intrinsics).toContain('trace:id'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should include all scopes', () => { |
||||||
|
expect(scopes).toContain('event'); |
||||||
|
expect(scopes).toContain('instrumentation'); |
||||||
|
expect(scopes).toContain('link'); |
||||||
|
expect(scopes).toContain('resource'); |
||||||
|
expect(scopes).toContain('span'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should identify enum intrinsics', () => { |
||||||
|
expect(enumIntrinsics).toContain('kind'); |
||||||
|
expect(enumIntrinsics).toContain('span:kind'); |
||||||
|
expect(enumIntrinsics).toContain('status'); |
||||||
|
expect(enumIntrinsics).toContain('span:status'); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('TraceQL patterns', () => { |
||||||
|
it('should match span-set patterns', () => { |
||||||
|
const spanSetRule = (traceqlGrammar as any)['span-set']; |
||||||
|
expect(spanSetRule).toBeDefined(); |
||||||
|
expect(spanSetRule.pattern).toBeDefined(); |
||||||
|
const spanSetPattern = spanSetRule.pattern as RegExp; |
||||||
|
expect(spanSetPattern.test('{span.name="foo"}')).toBe(true); |
||||||
|
expect(spanSetPattern.test('{resource.service.name="bar"}')).toBe(true); |
||||||
|
expect(spanSetPattern.test('{duration>1s}')).toBe(true); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should match with-clause patterns', () => { |
||||||
|
const withClauseRule = (traceqlGrammar as any)['with-clause']; |
||||||
|
expect(withClauseRule).toBeDefined(); |
||||||
|
expect(withClauseRule.pattern).toBeDefined(); |
||||||
|
const withClausePattern = withClauseRule.pattern as RegExp; |
||||||
|
expect(withClausePattern.test('with (most_recent=true)')).toBe(true); |
||||||
|
expect(withClausePattern.test('with (most_recent=false)')).toBe(true); |
||||||
|
expect(withClausePattern.test('with(most_recent=true)')).toBe(true); |
||||||
|
expect(withClausePattern.test('with (most_recent = true)')).toBe(true); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should match comment patterns', () => { |
||||||
|
const commentRule = (traceqlGrammar as any).comment; |
||||||
|
expect(commentRule).toBeDefined(); |
||||||
|
expect(commentRule.pattern).toBeDefined(); |
||||||
|
const commentPattern = commentRule.pattern as RegExp; |
||||||
|
expect(commentPattern.test('// This is a comment')).toBe(true); |
||||||
|
expect(commentPattern.test('//Another comment')).toBe(true); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should match number patterns', () => { |
||||||
|
const numberRule = (traceqlGrammar as any).number; |
||||||
|
expect(numberRule).toBeDefined(); |
||||||
|
const numberPattern = numberRule as RegExp; |
||||||
|
expect(numberPattern.test('123')).toBe(true); |
||||||
|
expect(numberPattern.test('123.45')).toBe(true); |
||||||
|
expect(numberPattern.test('-123')).toBe(true); |
||||||
|
expect(numberPattern.test('1.23e10')).toBe(true); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should match operator patterns', () => { |
||||||
|
const operatorRule = (traceqlGrammar as any).operator; |
||||||
|
expect(operatorRule).toBeDefined(); |
||||||
|
const operatorPattern = operatorRule as RegExp; |
||||||
|
expect(operatorPattern.test('=')).toBe(true); |
||||||
|
expect(operatorPattern.test('!=')).toBe(true); |
||||||
|
expect(operatorPattern.test('>')).toBe(true); |
||||||
|
expect(operatorPattern.test('<')).toBe(true); |
||||||
|
expect(operatorPattern.test('>=')).toBe(true); |
||||||
|
expect(operatorPattern.test('<=')).toBe(true); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('TraceQL query syntax', () => { |
||||||
|
const testCases = [ |
||||||
|
// Empty query
|
||||||
|
{ |
||||||
|
name: 'empty query', |
||||||
|
query: '{}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
// Basic queries
|
||||||
|
{ |
||||||
|
name: 'basic span query with string equality', |
||||||
|
query: '{ span.name="test" }', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'basic span query with string inequality', |
||||||
|
query: '{ span.name!="test" }', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'basic span query with regex match', |
||||||
|
query: '{ span.name=~"test" }', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'basic span query with regex mismatch', |
||||||
|
query: '{ span.name!~"test" }', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'basic span query with number equality', |
||||||
|
query: '{span.duration=10}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'basic span query with boolean', |
||||||
|
query: '{span.flags.sampled=true}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'resource query', |
||||||
|
query: '{resource.service.name="my-service"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'duration query', |
||||||
|
query: '{duration>1s}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
// Structural operators
|
||||||
|
{ |
||||||
|
name: 'structural operator child query', |
||||||
|
query: '{span.name="parent"} > {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'structural operator parent query', |
||||||
|
query: '{span.name="parent"} < {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'structural operator descendant query', |
||||||
|
query: '{span.name="parent"} >> {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'structural operator ancestor query', |
||||||
|
query: '{span.name="parent"} << {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'structural operator sibling query', |
||||||
|
query: '{span.name="parent"} ~ {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
// Union structure operators
|
||||||
|
{ |
||||||
|
name: 'union structural operator child query', |
||||||
|
query: '{span.name="parent"} &> {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'union structural operator parent query', |
||||||
|
query: '{span.name="parent"} &< {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'union structural operator descendant query', |
||||||
|
query: '{span.name="parent"} &>> {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'union structural operator ancestor query', |
||||||
|
query: '{span.name="parent"} &<< {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'union structural operator sibling query', |
||||||
|
query: '{span.name="parent"} &~ {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
// Negated structure operators
|
||||||
|
{ |
||||||
|
name: 'negated structural operator child query', |
||||||
|
query: '{span.name="parent"} !> {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'negated structural operator parent query', |
||||||
|
query: '{span.name="parent"} !< {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'negated structural operator descendant query', |
||||||
|
query: '{span.name="parent"} !>> {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'negated structural operator ancestor query', |
||||||
|
query: '{span.name="parent"} !<< {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'negated structural operator sibling query', |
||||||
|
query: '{span.name="parent"} !~ {span.name="child"}', |
||||||
|
shouldMatch: true, |
||||||
|
}, // Comments
|
||||||
|
{ |
||||||
|
name: 'query with comment', |
||||||
|
query: '// Find slow requests\n{duration>1s}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
// Query hint queries
|
||||||
|
{ |
||||||
|
name: 'query hint - most_recent true', |
||||||
|
query: '{span.name="test"} with (most_recent=true)', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'query hint - most_recent false', |
||||||
|
query: '{span.name="test"} with (most_recent=false)', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'query hint - no spaces', |
||||||
|
query: '{span.name="test"} with(most_recent=true)', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'query hint - extra spaces', |
||||||
|
query: '{span.name="test"} with ( most_recent = true )', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
// Test enum intrinsics with valid values
|
||||||
|
{ |
||||||
|
name: 'enum intrinsic - kind with server value', |
||||||
|
query: '{kind=server}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'enum intrinsic - kind with client value', |
||||||
|
query: '{kind=client}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'enum intrinsic - kind with producer value', |
||||||
|
query: '{kind=producer}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'enum intrinsic - kind with consumer value', |
||||||
|
query: '{kind=consumer}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'enum intrinsic - kind with internal value', |
||||||
|
query: '{kind=internal}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'enum intrinsic - status with ok value', |
||||||
|
query: '{status=ok}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'enum intrinsic - status with error value', |
||||||
|
query: '{status=error}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'enum intrinsic - status with unset value', |
||||||
|
query: '{status=unset}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'enum intrinsic - span:kind with server value', |
||||||
|
query: '{span:kind=server}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'enum intrinsic - span:status with error value', |
||||||
|
query: '{span:status=error}', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
// Complex queries
|
||||||
|
{ |
||||||
|
name: 'complex query with with clause', |
||||||
|
query: '{span.http.status_code=200 && span.name="GET /api"} | select(span.duration) with (most_recent=true)', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: 'aggregation with with clause', |
||||||
|
query: '{span.service.name="frontend"} | avg(duration) with (most_recent=false)', |
||||||
|
shouldMatch: true, |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
testCases.forEach(({ name, query, shouldMatch }) => { |
||||||
|
it(`should ${shouldMatch ? 'match' : 'not match'} ${name}`, () => { |
||||||
|
const grammar = traceqlGrammar as any; |
||||||
|
const spanSetPattern = grammar['span-set']?.pattern as RegExp; |
||||||
|
const withClausePattern = grammar['with-clause']?.pattern as RegExp; |
||||||
|
const commentPattern = grammar.comment?.pattern as RegExp; |
||||||
|
|
||||||
|
const spanSetMatches = spanSetPattern ? (query.match(spanSetPattern) || []).length > 0 : false; |
||||||
|
const withClauseMatches = withClausePattern ? (query.match(withClausePattern) || []).length > 0 : false; |
||||||
|
const commentMatches = commentPattern ? (query.match(commentPattern) || []).length > 0 : false; |
||||||
|
|
||||||
|
const hasAnyMatch = spanSetMatches || withClauseMatches || commentMatches; |
||||||
|
|
||||||
|
if (shouldMatch) { |
||||||
|
expect(hasAnyMatch).toBe(true); |
||||||
|
} else { |
||||||
|
expect(hasAnyMatch).toBe(false); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('With clause validation', () => { |
||||||
|
it('should validate with clause parameter names', () => { |
||||||
|
const grammar = traceqlGrammar as any; |
||||||
|
const withClause = grammar['with-clause']; |
||||||
|
expect(withClause).toBeDefined(); |
||||||
|
expect(withClause.inside).toBeDefined(); |
||||||
|
|
||||||
|
const parameterNameRule = withClause.inside['parameter-name']; |
||||||
|
expect(parameterNameRule).toBeDefined(); |
||||||
|
const parameterNamePattern = parameterNameRule.pattern as RegExp; |
||||||
|
|
||||||
|
expect(parameterNamePattern.test('most_recent=')).toBe(true); |
||||||
|
expect(parameterNamePattern.test('invalid_param=')).toBe(true); |
||||||
|
expect(parameterNamePattern.test('123invalid=')).toBe(false); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should validate with clause parameter values', () => { |
||||||
|
const grammar = traceqlGrammar as any; |
||||||
|
const withClause = grammar['with-clause']; |
||||||
|
expect(withClause).toBeDefined(); |
||||||
|
expect(withClause.inside).toBeDefined(); |
||||||
|
|
||||||
|
const parameterValueRule = withClause.inside['parameter-value']; |
||||||
|
expect(parameterValueRule).toBeDefined(); |
||||||
|
const parameterValuePattern = parameterValueRule.pattern as RegExp; |
||||||
|
|
||||||
|
expect(parameterValuePattern.test('true')).toBe(true); |
||||||
|
expect(parameterValuePattern.test('false')).toBe(true); |
||||||
|
expect(parameterValuePattern.test('"string_value"')).toBe(true); |
||||||
|
expect(parameterValuePattern.test("'string_value'")).toBe(true); |
||||||
|
expect(parameterValuePattern.test('123')).toBe(true); |
||||||
|
expect(parameterValuePattern.test('123.45')).toBe(true); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should validate with clause keyword', () => { |
||||||
|
const grammar = traceqlGrammar as any; |
||||||
|
const withClause = grammar['with-clause']; |
||||||
|
expect(withClause).toBeDefined(); |
||||||
|
expect(withClause.inside).toBeDefined(); |
||||||
|
|
||||||
|
const keywordRule = withClause.inside['with-keyword']; |
||||||
|
expect(keywordRule).toBeDefined(); |
||||||
|
const keywordPattern = keywordRule.pattern as RegExp; |
||||||
|
|
||||||
|
expect(keywordPattern.test('with')).toBe(true); |
||||||
|
expect(keywordPattern.test('WITH')).toBe(false); // Case sensitive
|
||||||
|
expect(keywordPattern.test('width')).toBe(false); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
describe('Edge cases', () => { |
||||||
|
it('should handle multiple with clauses (invalid but should not crash)', () => { |
||||||
|
const query = '{span.name="test"} with (most_recent=true) with (other=false)'; |
||||||
|
const grammar = traceqlGrammar as any; |
||||||
|
const withClausePattern = grammar['with-clause']?.pattern as RegExp; |
||||||
|
|
||||||
|
if (withClausePattern) { |
||||||
|
// Use global flag to match all occurrences
|
||||||
|
const globalPattern = new RegExp(withClausePattern.source, 'g'); |
||||||
|
const matches = query.match(globalPattern); |
||||||
|
expect(matches).not.toBeNull(); |
||||||
|
expect(matches!.length).toBe(2); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
it('should handle with clause without parameters', () => { |
||||||
|
const query = '{span.name="test"} with ()'; |
||||||
|
const grammar = traceqlGrammar as any; |
||||||
|
const withClausePattern = grammar['with-clause']?.pattern as RegExp; |
||||||
|
|
||||||
|
if (withClausePattern) { |
||||||
|
const matches = query.match(withClausePattern); |
||||||
|
expect(matches).not.toBeNull(); |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
Loading…
Reference in new issue