mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
parent
2d61022d93
commit
554492ec4e
2
.yarn/sdks/eslint/package.json
vendored
2
.yarn/sdks/eslint/package.json
vendored
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eslint",
|
||||
"version": "8.10.0-sdk",
|
||||
"version": "8.11.0-sdk",
|
||||
"main": "./lib/api.js",
|
||||
"type": "commonjs"
|
||||
}
|
||||
|
@ -246,6 +246,7 @@
|
||||
"@grafana/e2e-selectors": "workspace:*",
|
||||
"@grafana/experimental": "0.0.2-canary.22",
|
||||
"@grafana/google-sdk": "0.0.3",
|
||||
"@grafana/lezer-logql": "^0.0.11",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/slate-react": "0.22.10-grafana",
|
||||
|
@ -71,6 +71,7 @@ interface LokiQueryFieldState {
|
||||
|
||||
export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, LokiQueryFieldState> {
|
||||
plugins: Plugin[];
|
||||
_isMounted = false;
|
||||
|
||||
constructor(props: LokiQueryFieldProps) {
|
||||
super(props);
|
||||
@ -90,8 +91,15 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
this._isMounted = true;
|
||||
await this.props.datasource.languageProvider.start();
|
||||
this.setState({ labelsLoaded: true });
|
||||
if (this._isMounted) {
|
||||
this.setState({ labelsLoaded: true });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: LokiQueryFieldProps) {
|
||||
|
@ -79,6 +79,13 @@ describe('LokiQueryEditorSelector', () => {
|
||||
expr: defaultQuery.expr,
|
||||
queryType: LokiQueryType.Range,
|
||||
editorMode: QueryEditorMode.Builder,
|
||||
visualQuery: {
|
||||
labels: [
|
||||
{ label: 'label1', op: '=', value: 'foo' },
|
||||
{ label: 'label2', op: '=', value: 'bar' },
|
||||
],
|
||||
operations: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -128,29 +135,28 @@ describe('LokiQueryEditorSelector', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// 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,
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
it('parses query when changing to builder mode', async () => {
|
||||
const { rerender } = renderWithProps({
|
||||
refId: 'A',
|
||||
expr: 'rate({instance="host.docker.internal:3000"}[$__interval])',
|
||||
editorMode: QueryEditorMode.Code,
|
||||
});
|
||||
switchToMode(QueryEditorMode.Builder);
|
||||
rerender(
|
||||
<LokiQueryEditorSelector
|
||||
{...defaultProps}
|
||||
query={{
|
||||
refId: 'A',
|
||||
expr: 'rate({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();
|
||||
// });
|
||||
await screen.findByText('host.docker.internal:3000');
|
||||
expect(screen.getByText('Rate')).toBeInTheDocument();
|
||||
expect(screen.getByText('$__interval')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
function renderWithMode(mode: QueryEditorMode) {
|
||||
|
@ -1,11 +1,13 @@
|
||||
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, useStyles2, ConfirmModal } from '@grafana/ui';
|
||||
import { QueryEditorModeToggle } from 'app/plugins/datasource/prometheus/querybuilder/shared/QueryEditorModeToggle';
|
||||
import { QueryEditorMode } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { LokiQueryEditorProps } from '../../components/types';
|
||||
import { LokiQuery } from '../../types';
|
||||
|
||||
import { lokiQueryModeller } from '../LokiQueryModeller';
|
||||
import { getQueryWithDefaults } from '../state';
|
||||
import { getDefaultEmptyQuery, LokiVisualQuery } from '../types';
|
||||
@ -13,16 +15,31 @@ import { LokiQueryBuilder } from './LokiQueryBuilder';
|
||||
import { LokiQueryBuilderExplained } from './LokiQueryBuilderExplaind';
|
||||
import { LokiQueryBuilderOptions } from './LokiQueryBuilderOptions';
|
||||
import { LokiQueryCodeEditor } from './LokiQueryCodeEditor';
|
||||
import { buildVisualQueryFromString } from '../parsing';
|
||||
|
||||
export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((props) => {
|
||||
const { onChange, onRunQuery, data } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
const query = getQueryWithDefaults(props.query);
|
||||
const [visualQuery, setVisualQuery] = useState<LokiVisualQuery>(query.visualQuery ?? getDefaultEmptyQuery());
|
||||
const [parseModalOpen, setParseModalOpen] = useState(false);
|
||||
const [pendingChange, setPendingChange] = useState<LokiQuery | 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]
|
||||
);
|
||||
@ -43,6 +60,18 @@ export const LokiQueryEditorSelector = React.memo<LokiQueryEditorProps>((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
|
||||
|
326
public/app/plugins/datasource/loki/querybuilder/parsing.test.ts
Normal file
326
public/app/plugins/datasource/loki/querybuilder/parsing.test.ts
Normal file
@ -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,
|
||||
};
|
||||
}
|
484
public/app/plugins/datasource/loki/querybuilder/parsing.ts
Normal file
484
public/app/plugins/datasource/loki/querybuilder/parsing.ts
Normal file
@ -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);
|
||||
});
|
||||
}
|
28
yarn.lock
28
yarn.lock
@ -4156,6 +4156,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@grafana/lezer-logql@npm:^0.0.11":
|
||||
version: 0.0.11
|
||||
resolution: "@grafana/lezer-logql@npm:0.0.11"
|
||||
dependencies:
|
||||
lezer: ^0.13.5
|
||||
peerDependencies:
|
||||
"@lezer/lr": ^0.15.8
|
||||
checksum: 0427e59528ea5092e53a70b9b6be37b0ed29ca4c0ddf452cc192f8bd14d21a725ba1959b2e33e7567b42e4d0391cff4e4f36c249d7099a157d0f1e9093ba64e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@grafana/runtime@workspace:*, @grafana/runtime@workspace:packages/grafana-runtime":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@grafana/runtime@workspace:packages/grafana-runtime"
|
||||
@ -20420,6 +20431,7 @@ __metadata:
|
||||
"@grafana/eslint-config": 3.0.0
|
||||
"@grafana/experimental": 0.0.2-canary.22
|
||||
"@grafana/google-sdk": 0.0.3
|
||||
"@grafana/lezer-logql": ^0.0.11
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/schema": "workspace:*"
|
||||
"@grafana/slate-react": 0.22.10-grafana
|
||||
@ -24754,6 +24766,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lezer-tree@npm:^0.13.2":
|
||||
version: 0.13.2
|
||||
resolution: "lezer-tree@npm:0.13.2"
|
||||
checksum: b8be213c780191e0669c7f440aa563218ada762d2cf399b94e755a563cc7da8951929fa3ee65df9ef6586a81223c55cd662e1ec5b49060d892acca4198cf3596
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lezer@npm:^0.13.5":
|
||||
version: 0.13.5
|
||||
resolution: "lezer@npm:0.13.5"
|
||||
dependencies:
|
||||
lezer-tree: ^0.13.2
|
||||
checksum: a5c3aa01c539aba3377a927063bcd63b311737a7abfd71ad2c2229ed4e48b7858f2e8e11e925f8f5286c2629251f77dfabf7505ea6c707499cd9917ca90934c8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"libnpmaccess@npm:^4.0.1":
|
||||
version: 4.0.3
|
||||
resolution: "libnpmaccess@npm:4.0.3"
|
||||
|
Loading…
Reference in New Issue
Block a user