diff --git a/public/app/plugins/datasource/prometheus/querybuilder/PromQueryModeller.test.ts b/public/app/plugins/datasource/prometheus/querybuilder/PromQueryModeller.test.ts index cdf8c5e4a50..d4ef1582682 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/PromQueryModeller.test.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/PromQueryModeller.test.ts @@ -258,26 +258,6 @@ describe('PromQueryModeller', () => { ).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', () => { expect( modeller.renderQuery({ @@ -305,6 +285,7 @@ describe('PromQueryModeller', () => { }) ).toBe('label_join(metric_a, "label_1", ",", "label_2")'); }); + it('Can render label_join with extra parameters', () => { expect( modeller.renderQuery({ @@ -314,4 +295,26 @@ describe('PromQueryModeller', () => { }) ).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'); + }); }); diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/NestedQuery.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/NestedQuery.tsx index 37c04f62e66..5c1780ad76f 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/components/NestedQuery.tsx +++ b/public/app/plugins/datasource/prometheus/querybuilder/components/NestedQuery.tsx @@ -37,18 +37,35 @@ export const NestedQuery = React.memo(({ nestedQuery, index, datasource, }} />
Vector matches
- - { - onChange(index, { - ...nestedQuery, - vectorMatches: evt.currentTarget.value, - }); - }} - /> - +
+ + width="auto" + value={nestedQuery.vectorMatchesType || 'on'} + allowCustomValue + options={[ + { value: 'on', label: 'on' }, + { value: 'ignoring', label: 'ignoring' }, + ]} + onChange={(val) => { + onChange(index, { + ...nestedQuery, + vectorMatchesType: val.value, + }); + }} + /> + { + onChange(index, { + ...nestedQuery, + vectorMatches: evt.currentTarget.value, + vectorMatchesType: nestedQuery.vectorMatchesType || 'on', + }); + }} + /> +
onRemove(index)} /> @@ -76,21 +93,33 @@ NestedQuery.displayName = 'NestedQuery'; const getStyles = (theme: GrafanaTheme2) => { return { card: css({ + label: 'card', display: 'flex', flexDirection: 'column', gap: theme.spacing(0.5), }), header: css({ + label: 'header', padding: theme.spacing(0.5, 0.5, 0.5, 1), gap: theme.spacing(1), display: 'flex', alignItems: 'center', }), name: css({ + label: 'name', whiteSpace: 'nowrap', }), body: css({ + label: 'body', paddingLeft: theme.spacing(2), }), + vectorMatchInput: css({ + label: 'vectorMatchInput', + marginLeft: -1, + }), + vectorMatchWrapper: css({ + label: 'vectorMatchWrapper', + display: 'flex', + }), }; }; diff --git a/public/app/plugins/datasource/prometheus/querybuilder/parsing.test.ts b/public/app/plugins/datasource/prometheus/querybuilder/parsing.test.ts index b4dc7919841..89643c4b3a6 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/parsing.test.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/parsing.test.ts @@ -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) { diff --git a/public/app/plugins/datasource/prometheus/querybuilder/parsing.ts b/public/app/plugins/datasource/prometheus/querybuilder/parsing.ts index c6cb0488b42..eb71fb5688b 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/parsing.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/parsing.ts @@ -1,7 +1,7 @@ import { parser } from 'lezer-promql'; import { SyntaxNode, TreeCursor } from '@lezer/common'; import { QueryBuilderLabelFilter, QueryBuilderOperation } from './shared/types'; -import { PromVisualQuery } from './types'; +import { PromVisualQuery, PromVisualQueryBinary } from './types'; import { binaryScalarDefs } from './binaryScalarOperations'; // 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 left = node.firstChild!; 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 opDef = operatorToOpName[op]; @@ -362,13 +363,13 @@ function handleBinary(expr: string, node: SyntaxNode, context: Context) { } if (rightNumber) { - visQuery.operations.push(makeBinOp(opDef, expr, right, binModifier)); + 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)); + 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 @@ -376,7 +377,7 @@ function handleBinary(expr: string, node: SyntaxNode, context: Context) { handleExpression(expr, right, context); } else { visQuery.binaryQueries = visQuery.binaryQueries || []; - const binQuery = { + const binQuery: PromVisualQueryBinary = { operator: op, query: { metric: '', @@ -384,6 +385,10 @@ function handleBinary(expr: string, node: SyntaxNode, context: Context) { operations: [], }, }; + if (binModifier?.isMatcher) { + binQuery.vectorMatchesType = binModifier.matchType; + binQuery.vectorMatches = binModifier.matches; + } visQuery.binaryQueries.push(binQuery); handleExpression(expr, right, { 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( opDef: { id: string; comparison?: boolean }, expr: string, numberNode: SyntaxNode, - binModifier?: SyntaxNode | null + hasBool: boolean ) { const params: any[] = [parseFloat(getString(expr, numberNode))]; if (opDef.comparison) { - params.unshift(Boolean(binModifier)); + params.unshift(hasBool); } return { id: opDef.id, @@ -414,7 +447,7 @@ function makeBinOp( * @param expr * @param node */ -function getString(expr: string, node: SyntaxNode | TreeCursor | null) { +function getString(expr: string, node: SyntaxNode | TreeCursor | null | undefined) { if (!node) { return ''; } diff --git a/public/app/plugins/datasource/prometheus/querybuilder/shared/LokiAndPromQueryModellerBase.ts b/public/app/plugins/datasource/prometheus/querybuilder/shared/LokiAndPromQueryModellerBase.ts index 1871b7a2a44..f2b0a94fa67 100644 --- a/public/app/plugins/datasource/prometheus/querybuilder/shared/LokiAndPromQueryModellerBase.ts +++ b/public/app/plugins/datasource/prometheus/querybuilder/shared/LokiAndPromQueryModellerBase.ts @@ -9,6 +9,7 @@ import { export interface VisualQueryBinary { operator: string; + vectorMatchesType?: 'on' | 'ignoring'; vectorMatches?: string; query: T; } @@ -66,7 +67,7 @@ export abstract class LokiAndPromQueryModellerBase