Prometheus: Parsing for vector matching and UI changes (#46668)
This commit is contained in:
@@ -258,26 +258,6 @@ describe('PromQueryModeller', () => {
|
|||||||
).toBe('(metric_a * 1000) / metric_b');
|
).toBe('(metric_a * 1000) / metric_b');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Can render with binary queries with vectorMatches expression', () => {
|
|
||||||
expect(
|
|
||||||
modeller.renderQuery({
|
|
||||||
metric: 'metric_a',
|
|
||||||
labels: [],
|
|
||||||
operations: [],
|
|
||||||
binaryQueries: [
|
|
||||||
{
|
|
||||||
operator: '/',
|
|
||||||
vectorMatches: 'on(le)',
|
|
||||||
query: {
|
|
||||||
metric: 'metric_b',
|
|
||||||
labels: [],
|
|
||||||
operations: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
).toBe('metric_a / on(le) metric_b');
|
|
||||||
});
|
|
||||||
it('Can render functions that require a range as a parameter', () => {
|
it('Can render functions that require a range as a parameter', () => {
|
||||||
expect(
|
expect(
|
||||||
modeller.renderQuery({
|
modeller.renderQuery({
|
||||||
@@ -305,6 +285,7 @@ describe('PromQueryModeller', () => {
|
|||||||
})
|
})
|
||||||
).toBe('label_join(metric_a, "label_1", ",", "label_2")');
|
).toBe('label_join(metric_a, "label_1", ",", "label_2")');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Can render label_join with extra parameters', () => {
|
it('Can render label_join with extra parameters', () => {
|
||||||
expect(
|
expect(
|
||||||
modeller.renderQuery({
|
modeller.renderQuery({
|
||||||
@@ -314,4 +295,26 @@ describe('PromQueryModeller', () => {
|
|||||||
})
|
})
|
||||||
).toBe('label_join(metric_a, "label_1", ", ", "label_2", "label_3", "label_4", "label_5")');
|
).toBe('label_join(metric_a, "label_1", ", ", "label_2", "label_3", "label_4", "label_5")');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can render vector matchers', () => {
|
||||||
|
expect(
|
||||||
|
modeller.renderQuery({
|
||||||
|
metric: 'metric_a',
|
||||||
|
labels: [],
|
||||||
|
operations: [],
|
||||||
|
binaryQueries: [
|
||||||
|
{
|
||||||
|
operator: '/',
|
||||||
|
vectorMatches: 'le, foo',
|
||||||
|
vectorMatchesType: 'on',
|
||||||
|
query: {
|
||||||
|
metric: 'metric_b',
|
||||||
|
labels: [],
|
||||||
|
operations: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
).toBe('metric_a / on(le, foo) metric_b');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,18 +37,35 @@ export const NestedQuery = React.memo<Props>(({ nestedQuery, index, datasource,
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className={styles.name}>Vector matches</div>
|
<div className={styles.name}>Vector matches</div>
|
||||||
|
<div className={styles.vectorMatchWrapper}>
|
||||||
<AutoSizeInput
|
<Select<PromVisualQueryBinary['vectorMatchesType']>
|
||||||
minWidth={20}
|
width="auto"
|
||||||
defaultValue={nestedQuery.vectorMatches}
|
value={nestedQuery.vectorMatchesType || 'on'}
|
||||||
onCommitChange={(evt) => {
|
allowCustomValue
|
||||||
onChange(index, {
|
options={[
|
||||||
...nestedQuery,
|
{ value: 'on', label: 'on' },
|
||||||
vectorMatches: evt.currentTarget.value,
|
{ value: 'ignoring', label: 'ignoring' },
|
||||||
});
|
]}
|
||||||
}}
|
onChange={(val) => {
|
||||||
/>
|
onChange(index, {
|
||||||
|
...nestedQuery,
|
||||||
|
vectorMatchesType: val.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AutoSizeInput
|
||||||
|
className={styles.vectorMatchInput}
|
||||||
|
minWidth={20}
|
||||||
|
defaultValue={nestedQuery.vectorMatches}
|
||||||
|
onCommitChange={(evt) => {
|
||||||
|
onChange(index, {
|
||||||
|
...nestedQuery,
|
||||||
|
vectorMatches: evt.currentTarget.value,
|
||||||
|
vectorMatchesType: nestedQuery.vectorMatchesType || 'on',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<FlexItem grow={1} />
|
<FlexItem grow={1} />
|
||||||
<IconButton name="times" size="sm" onClick={() => onRemove(index)} />
|
<IconButton name="times" size="sm" onClick={() => onRemove(index)} />
|
||||||
</div>
|
</div>
|
||||||
@@ -76,21 +93,33 @@ NestedQuery.displayName = 'NestedQuery';
|
|||||||
const getStyles = (theme: GrafanaTheme2) => {
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
card: css({
|
card: css({
|
||||||
|
label: 'card',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: theme.spacing(0.5),
|
gap: theme.spacing(0.5),
|
||||||
}),
|
}),
|
||||||
header: css({
|
header: css({
|
||||||
|
label: 'header',
|
||||||
padding: theme.spacing(0.5, 0.5, 0.5, 1),
|
padding: theme.spacing(0.5, 0.5, 0.5, 1),
|
||||||
gap: theme.spacing(1),
|
gap: theme.spacing(1),
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}),
|
}),
|
||||||
name: css({
|
name: css({
|
||||||
|
label: 'name',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
}),
|
}),
|
||||||
body: css({
|
body: css({
|
||||||
|
label: 'body',
|
||||||
paddingLeft: theme.spacing(2),
|
paddingLeft: theme.spacing(2),
|
||||||
}),
|
}),
|
||||||
|
vectorMatchInput: css({
|
||||||
|
label: 'vectorMatchInput',
|
||||||
|
marginLeft: -1,
|
||||||
|
}),
|
||||||
|
vectorMatchWrapper: css({
|
||||||
|
label: 'vectorMatchWrapper',
|
||||||
|
display: 'flex',
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -505,6 +505,42 @@ describe('buildVisualQueryFromString', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('handles binary operation with vector matchers', () => {
|
||||||
|
expect(buildVisualQueryFromString('foo * on(foo, bar) metric')).toEqual({
|
||||||
|
errors: [],
|
||||||
|
query: {
|
||||||
|
metric: 'foo',
|
||||||
|
labels: [],
|
||||||
|
operations: [],
|
||||||
|
binaryQueries: [
|
||||||
|
{
|
||||||
|
operator: '*',
|
||||||
|
vectorMatches: 'foo, bar',
|
||||||
|
vectorMatchesType: 'on',
|
||||||
|
query: { metric: 'metric', labels: [], operations: [] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(buildVisualQueryFromString('foo * ignoring(foo) metric')).toEqual({
|
||||||
|
errors: [],
|
||||||
|
query: {
|
||||||
|
metric: 'foo',
|
||||||
|
labels: [],
|
||||||
|
operations: [],
|
||||||
|
binaryQueries: [
|
||||||
|
{
|
||||||
|
operator: '*',
|
||||||
|
vectorMatches: 'foo',
|
||||||
|
vectorMatchesType: 'ignoring',
|
||||||
|
query: { metric: 'metric', labels: [], operations: [] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function noErrors(query: PromVisualQuery) {
|
function noErrors(query: PromVisualQuery) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { parser } from 'lezer-promql';
|
import { parser } from 'lezer-promql';
|
||||||
import { SyntaxNode, TreeCursor } from '@lezer/common';
|
import { SyntaxNode, TreeCursor } from '@lezer/common';
|
||||||
import { QueryBuilderLabelFilter, QueryBuilderOperation } from './shared/types';
|
import { QueryBuilderLabelFilter, QueryBuilderOperation } from './shared/types';
|
||||||
import { PromVisualQuery } from './types';
|
import { PromVisualQuery, PromVisualQueryBinary } from './types';
|
||||||
import { binaryScalarDefs } from './binaryScalarOperations';
|
import { binaryScalarDefs } from './binaryScalarOperations';
|
||||||
|
|
||||||
// Taken from template_srv, but copied so to not mess with the regex.index which is manipulated in the service
|
// Taken from template_srv, but copied so to not mess with the regex.index which is manipulated in the service
|
||||||
@@ -342,7 +342,8 @@ function handleBinary(expr: string, node: SyntaxNode, context: Context) {
|
|||||||
const visQuery = context.query;
|
const visQuery = context.query;
|
||||||
const left = node.firstChild!;
|
const left = node.firstChild!;
|
||||||
const op = getString(expr, left.nextSibling);
|
const op = getString(expr, left.nextSibling);
|
||||||
const binModifier = node.getChild('BinModifiers')?.getChild('Bool');
|
const binModifier = getBinaryModifier(expr, node.getChild('BinModifiers'));
|
||||||
|
|
||||||
const right = node.lastChild!;
|
const right = node.lastChild!;
|
||||||
|
|
||||||
const opDef = operatorToOpName[op];
|
const opDef = operatorToOpName[op];
|
||||||
@@ -362,13 +363,13 @@ function handleBinary(expr: string, node: SyntaxNode, context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (rightNumber) {
|
if (rightNumber) {
|
||||||
visQuery.operations.push(makeBinOp(opDef, expr, right, binModifier));
|
visQuery.operations.push(makeBinOp(opDef, expr, right, !!binModifier?.isBool));
|
||||||
} else if (rightBinary) {
|
} 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
|
// 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.
|
// is a factor for a current binary operation. So we have to add it as an operation now.
|
||||||
const leftMostChild = getLeftMostChild(right);
|
const leftMostChild = getLeftMostChild(right);
|
||||||
if (leftMostChild?.name === 'NumberLiteral') {
|
if (leftMostChild?.name === 'NumberLiteral') {
|
||||||
visQuery.operations.push(makeBinOp(opDef, expr, leftMostChild, binModifier));
|
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
|
// If we added the first number literal as operation here we still can continue and handle the rest as the first
|
||||||
@@ -376,7 +377,7 @@ function handleBinary(expr: string, node: SyntaxNode, context: Context) {
|
|||||||
handleExpression(expr, right, context);
|
handleExpression(expr, right, context);
|
||||||
} else {
|
} else {
|
||||||
visQuery.binaryQueries = visQuery.binaryQueries || [];
|
visQuery.binaryQueries = visQuery.binaryQueries || [];
|
||||||
const binQuery = {
|
const binQuery: PromVisualQueryBinary = {
|
||||||
operator: op,
|
operator: op,
|
||||||
query: {
|
query: {
|
||||||
metric: '',
|
metric: '',
|
||||||
@@ -384,6 +385,10 @@ function handleBinary(expr: string, node: SyntaxNode, context: Context) {
|
|||||||
operations: [],
|
operations: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
if (binModifier?.isMatcher) {
|
||||||
|
binQuery.vectorMatchesType = binModifier.matchType;
|
||||||
|
binQuery.vectorMatches = binModifier.matches;
|
||||||
|
}
|
||||||
visQuery.binaryQueries.push(binQuery);
|
visQuery.binaryQueries.push(binQuery);
|
||||||
handleExpression(expr, right, {
|
handleExpression(expr, right, {
|
||||||
query: binQuery.query,
|
query: binQuery.query,
|
||||||
@@ -392,15 +397,43 @@ function handleBinary(expr: string, node: SyntaxNode, context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
function makeBinOp(
|
||||||
opDef: { id: string; comparison?: boolean },
|
opDef: { id: string; comparison?: boolean },
|
||||||
expr: string,
|
expr: string,
|
||||||
numberNode: SyntaxNode,
|
numberNode: SyntaxNode,
|
||||||
binModifier?: SyntaxNode | null
|
hasBool: boolean
|
||||||
) {
|
) {
|
||||||
const params: any[] = [parseFloat(getString(expr, numberNode))];
|
const params: any[] = [parseFloat(getString(expr, numberNode))];
|
||||||
if (opDef.comparison) {
|
if (opDef.comparison) {
|
||||||
params.unshift(Boolean(binModifier));
|
params.unshift(hasBool);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
id: opDef.id,
|
id: opDef.id,
|
||||||
@@ -414,7 +447,7 @@ function makeBinOp(
|
|||||||
* @param expr
|
* @param expr
|
||||||
* @param node
|
* @param node
|
||||||
*/
|
*/
|
||||||
function getString(expr: string, node: SyntaxNode | TreeCursor | null) {
|
function getString(expr: string, node: SyntaxNode | TreeCursor | null | undefined) {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -9,6 +9,7 @@ import {
|
|||||||
|
|
||||||
export interface VisualQueryBinary<T> {
|
export interface VisualQueryBinary<T> {
|
||||||
operator: string;
|
operator: string;
|
||||||
|
vectorMatchesType?: 'on' | 'ignoring';
|
||||||
vectorMatches?: string;
|
vectorMatches?: string;
|
||||||
query: T;
|
query: T;
|
||||||
}
|
}
|
||||||
@@ -66,7 +67,7 @@ export abstract class LokiAndPromQueryModellerBase<T extends QueryWithOperations
|
|||||||
let result = leftOperand + ` ${binaryQuery.operator} `;
|
let result = leftOperand + ` ${binaryQuery.operator} `;
|
||||||
|
|
||||||
if (binaryQuery.vectorMatches) {
|
if (binaryQuery.vectorMatches) {
|
||||||
result += `${binaryQuery.vectorMatches} `;
|
result += `${binaryQuery.vectorMatchesType}(${binaryQuery.vectorMatches}) `;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result + this.renderQuery(binaryQuery.query, true);
|
return result + this.renderQuery(binaryQuery.query, true);
|
||||||
|
|||||||
Reference in New Issue
Block a user