mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Prometheus: Query parsing for visual editor (#44824)
* Add parsing from string to visual query * Add comments * Parse expr when changing to builder * Support template variables * Report parsing errors * More error handling * Add ts-ignore for debug func * Fix comments
This commit is contained in:
parent
914966a347
commit
071ff0b399
@ -149,7 +149,7 @@ const MonacoQueryField = (props: Props) => {
|
||||
const completionProvider = getCompletionProvider(monaco, dataProvider);
|
||||
|
||||
// completion-providers in monaco are not registered directly to editor-instances,
|
||||
// they are registerd to languages. this makes it hard for us to have
|
||||
// they are registered to languages. this makes it hard for us to have
|
||||
// separate completion-providers for every query-field-instance
|
||||
// (but we need that, because they might connect to different datasources).
|
||||
// the trick we do is, we wrap the callback in a "proxy",
|
||||
@ -206,6 +206,6 @@ const MonacoQueryField = (props: Props) => {
|
||||
// we will lazy-load this module using React.lazy,
|
||||
// and that only supports default-exports,
|
||||
// so we have to default-export this, even if
|
||||
// it is agains the style-guidelines.
|
||||
// it is against the style-guidelines.
|
||||
|
||||
export default MonacoQueryField;
|
||||
|
@ -86,6 +86,14 @@ describe('PromQueryEditorSelector', () => {
|
||||
refId: 'A',
|
||||
expr: defaultQuery.expr,
|
||||
editorMode: QueryEditorMode.Builder,
|
||||
visualQuery: {
|
||||
labels: [
|
||||
{ label: 'label1', op: '=', value: 'foo' },
|
||||
{ label: 'label2', op: '=', value: 'bar' },
|
||||
],
|
||||
metric: 'metric',
|
||||
operations: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -135,6 +143,30 @@ describe('PromQueryEditorSelector', () => {
|
||||
editorMode: QueryEditorMode.Explain,
|
||||
});
|
||||
});
|
||||
|
||||
it('parses query when changing to builder mode', async () => {
|
||||
const { rerender } = renderWithProps({
|
||||
refId: 'A',
|
||||
expr: 'rate(test_metric{instance="host.docker.internal:3000"}[$__interval])',
|
||||
editorMode: QueryEditorMode.Code,
|
||||
});
|
||||
switchToMode(QueryEditorMode.Builder);
|
||||
rerender(
|
||||
<PromQueryEditorSelector
|
||||
{...defaultProps}
|
||||
query={{
|
||||
refId: 'A',
|
||||
expr: 'rate(test_metric{instance="host.docker.internal:3000"}[$__interval])',
|
||||
editorMode: QueryEditorMode.Builder,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
await screen.findByText('test_metric');
|
||||
expect(screen.getByText('host.docker.internal:3000')).toBeInTheDocument();
|
||||
expect(screen.getByText('Rate')).toBeInTheDocument();
|
||||
expect(screen.getByText('$__interval')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function renderWithMode(mode: QueryEditorMode) {
|
||||
@ -145,8 +177,8 @@ function renderWithProps(overrides?: Partial<PromQuery>) {
|
||||
const query = defaultsDeep(overrides ?? {}, cloneDeep(defaultQuery));
|
||||
const onChange = jest.fn();
|
||||
|
||||
render(<PromQueryEditorSelector {...defaultProps} query={query} onChange={onChange} />);
|
||||
return { onChange };
|
||||
const stuff = render(<PromQueryEditorSelector {...defaultProps} query={query} onChange={onChange} />);
|
||||
return { onChange, ...stuff };
|
||||
}
|
||||
|
||||
function expectCodeEditor() {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2, LoadingState } from '@grafana/data';
|
||||
import { EditorHeader, EditorRows, FlexItem, InlineSelect, Space } from '@grafana/experimental';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { Button, ConfirmModal, useStyles2 } from '@grafana/ui';
|
||||
import React, { SyntheticEvent, useCallback, useState } from 'react';
|
||||
|
||||
import { PromQueryEditor } from '../../components/PromQueryEditor';
|
||||
import { PromQueryEditorProps } from '../../components/types';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
@ -14,15 +15,31 @@ import { PromQueryBuilder } from './PromQueryBuilder';
|
||||
import { PromQueryBuilderExplained } from './PromQueryBuilderExplained';
|
||||
import { PromQueryBuilderOptions } from './PromQueryBuilderOptions';
|
||||
import { QueryPreview } from './QueryPreview';
|
||||
import { buildVisualQueryFromString } from '../parsing';
|
||||
import { PromQuery } from '../../types';
|
||||
|
||||
export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props) => {
|
||||
const { query, onChange, onRunQuery, data } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
const [visualQuery, setVisualQuery] = useState<PromVisualQuery>(query.visualQuery ?? getDefaultEmptyQuery());
|
||||
const [parseModalOpen, setParseModalOpen] = useState(false);
|
||||
const [pendingChange, setPendingChange] = useState<PromQuery | undefined>(undefined);
|
||||
|
||||
const onEditorModeChange = useCallback(
|
||||
(newMetricEditorMode: QueryEditorMode) => {
|
||||
onChange({ ...query, editorMode: newMetricEditorMode });
|
||||
const change = { ...query, editorMode: newMetricEditorMode };
|
||||
if (newMetricEditorMode === QueryEditorMode.Builder) {
|
||||
const result = buildVisualQueryFromString(query.expr);
|
||||
change.visualQuery = result.query;
|
||||
// If there are errors, give user a chance to decide if they want to go to builder as that can loose some data.
|
||||
if (result.errors.length) {
|
||||
setParseModalOpen(true);
|
||||
setPendingChange(change);
|
||||
return;
|
||||
}
|
||||
setVisualQuery(change.visualQuery);
|
||||
}
|
||||
onChange(change);
|
||||
},
|
||||
[onChange, query]
|
||||
);
|
||||
@ -49,6 +66,18 @@ export const PromQueryEditorSelector = React.memo<PromQueryEditorProps>((props)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfirmModal
|
||||
isOpen={parseModalOpen}
|
||||
title="Query parsing"
|
||||
body="There were errors while trying to parse the query. Continuing to visual builder may loose some parts of the query."
|
||||
confirmText="Continue"
|
||||
onConfirm={() => {
|
||||
setVisualQuery(pendingChange!.visualQuery!);
|
||||
onChange(pendingChange!);
|
||||
setParseModalOpen(false);
|
||||
}}
|
||||
onDismiss={() => setParseModalOpen(false)}
|
||||
/>
|
||||
<EditorHeader>
|
||||
<FlexItem grow={1} />
|
||||
<Button
|
||||
|
@ -0,0 +1,312 @@
|
||||
import { buildVisualQueryFromString } from './parsing';
|
||||
import { PromVisualQuery } from './types';
|
||||
|
||||
describe('buildVisualQueryFromString', () => {
|
||||
it('parses simple query', () => {
|
||||
expect(buildVisualQueryFromString('counters_logins{app="frontend"}')).toEqual(
|
||||
noErrors({
|
||||
metric: 'counters_logins',
|
||||
labels: [
|
||||
{
|
||||
op: '=',
|
||||
value: 'frontend',
|
||||
label: 'app',
|
||||
},
|
||||
],
|
||||
operations: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('parses query with rate and interval', () => {
|
||||
expect(buildVisualQueryFromString('rate(counters_logins{app="frontend"}[5m])')).toEqual(
|
||||
noErrors({
|
||||
metric: 'counters_logins',
|
||||
labels: [
|
||||
{
|
||||
op: '=',
|
||||
value: 'frontend',
|
||||
label: 'app',
|
||||
},
|
||||
],
|
||||
operations: [
|
||||
{
|
||||
id: 'rate',
|
||||
params: ['5m'],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('parses query with nested query and interval variable', () => {
|
||||
expect(
|
||||
buildVisualQueryFromString(
|
||||
'avg(rate(access_evaluation_duration_count{instance="host.docker.internal:3000"}[$__rate_interval]))'
|
||||
)
|
||||
).toEqual(
|
||||
noErrors({
|
||||
metric: 'access_evaluation_duration_count',
|
||||
labels: [
|
||||
{
|
||||
op: '=',
|
||||
value: 'host.docker.internal:3000',
|
||||
label: 'instance',
|
||||
},
|
||||
],
|
||||
operations: [
|
||||
{
|
||||
id: 'rate',
|
||||
params: ['$__rate_interval'],
|
||||
},
|
||||
{
|
||||
id: 'avg',
|
||||
params: [],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('parses query with aggregation by labels', () => {
|
||||
const visQuery = {
|
||||
metric: 'metric_name',
|
||||
labels: [
|
||||
{
|
||||
label: 'instance',
|
||||
op: '=',
|
||||
value: 'internal:3000',
|
||||
},
|
||||
],
|
||||
operations: [
|
||||
{
|
||||
id: '__sum_by',
|
||||
params: ['app', 'version'],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(buildVisualQueryFromString('sum(metric_name{instance="internal:3000"}) by (app, version)')).toEqual(
|
||||
noErrors(visQuery)
|
||||
);
|
||||
expect(buildVisualQueryFromString('sum by (app, version)(metric_name{instance="internal:3000"})')).toEqual(
|
||||
noErrors(visQuery)
|
||||
);
|
||||
});
|
||||
|
||||
it('parses aggregation with params', () => {
|
||||
expect(buildVisualQueryFromString('topk(5, http_requests_total)')).toEqual(
|
||||
noErrors({
|
||||
metric: 'http_requests_total',
|
||||
labels: [],
|
||||
operations: [
|
||||
{
|
||||
id: 'topk',
|
||||
params: [5],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('parses aggregation with params and labels', () => {
|
||||
expect(buildVisualQueryFromString('topk by(instance, job) (5, http_requests_total)')).toEqual(
|
||||
noErrors({
|
||||
metric: 'http_requests_total',
|
||||
labels: [],
|
||||
operations: [
|
||||
{
|
||||
id: '__topk_by',
|
||||
params: [5, 'instance', 'job'],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('parses function with multiple arguments', () => {
|
||||
expect(
|
||||
buildVisualQueryFromString(
|
||||
'label_replace(avg_over_time(http_requests_total{instance="foo"}[$__interval]), "instance", "$1", "", "(.*)")'
|
||||
)
|
||||
).toEqual(
|
||||
noErrors({
|
||||
metric: 'http_requests_total',
|
||||
labels: [{ label: 'instance', op: '=', value: 'foo' }],
|
||||
operations: [
|
||||
{
|
||||
id: 'avg_over_time',
|
||||
params: ['$__interval'],
|
||||
},
|
||||
{
|
||||
id: 'label_replace',
|
||||
params: ['instance', '$1', '', '(.*)'],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('parses binary operation with scalar', () => {
|
||||
expect(buildVisualQueryFromString('avg_over_time(http_requests_total{instance="foo"}[$__interval]) / 2')).toEqual(
|
||||
noErrors({
|
||||
metric: 'http_requests_total',
|
||||
labels: [{ label: 'instance', op: '=', value: 'foo' }],
|
||||
operations: [
|
||||
{
|
||||
id: 'avg_over_time',
|
||||
params: ['$__interval'],
|
||||
},
|
||||
{
|
||||
id: '__divide_by',
|
||||
params: [2],
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('parses binary operation with 2 queries', () => {
|
||||
expect(
|
||||
buildVisualQueryFromString('avg_over_time(http_requests_total{instance="foo"}[$__interval]) / sum(logins_count)')
|
||||
).toEqual(
|
||||
noErrors({
|
||||
metric: 'http_requests_total',
|
||||
labels: [{ label: 'instance', op: '=', value: 'foo' }],
|
||||
operations: [{ id: 'avg_over_time', params: ['$__interval'] }],
|
||||
binaryQueries: [
|
||||
{
|
||||
operator: '/',
|
||||
query: {
|
||||
metric: 'logins_count',
|
||||
labels: [],
|
||||
operations: [{ id: 'sum', params: [] }],
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('parses template variables in strings', () => {
|
||||
expect(buildVisualQueryFromString('http_requests_total{instance="$label_variable"}')).toEqual(
|
||||
noErrors({
|
||||
metric: 'http_requests_total',
|
||||
labels: [{ label: 'instance', op: '=', value: '$label_variable' }],
|
||||
operations: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('parses template variables for metric', () => {
|
||||
expect(buildVisualQueryFromString('$metric_variable{instance="foo"}')).toEqual(
|
||||
noErrors({
|
||||
metric: '$metric_variable',
|
||||
labels: [{ label: 'instance', op: '=', value: 'foo' }],
|
||||
operations: [],
|
||||
})
|
||||
);
|
||||
|
||||
expect(buildVisualQueryFromString('${metric_variable:fmt}{instance="foo"}')).toEqual(
|
||||
noErrors({
|
||||
metric: '${metric_variable:fmt}',
|
||||
labels: [{ label: 'instance', op: '=', value: 'foo' }],
|
||||
operations: [],
|
||||
})
|
||||
);
|
||||
|
||||
expect(buildVisualQueryFromString('[[metric_variable:fmt]]{instance="foo"}')).toEqual(
|
||||
noErrors({
|
||||
metric: '[[metric_variable:fmt]]',
|
||||
labels: [{ label: 'instance', op: '=', value: 'foo' }],
|
||||
operations: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('parses template variables in label name', () => {
|
||||
expect(buildVisualQueryFromString('metric{${variable_label}="foo"}')).toEqual(
|
||||
noErrors({
|
||||
metric: 'metric',
|
||||
labels: [{ label: '${variable_label}', op: '=', value: 'foo' }],
|
||||
operations: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('fails to parse variable for function', () => {
|
||||
expect(buildVisualQueryFromString('${func_var}(metric{bar="foo"})')).toEqual({
|
||||
errors: [
|
||||
{
|
||||
text: '(',
|
||||
from: 20,
|
||||
to: 21,
|
||||
parentType: 'VectorSelector',
|
||||
},
|
||||
{
|
||||
text: 'metric',
|
||||
from: 21,
|
||||
to: 27,
|
||||
parentType: 'VectorSelector',
|
||||
},
|
||||
],
|
||||
query: {
|
||||
metric: '${func_var}',
|
||||
labels: [{ label: 'bar', op: '=', value: 'foo' }],
|
||||
operations: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('fails to parse malformed query', () => {
|
||||
expect(buildVisualQueryFromString('asdf-metric{bar="})')).toEqual({
|
||||
errors: [
|
||||
{
|
||||
text: '',
|
||||
from: 19,
|
||||
to: 19,
|
||||
parentType: 'LabelMatchers',
|
||||
},
|
||||
],
|
||||
query: {
|
||||
metric: 'asdf',
|
||||
labels: [],
|
||||
operations: [],
|
||||
binaryQueries: [
|
||||
{
|
||||
operator: '-',
|
||||
query: {
|
||||
metric: 'metric',
|
||||
labels: [{ label: 'bar', op: '=', value: '})' }],
|
||||
operations: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('fails to parse malformed query 2', () => {
|
||||
expect(buildVisualQueryFromString('ewafweaf{afea=afe}')).toEqual({
|
||||
errors: [
|
||||
{
|
||||
text: 'afe}',
|
||||
from: 14,
|
||||
to: 18,
|
||||
parentType: 'LabelMatcher',
|
||||
},
|
||||
],
|
||||
query: {
|
||||
metric: 'ewafweaf',
|
||||
labels: [{ label: 'afea', op: '=', value: '' }],
|
||||
operations: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function noErrors(query: PromVisualQuery) {
|
||||
return {
|
||||
errors: [],
|
||||
query,
|
||||
};
|
||||
}
|
417
public/app/plugins/datasource/prometheus/querybuilder/parsing.ts
Normal file
417
public/app/plugins/datasource/prometheus/querybuilder/parsing.ts
Normal file
@ -0,0 +1,417 @@
|
||||
import { parser } from 'lezer-promql';
|
||||
import { SyntaxNode } from 'lezer-tree';
|
||||
import { QueryBuilderLabelFilter, QueryBuilderOperation } from './shared/types';
|
||||
import { PromVisualQuery } from './types';
|
||||
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a PromQL query into a visual query model.
|
||||
*
|
||||
* It traverses the tree and uses sort of state machine to update update the query model. The query model is modified
|
||||
* during the traversal and sent to each handler as context.
|
||||
*
|
||||
* @param expr
|
||||
*/
|
||||
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 handlers.
|
||||
const visQuery: PromVisualQuery = {
|
||||
metric: '',
|
||||
labels: [],
|
||||
operations: [],
|
||||
};
|
||||
const context = {
|
||||
query: visQuery,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
handleExpression(replacedExpr, node, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
interface ParsingError {
|
||||
text: string;
|
||||
from: number;
|
||||
to: number;
|
||||
parentType?: string;
|
||||
}
|
||||
|
||||
interface Context {
|
||||
query: PromVisualQuery;
|
||||
errors: ParsingError[];
|
||||
}
|
||||
|
||||
// This is used for error type for some reason
|
||||
const ErrorName = '⚠';
|
||||
|
||||
/**
|
||||
* Handler for default state. It will traverse the tree and call the appropriate handler for each node. The node
|
||||
* handled here does not necessarily needs to be of type == Expr.
|
||||
* @param expr
|
||||
* @param node
|
||||
* @param context
|
||||
*/
|
||||
export function handleExpression(expr: string, node: SyntaxNode, context: Context) {
|
||||
const visQuery = context.query;
|
||||
switch (node.name) {
|
||||
case 'MetricIdentifier': {
|
||||
// Expectation is that there is only one of those per query.
|
||||
visQuery.metric = getString(expr, node);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'LabelMatcher': {
|
||||
// Same as MetricIdentifier should be just one per query.
|
||||
visQuery.labels.push(getLabel(expr, node));
|
||||
const err = node.getChild(ErrorName);
|
||||
if (err) {
|
||||
context.errors.push(makeError(expr, err));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'FunctionCall': {
|
||||
handleFunction(expr, node, context);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'AggregateExpr': {
|
||||
handleAggregation(expr, node, context);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'BinaryExpr': {
|
||||
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 makeError(expr: string, node: SyntaxNode) {
|
||||
return {
|
||||
text: getString(expr, node),
|
||||
// TODO: this are positions in the string with the replaced variables. Means it cannot be used to show exact
|
||||
// placement of the error for the user. We need some translation table to positions before the variable
|
||||
// replace.
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
parentType: node.parent?.name,
|
||||
};
|
||||
}
|
||||
|
||||
function isIntervalVariableError(node: SyntaxNode) {
|
||||
return node.prevSibling?.name === 'Expr' && node.prevSibling?.firstChild?.name === 'VectorSelector';
|
||||
}
|
||||
|
||||
function getLabel(expr: string, node: SyntaxNode): QueryBuilderLabelFilter {
|
||||
const label = getString(expr, node.getChild('LabelName'));
|
||||
const op = getString(expr, node.getChild('MatchOp'));
|
||||
const value = getString(expr, node.getChild('StringLiteral')).replace(/"/g, '');
|
||||
return {
|
||||
label,
|
||||
op,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
const rangeFunctions = ['changes', 'rate', 'irate', 'increase', 'delta'];
|
||||
/**
|
||||
* Handle function call which is usually and identifier and its body > arguments.
|
||||
* @param expr
|
||||
* @param node
|
||||
* @param context
|
||||
*/
|
||||
function handleFunction(expr: string, node: SyntaxNode, context: Context) {
|
||||
const visQuery = context.query;
|
||||
const nameNode = node.getChild('FunctionIdentifier');
|
||||
const funcName = getString(expr, nameNode);
|
||||
|
||||
const body = node.getChild('FunctionCallBody');
|
||||
const callArgs = body!.getChild('FunctionCallArgs');
|
||||
const params = [];
|
||||
|
||||
// This is a bit of a shortcut to get the interval argument. Reasons are
|
||||
// - interval is not part of the function args per promQL grammar but we model it as argument for the function in
|
||||
// the query model.
|
||||
// - it is easier to handle template variables this way as template variable is an error for the parser
|
||||
if (rangeFunctions.includes(funcName) || funcName.endsWith('_over_time')) {
|
||||
let match = getString(expr, node).match(/\[(.+)\]/);
|
||||
if (match?.[1]) {
|
||||
params.push(returnVariables(match[1]));
|
||||
}
|
||||
}
|
||||
|
||||
const op = { id: funcName, params };
|
||||
// We unshift operations to keep the more natural order that we want to have in the visual query editor.
|
||||
visQuery.operations.unshift(op);
|
||||
updateFunctionArgs(expr, callArgs!, context, op);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle aggregation as they are distinct type from other functions.
|
||||
* @param expr
|
||||
* @param node
|
||||
* @param context
|
||||
*/
|
||||
function handleAggregation(expr: string, node: SyntaxNode, context: Context) {
|
||||
const visQuery = context.query;
|
||||
const nameNode = node.getChild('AggregateOp');
|
||||
let funcName = getString(expr, nameNode);
|
||||
|
||||
const modifier = node.getChild('AggregateModifier');
|
||||
const labels = [];
|
||||
|
||||
// TODO: support also Without modifier (but we don't support it in visual query yet)
|
||||
if (modifier) {
|
||||
const byModifier = modifier.getChild(`By`);
|
||||
if (byModifier && funcName) {
|
||||
funcName = `__${funcName}_by`;
|
||||
}
|
||||
labels.push(...getAllByType(expr, modifier, 'GroupingLabel'));
|
||||
}
|
||||
|
||||
const body = node.getChild('FunctionCallBody');
|
||||
const callArgs = body!.getChild('FunctionCallArgs');
|
||||
|
||||
const op: QueryBuilderOperation = { id: funcName, params: [] };
|
||||
visQuery.operations.unshift(op);
|
||||
updateFunctionArgs(expr, callArgs!, context, op);
|
||||
// We add labels after params in the visual query editor.
|
||||
op.params.push(...labels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle (probably) all types of arguments that function or aggregation can have.
|
||||
*
|
||||
* FunctionCallArgs are nested bit weirdly basically its [firstArg, ...rest] where rest is again FunctionCallArgs so
|
||||
* we cannot just get all the children and iterate them as arguments we have to again recursively traverse through
|
||||
* them.
|
||||
*
|
||||
* @param expr
|
||||
* @param node
|
||||
* @param context
|
||||
* @param op - We need the operation to add the params to as an additional context.
|
||||
*/
|
||||
function updateFunctionArgs(expr: string, node: SyntaxNode, context: Context, op: QueryBuilderOperation) {
|
||||
switch (node.name) {
|
||||
// In case we have an expression we don't know what kind so we have to look at the child as it can be anything.
|
||||
case 'Expr':
|
||||
// FunctionCallArgs are nested bit weirdly as mentioned so we have to go one deeper in this case.
|
||||
case 'FunctionCallArgs': {
|
||||
let child = node.firstChild;
|
||||
while (child) {
|
||||
updateFunctionArgs(expr, child, context, op);
|
||||
child = child.nextSibling;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'NumberLiteral': {
|
||||
op.params.push(parseInt(getString(expr, node), 10));
|
||||
break;
|
||||
}
|
||||
|
||||
case 'StringLiteral': {
|
||||
op.params.push(getString(expr, node).replace(/"/g, ''));
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// Means we get to something that does not seem like simple function arg and is probably nested query so jump
|
||||
// back to main context
|
||||
handleExpression(expr, node, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const operatorToOpName: Record<string, string> = {
|
||||
'/': '__divide_by',
|
||||
'*': '__multiply_by',
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 right = node.lastChild!;
|
||||
|
||||
const opName = operatorToOpName[op];
|
||||
|
||||
const leftNumber = left.getChild('NumberLiteral');
|
||||
const rightNumber = right.getChild('NumberLiteral');
|
||||
|
||||
if (leftNumber || rightNumber) {
|
||||
// Scalar case, just add operation.
|
||||
const [num, query] = leftNumber ? [leftNumber, right] : [rightNumber, left];
|
||||
visQuery.operations.push({ id: opName, params: [parseInt(getString(expr, num), 10)] });
|
||||
handleExpression(expr, query, context);
|
||||
} else {
|
||||
// Two queries case so we create a binary query.
|
||||
visQuery.binaryQueries = visQuery.binaryQueries || [];
|
||||
const binQuery = {
|
||||
operator: op,
|
||||
query: {
|
||||
metric: '',
|
||||
labels: [],
|
||||
operations: [],
|
||||
},
|
||||
};
|
||||
visQuery.binaryQueries.push(binQuery);
|
||||
// One query is the main query, second is wrapped in the binaryQuery wrapper.
|
||||
handleExpression(expr, left, context);
|
||||
handleExpression(expr, right, {
|
||||
query: binQuery.query,
|
||||
errors: context.errors,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual string of the expression. That is not stored in the tree so we have to get the indexes from the node
|
||||
* and then based on that get it from the expression.
|
||||
* @param expr
|
||||
* @param node
|
||||
*/
|
||||
function getString(expr: string, node: SyntaxNode | null) {
|
||||
if (!node) {
|
||||
return '';
|
||||
}
|
||||
return returnVariables(expr.substring(node.from, node.to));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all nodes with type in the tree. This traverses the tree so it is safe only when you know there shouldn't be
|
||||
* too much nesting but you just want to skip some of the wrappers. For example getting function args this way would
|
||||
* not be safe is it would also find arguments of nested functions.
|
||||
* @param expr
|
||||
* @param cur
|
||||
* @param type
|
||||
*/
|
||||
function getAllByType(expr: string, cur: SyntaxNode, type: string): string[] {
|
||||
if (cur.name === type) {
|
||||
return [getString(expr, cur)];
|
||||
}
|
||||
const values: string[] = [];
|
||||
let pos = 0;
|
||||
let child = cur.childAfter(pos);
|
||||
while (child) {
|
||||
values.push(...getAllByType(expr, child, type));
|
||||
pos = child.to;
|
||||
child = cur.childAfter(pos);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
// Debugging function for convenience.
|
||||
// @ts-ignore
|
||||
function log(expr: string, cur?: SyntaxNode) {
|
||||
const json = toJson(expr, cur);
|
||||
if (!json) {
|
||||
console.log('<empty>');
|
||||
return;
|
||||
}
|
||||
console.log(JSON.stringify(json, undefined, 2));
|
||||
}
|
||||
|
||||
function toJson(expr: string, cur?: SyntaxNode) {
|
||||
if (!cur) {
|
||||
return undefined;
|
||||
}
|
||||
const treeJson: any = {};
|
||||
const name = nodeToString(expr, cur);
|
||||
const children = [];
|
||||
|
||||
let pos = 0;
|
||||
let child = cur.childAfter(pos);
|
||||
while (child) {
|
||||
children.push(toJson(expr, child));
|
||||
pos = child.to;
|
||||
child = cur.childAfter(pos);
|
||||
}
|
||||
|
||||
treeJson[name] = children;
|
||||
return treeJson;
|
||||
}
|
||||
|
||||
function nodeToString(expr: string, node: SyntaxNode) {
|
||||
return node.name + ':' + getString(expr, node);
|
||||
}
|
Loading…
Reference in New Issue
Block a user