mirror of https://github.com/grafana/grafana
Loki: Add parsing of query to visual query (#46700)
* Create parser * Add parsing * Update comment * Remove operations that we don't support * Resolve type errors * Update test * Handle backticks * Handle backticks * Remove copied test, update test * Parsing for binary operations * Remove error about setting state after unmount Co-authored-by: Andrej Ocenas <mr.ocenas@gmail.com>pull/47122/head
parent
2d61022d93
commit
554492ec4e
@ -1,6 +1,6 @@ |
|||||||
{ |
{ |
||||||
"name": "eslint", |
"name": "eslint", |
||||||
"version": "8.10.0-sdk", |
"version": "8.11.0-sdk", |
||||||
"main": "./lib/api.js", |
"main": "./lib/api.js", |
||||||
"type": "commonjs" |
"type": "commonjs" |
||||||
} |
} |
||||||
|
@ -0,0 +1,326 @@ |
|||||||
|
import { buildVisualQueryFromString } from './parsing'; |
||||||
|
import { LokiVisualQuery } from './types'; |
||||||
|
|
||||||
|
describe('buildVisualQueryFromString', () => { |
||||||
|
it('parses simple query with label-values', () => { |
||||||
|
expect(buildVisualQueryFromString('{app="frontend"}')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses query with multiple label-values pairs', () => { |
||||||
|
expect(buildVisualQueryFromString('{app="frontend", instance!="1"}')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
{ |
||||||
|
op: '!=', |
||||||
|
value: '1', |
||||||
|
label: 'instance', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses query with line filter', () => { |
||||||
|
expect(buildVisualQueryFromString('{app="frontend"} |= "line"')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [{ id: '__line_contains', params: ['line'] }], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses query with line filters and escaped characters', () => { |
||||||
|
expect(buildVisualQueryFromString('{app="frontend"} |= "\\\\line"')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [{ id: '__line_contains', params: ['\\line'] }], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses query with matcher label filter', () => { |
||||||
|
expect(buildVisualQueryFromString('{app="frontend"} | bar="baz"')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [{ id: '__label_filter', params: ['bar', '=', 'baz'] }], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses query with number label filter', () => { |
||||||
|
expect(buildVisualQueryFromString('{app="frontend"} | bar >= 8')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [{ id: '__label_filter', params: ['bar', '>=', '8'] }], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses query with no pipe errors filter', () => { |
||||||
|
expect(buildVisualQueryFromString('{app="frontend"} | __error__=""')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [{ id: '__label_filter_no_errors', params: [] }], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses query with with unit label filter', () => { |
||||||
|
expect(buildVisualQueryFromString('{app="frontend"} | bar < 8mb')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [{ id: '__label_filter', params: ['bar', '<', '8mb'] }], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses query with with parser', () => { |
||||||
|
expect(buildVisualQueryFromString('{app="frontend"} | json')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [{ id: 'json', params: [] }], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses metrics query with function', () => { |
||||||
|
expect(buildVisualQueryFromString('rate({app="frontend"} | json [5m])')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [ |
||||||
|
{ id: 'json', params: [] }, |
||||||
|
{ id: 'rate', params: ['5m'] }, |
||||||
|
], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses metrics query with function and aggregation', () => { |
||||||
|
expect(buildVisualQueryFromString('sum(rate({app="frontend"} | json [5m]))')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [ |
||||||
|
{ id: 'json', params: [] }, |
||||||
|
{ id: 'rate', params: ['5m'] }, |
||||||
|
{ id: 'sum', params: [] }, |
||||||
|
], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses metrics query with function and aggregation and filters', () => { |
||||||
|
expect(buildVisualQueryFromString('sum(rate({app="frontend"} |~ `abc` | json | bar="baz" [5m]))')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [ |
||||||
|
{ id: '__line_matches_regex', params: ['abc'] }, |
||||||
|
{ id: 'json', params: [] }, |
||||||
|
{ id: '__label_filter', params: ['bar', '=', 'baz'] }, |
||||||
|
{ id: 'rate', params: ['5m'] }, |
||||||
|
{ id: 'sum', params: [] }, |
||||||
|
], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses template variables in strings', () => { |
||||||
|
expect(buildVisualQueryFromString('{instance="$label_variable"}')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [{ label: 'instance', op: '=', value: '$label_variable' }], |
||||||
|
operations: [], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses metrics query with interval variables', () => { |
||||||
|
expect(buildVisualQueryFromString('rate({app="frontend"} [$__interval])')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [{ id: 'rate', params: ['$__interval'] }], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses quantile queries', () => { |
||||||
|
expect(buildVisualQueryFromString(`quantile_over_time(0.99, {app="frontend"} [1m])`)).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [{ id: 'quantile_over_time', params: ['0.99', '1m'] }], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses query with line format', () => { |
||||||
|
expect(buildVisualQueryFromString('{app="frontend"} | line_format "abc"')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [{ id: 'line_format', params: ['abc'] }], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses query with label format', () => { |
||||||
|
expect(buildVisualQueryFromString('{app="frontend"} | label_format newLabel=oldLabel')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [{ id: 'label_format', params: ['newLabel', '=', 'oldLabel'] }], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses query with multiple label format', () => { |
||||||
|
expect(buildVisualQueryFromString('{app="frontend"} | label_format newLabel=oldLabel, bar="baz"')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'frontend', |
||||||
|
label: 'app', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [ |
||||||
|
{ id: 'label_format', params: ['newLabel', '=', 'oldLabel'] }, |
||||||
|
{ id: 'label_format', params: ['bar', '=', 'baz'] }, |
||||||
|
], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('parses binary query', () => { |
||||||
|
expect(buildVisualQueryFromString('rate({project="bar"}[5m]) / rate({project="foo"}[5m])')).toEqual( |
||||||
|
noErrors({ |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'bar', |
||||||
|
label: 'project', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [{ id: 'rate', params: ['5m'] }], |
||||||
|
binaryQueries: [ |
||||||
|
{ |
||||||
|
operator: '/', |
||||||
|
query: { |
||||||
|
labels: [ |
||||||
|
{ |
||||||
|
op: '=', |
||||||
|
value: 'foo', |
||||||
|
label: 'project', |
||||||
|
}, |
||||||
|
], |
||||||
|
operations: [{ id: 'rate', params: ['5m'] }], |
||||||
|
}, |
||||||
|
}, |
||||||
|
], |
||||||
|
}) |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
function noErrors(query: LokiVisualQuery) { |
||||||
|
return { |
||||||
|
errors: [], |
||||||
|
query, |
||||||
|
}; |
||||||
|
} |
@ -0,0 +1,484 @@ |
|||||||
|
import { parser } from '@grafana/lezer-logql'; |
||||||
|
import { SyntaxNode, TreeCursor } from '@lezer/common'; |
||||||
|
import { QueryBuilderLabelFilter, QueryBuilderOperation } from '../../prometheus/querybuilder/shared/types'; |
||||||
|
import { binaryScalarDefs } from './binaryScalarOperations'; |
||||||
|
import { LokiVisualQuery, LokiVisualQueryBinary } from './types'; |
||||||
|
|
||||||
|
// This is used for error type
|
||||||
|
const ErrorName = '⚠'; |
||||||
|
|
||||||
|
interface Context { |
||||||
|
query: LokiVisualQuery; |
||||||
|
errors: ParsingError[]; |
||||||
|
} |
||||||
|
|
||||||
|
interface ParsingError { |
||||||
|
text: string; |
||||||
|
from: number; |
||||||
|
to: number; |
||||||
|
parentType?: string; |
||||||
|
} |
||||||
|
|
||||||
|
export function buildVisualQueryFromString(expr: string): Context { |
||||||
|
const replacedExpr = replaceVariables(expr); |
||||||
|
const tree = parser.parse(replacedExpr); |
||||||
|
const node = tree.topNode; |
||||||
|
|
||||||
|
// This will be modified in the handleExpression
|
||||||
|
const visQuery: LokiVisualQuery = { |
||||||
|
labels: [], |
||||||
|
operations: [], |
||||||
|
}; |
||||||
|
|
||||||
|
const context = { |
||||||
|
query: visQuery, |
||||||
|
errors: [], |
||||||
|
}; |
||||||
|
|
||||||
|
handleExpression(replacedExpr, node, context); |
||||||
|
return context; |
||||||
|
} |
||||||
|
|
||||||
|
export function handleExpression(expr: string, node: SyntaxNode, context: Context) { |
||||||
|
const visQuery = context.query; |
||||||
|
switch (node.name) { |
||||||
|
case 'Matcher': { |
||||||
|
visQuery.labels.push(getLabel(expr, node)); |
||||||
|
const err = node.getChild(ErrorName); |
||||||
|
if (err) { |
||||||
|
context.errors.push(makeError(expr, err)); |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case 'LineFilter': { |
||||||
|
visQuery.operations.push(getLineFilter(expr, node)); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case 'LabelParser': { |
||||||
|
visQuery.operations.push(getLabelParser(expr, node)); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case 'LabelFilter': { |
||||||
|
visQuery.operations.push(getLabelFilter(expr, node)); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
// Need to figure out JsonExpressionParser
|
||||||
|
|
||||||
|
case 'LineFormatExpr': { |
||||||
|
visQuery.operations.push(getLineFormat(expr, node)); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case 'LabelFormatMatcher': { |
||||||
|
visQuery.operations.push(getLabelFormat(expr, node)); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case 'RangeAggregationExpr': { |
||||||
|
visQuery.operations.push(handleRangeAggregation(expr, node, context)); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case 'VectorAggregationExpr': { |
||||||
|
visQuery.operations.push(handleVectorAggregation(expr, node, context)); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case 'BinOpExpr': { |
||||||
|
handleBinary(expr, node, context); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
case ErrorName: { |
||||||
|
if (isIntervalVariableError(node)) { |
||||||
|
break; |
||||||
|
} |
||||||
|
context.errors.push(makeError(expr, node)); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
default: { |
||||||
|
// Any other nodes we just ignore and go to it's children. This should be fine as there are lot's of wrapper
|
||||||
|
// nodes that can be skipped.
|
||||||
|
// TODO: there are probably cases where we will just skip nodes we don't support and we should be able to
|
||||||
|
// detect those and report back.
|
||||||
|
let child = node.firstChild; |
||||||
|
while (child) { |
||||||
|
handleExpression(expr, child, context); |
||||||
|
child = child.nextSibling; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getLabel(expr: string, node: SyntaxNode): QueryBuilderLabelFilter { |
||||||
|
const labelNode = node.getChild('Identifier'); |
||||||
|
const label = getString(expr, labelNode); |
||||||
|
const op = getString(expr, labelNode!.nextSibling); |
||||||
|
const value = getString(expr, node.getChild('String')).replace(/"/g, ''); |
||||||
|
|
||||||
|
return { |
||||||
|
label, |
||||||
|
op, |
||||||
|
value, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function getLineFilter(expr: string, node: SyntaxNode): QueryBuilderOperation { |
||||||
|
const mapFilter: any = { |
||||||
|
'|=': '__line_contains', |
||||||
|
'!=': '__line_contains_not', |
||||||
|
'|~': '__line_matches_regex', |
||||||
|
'!~': '"__line_matches_regex"_not', |
||||||
|
}; |
||||||
|
const filter = getString(expr, node.getChild('Filter')); |
||||||
|
const filterExpr = handleQuotes(getString(expr, node.getChild('String'))); |
||||||
|
|
||||||
|
return { |
||||||
|
id: mapFilter[filter], |
||||||
|
params: [filterExpr], |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function getLabelParser(expr: string, node: SyntaxNode): QueryBuilderOperation { |
||||||
|
const parserNode = node.firstChild; |
||||||
|
const parser = getString(expr, parserNode); |
||||||
|
|
||||||
|
const string = handleQuotes(getString(expr, node.getChild('String'))); |
||||||
|
const params = !!string ? [string] : []; |
||||||
|
return { |
||||||
|
id: parser, |
||||||
|
params, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function getLabelFilter(expr: string, node: SyntaxNode): QueryBuilderOperation { |
||||||
|
const id = '__label_filter'; |
||||||
|
|
||||||
|
if (node.firstChild!.name === 'UnitFilter') { |
||||||
|
const filter = node.firstChild!.firstChild; |
||||||
|
const label = filter!.firstChild; |
||||||
|
const op = label!.nextSibling; |
||||||
|
const value = op!.nextSibling; |
||||||
|
const valueString = handleQuotes(getString(expr, value)); |
||||||
|
|
||||||
|
return { |
||||||
|
id, |
||||||
|
params: [getString(expr, label), getString(expr, op), valueString], |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
if (node.firstChild!.name === 'IpLabelFilter') { |
||||||
|
// Not implemented in visual query builder yet
|
||||||
|
const filter = node.firstChild!; |
||||||
|
const label = filter.firstChild!; |
||||||
|
const op = label.nextSibling!; |
||||||
|
const ip = label.nextSibling; |
||||||
|
const value = op.nextSibling!; |
||||||
|
return { |
||||||
|
id, |
||||||
|
params: [ |
||||||
|
getString(expr, label), |
||||||
|
getString(expr, op), |
||||||
|
handleQuotes(getString(expr, ip)), |
||||||
|
handleQuotes(getString(expr, value)), |
||||||
|
], |
||||||
|
}; |
||||||
|
} else { |
||||||
|
// In this case it is Matcher or NumberFilter
|
||||||
|
const filter = node.firstChild; |
||||||
|
const label = filter!.firstChild; |
||||||
|
const op = label!.nextSibling; |
||||||
|
const value = op!.nextSibling; |
||||||
|
const params = [getString(expr, label), getString(expr, op), getString(expr, value).replace(/"/g, '')]; |
||||||
|
|
||||||
|
//Special case of pipe filtering - no errors
|
||||||
|
if (params.join('') === `__error__=`) { |
||||||
|
return { |
||||||
|
id: '__label_filter_no_errors', |
||||||
|
params: [], |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
id, |
||||||
|
params, |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getLineFormat(expr: string, node: SyntaxNode): QueryBuilderOperation { |
||||||
|
// Not implemented in visual query builder yet
|
||||||
|
const id = 'line_format'; |
||||||
|
const string = handleQuotes(getString(expr, node.getChild('String'))); |
||||||
|
|
||||||
|
return { |
||||||
|
id, |
||||||
|
params: [string], |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function getLabelFormat(expr: string, node: SyntaxNode): QueryBuilderOperation { |
||||||
|
// Not implemented in visual query builder yet
|
||||||
|
const id = 'label_format'; |
||||||
|
const identifier = node.getChild('Identifier'); |
||||||
|
const op = identifier!.nextSibling; |
||||||
|
const value = op!.nextSibling; |
||||||
|
|
||||||
|
let valueString = handleQuotes(getString(expr, value)); |
||||||
|
|
||||||
|
return { |
||||||
|
id, |
||||||
|
params: [getString(expr, identifier), getString(expr, op), valueString], |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function handleRangeAggregation(expr: string, node: SyntaxNode, context: Context) { |
||||||
|
const nameNode = node.getChild('RangeOp'); |
||||||
|
const funcName = getString(expr, nameNode); |
||||||
|
const number = node.getChild('Number'); |
||||||
|
const logExpr = node.getChild('LogRangeExpr'); |
||||||
|
const params = number !== null && number !== undefined ? [getString(expr, number)] : []; |
||||||
|
|
||||||
|
let match = getString(expr, node).match(/\[(.+)\]/); |
||||||
|
if (match?.[1]) { |
||||||
|
params.push(match[1]); |
||||||
|
} |
||||||
|
|
||||||
|
const op = { |
||||||
|
id: funcName, |
||||||
|
params, |
||||||
|
}; |
||||||
|
|
||||||
|
if (logExpr) { |
||||||
|
handleExpression(expr, logExpr, context); |
||||||
|
} |
||||||
|
|
||||||
|
return op; |
||||||
|
} |
||||||
|
|
||||||
|
function handleVectorAggregation(expr: string, node: SyntaxNode, context: Context) { |
||||||
|
const nameNode = node.getChild('VectorOp'); |
||||||
|
let funcName = getString(expr, nameNode); |
||||||
|
|
||||||
|
const metricExpr = node.getChild('MetricExpr'); |
||||||
|
const op: QueryBuilderOperation = { id: funcName, params: [] }; |
||||||
|
|
||||||
|
if (metricExpr) { |
||||||
|
handleExpression(expr, metricExpr, context); |
||||||
|
} |
||||||
|
|
||||||
|
return op; |
||||||
|
} |
||||||
|
|
||||||
|
const operatorToOpName = binaryScalarDefs.reduce((acc, def) => { |
||||||
|
acc[def.sign] = { |
||||||
|
id: def.id, |
||||||
|
comparison: def.comparison, |
||||||
|
}; |
||||||
|
return acc; |
||||||
|
}, {} as Record<string, { id: string; comparison?: boolean }>); |
||||||
|
|
||||||
|
/** |
||||||
|
* Right now binary expressions can be represented in 2 way in visual query. As additional operation in case it is |
||||||
|
* just operation with scalar or it creates a binaryQuery when it's 2 queries. |
||||||
|
* @param expr |
||||||
|
* @param node |
||||||
|
* @param context |
||||||
|
*/ |
||||||
|
function handleBinary(expr: string, node: SyntaxNode, context: Context) { |
||||||
|
const visQuery = context.query; |
||||||
|
const left = node.firstChild!; |
||||||
|
const op = getString(expr, left.nextSibling); |
||||||
|
const binModifier = getBinaryModifier(expr, node.getChild('BinModifiers')); |
||||||
|
|
||||||
|
const right = node.lastChild!; |
||||||
|
|
||||||
|
const opDef = operatorToOpName[op]; |
||||||
|
|
||||||
|
const leftNumber = left.getChild('NumberLiteral'); |
||||||
|
const rightNumber = right.getChild('NumberLiteral'); |
||||||
|
|
||||||
|
const rightBinary = right.getChild('BinOpExpr'); |
||||||
|
|
||||||
|
if (leftNumber) { |
||||||
|
// TODO: this should be already handled in case parent is binary expression as it has to be added to parent
|
||||||
|
// if query starts with a number that isn't handled now.
|
||||||
|
} else { |
||||||
|
// If this is binary we don't really know if there is a query or just chained scalars. So
|
||||||
|
// we have to traverse a bit deeper to know
|
||||||
|
handleExpression(expr, left, context); |
||||||
|
} |
||||||
|
|
||||||
|
if (rightNumber) { |
||||||
|
visQuery.operations.push(makeBinOp(opDef, expr, right, !!binModifier?.isBool)); |
||||||
|
} else if (rightBinary) { |
||||||
|
// Due to the way binary ops are parsed we can get a binary operation on the right that starts with a number which
|
||||||
|
// is a factor for a current binary operation. So we have to add it as an operation now.
|
||||||
|
const leftMostChild = getLeftMostChild(right); |
||||||
|
if (leftMostChild?.name === 'NumberLiteral') { |
||||||
|
visQuery.operations.push(makeBinOp(opDef, expr, leftMostChild, !!binModifier?.isBool)); |
||||||
|
} |
||||||
|
|
||||||
|
// If we added the first number literal as operation here we still can continue and handle the rest as the first
|
||||||
|
// number will be just skipped.
|
||||||
|
handleExpression(expr, right, context); |
||||||
|
} else { |
||||||
|
visQuery.binaryQueries = visQuery.binaryQueries || []; |
||||||
|
const binQuery: LokiVisualQueryBinary = { |
||||||
|
operator: op, |
||||||
|
query: { |
||||||
|
labels: [], |
||||||
|
operations: [], |
||||||
|
}, |
||||||
|
}; |
||||||
|
if (binModifier?.isMatcher) { |
||||||
|
binQuery.vectorMatchesType = binModifier.matchType; |
||||||
|
binQuery.vectorMatches = binModifier.matches; |
||||||
|
} |
||||||
|
visQuery.binaryQueries.push(binQuery); |
||||||
|
handleExpression(expr, right, { |
||||||
|
query: binQuery.query, |
||||||
|
errors: context.errors, |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function getBinaryModifier( |
||||||
|
expr: string, |
||||||
|
node: SyntaxNode | null |
||||||
|
): |
||||||
|
| { isBool: true; isMatcher: false } |
||||||
|
| { isBool: false; isMatcher: true; matches: string; matchType: 'ignoring' | 'on' } |
||||||
|
| undefined { |
||||||
|
if (!node) { |
||||||
|
return undefined; |
||||||
|
} |
||||||
|
if (node.getChild('Bool')) { |
||||||
|
return { isBool: true, isMatcher: false }; |
||||||
|
} else { |
||||||
|
const matcher = node.getChild('OnOrIgnoring'); |
||||||
|
if (!matcher) { |
||||||
|
// Not sure what this could be, maybe should be an error.
|
||||||
|
return undefined; |
||||||
|
} |
||||||
|
const labels = getString(expr, matcher.getChild('GroupingLabels')?.getChild('GroupingLabelList')); |
||||||
|
return { |
||||||
|
isMatcher: true, |
||||||
|
isBool: false, |
||||||
|
matches: labels, |
||||||
|
matchType: matcher.getChild('On') ? 'on' : 'ignoring', |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function makeBinOp( |
||||||
|
opDef: { id: string; comparison?: boolean }, |
||||||
|
expr: string, |
||||||
|
numberNode: SyntaxNode, |
||||||
|
hasBool: boolean |
||||||
|
) { |
||||||
|
const params: any[] = [parseFloat(getString(expr, numberNode))]; |
||||||
|
if (opDef.comparison) { |
||||||
|
params.unshift(hasBool); |
||||||
|
} |
||||||
|
return { |
||||||
|
id: opDef.id, |
||||||
|
params, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function getLeftMostChild(cur: SyntaxNode): SyntaxNode | null { |
||||||
|
let child = cur; |
||||||
|
while (true) { |
||||||
|
if (child.firstChild) { |
||||||
|
child = child.firstChild; |
||||||
|
} else { |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
return child; |
||||||
|
} |
||||||
|
|
||||||
|
function getString(expr: string, node: SyntaxNode | TreeCursor | null | undefined) { |
||||||
|
if (!node) { |
||||||
|
return ''; |
||||||
|
} |
||||||
|
|
||||||
|
return returnVariables(expr.substring(node.from, node.to)); |
||||||
|
} |
||||||
|
|
||||||
|
function makeError(expr: string, node: SyntaxNode) { |
||||||
|
return { |
||||||
|
text: getString(expr, node), |
||||||
|
from: node.from, |
||||||
|
to: node.to, |
||||||
|
parentType: node.parent?.name, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function isIntervalVariableError(node: SyntaxNode) { |
||||||
|
return node?.parent?.name === 'Range'; |
||||||
|
} |
||||||
|
|
||||||
|
function handleQuotes(string: string) { |
||||||
|
if (string[0] === `"` && string[string.length - 1] === `"`) { |
||||||
|
return string.replace(/"/g, '').replace(/\\\\/g, '\\'); |
||||||
|
} |
||||||
|
return string.replace(/`/g, ''); |
||||||
|
} |
||||||
|
|
||||||
|
// Template variables
|
||||||
|
// Taken from template_srv, but copied so to not mess with the regex.index which is manipulated in the service
|
||||||
|
/* |
||||||
|
* This regex matches 3 types of variable reference with an optional format specifier |
||||||
|
* \$(\w+) $var1 |
||||||
|
* \[\[([\s\S]+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]] |
||||||
|
* \${(\w+)(?::(\w+))?} ${var3} or ${var3:fmt3} |
||||||
|
*/ |
||||||
|
const variableRegex = /\$(\w+)|\[\[([\s\S]+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g; |
||||||
|
|
||||||
|
/** |
||||||
|
* As variables with $ are creating parsing errors, we first replace them with magic string that is parseable and at |
||||||
|
* the same time we can get the variable and it's format back from it. |
||||||
|
* @param expr |
||||||
|
*/ |
||||||
|
function replaceVariables(expr: string) { |
||||||
|
return expr.replace(variableRegex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => { |
||||||
|
const fmt = fmt2 || fmt3; |
||||||
|
let variable = var1; |
||||||
|
let varType = '0'; |
||||||
|
|
||||||
|
if (var2) { |
||||||
|
variable = var2; |
||||||
|
varType = '1'; |
||||||
|
} |
||||||
|
|
||||||
|
if (var3) { |
||||||
|
variable = var3; |
||||||
|
varType = '2'; |
||||||
|
} |
||||||
|
|
||||||
|
return `__V_${varType}__` + variable + '__V__' + (fmt ? '__F__' + fmt + '__F__' : ''); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
const varTypeFunc = [ |
||||||
|
(v: string, f?: string) => `\$${v}`, |
||||||
|
(v: string, f?: string) => `[[${v}${f ? `:${f}` : ''}]]`, |
||||||
|
(v: string, f?: string) => `\$\{${v}${f ? `:${f}` : ''}\}`, |
||||||
|
]; |
||||||
|
|
||||||
|
/** |
||||||
|
* Get beck the text with variables in their original format. |
||||||
|
* @param expr |
||||||
|
*/ |
||||||
|
function returnVariables(expr: string) { |
||||||
|
return expr.replace(/__V_(\d)__(.+)__V__(?:__F__(\w+)__F__)?/g, (match, type, v, f) => { |
||||||
|
return varTypeFunc[parseInt(type, 10)](v, f); |
||||||
|
}); |
||||||
|
} |
Loading…
Reference in new issue