From e67218a39ecc5cde32a466e6afbfb44f1692441f Mon Sep 17 00:00:00 2001 From: Augustin Husson Date: Wed, 15 Oct 2025 15:46:32 +0200 Subject: [PATCH] feat(ui): Support anchored and smoothed keyword in promql editor (#17239) * feat(ui): Support anchored and smoothed keyword in promql editor Signed-off-by: Augustin Husson * change parser logic about smoothed/anchored expression Signed-off-by: Augustin Husson --------- Signed-off-by: Augustin Husson --- .../codemirror-promql/src/complete/hybrid.ts | 2 +- .../codemirror-promql/src/parser/parser.ts | 42 ++++++++++++++++++- .../codemirror-promql/src/parser/type.ts | 6 +++ web/ui/module/lezer-promql/src/highlight.js | 2 +- web/ui/module/lezer-promql/src/promql.grammar | 14 ++++++- web/ui/module/lezer-promql/src/tokens.js | 4 ++ .../module/lezer-promql/test/expression.txt | 14 +++++++ 7 files changed, 80 insertions(+), 4 deletions(-) diff --git a/web/ui/module/codemirror-promql/src/complete/hybrid.ts b/web/ui/module/codemirror-promql/src/complete/hybrid.ts index 207173fca5..b2d439d2fe 100644 --- a/web/ui/module/codemirror-promql/src/complete/hybrid.ts +++ b/web/ui/module/codemirror-promql/src/complete/hybrid.ts @@ -376,7 +376,7 @@ export function analyzeCompletion(state: EditorState, node: SyntaxNode, pos: num { kind: ContextKind.Aggregation } ); if (parent.type.id !== FunctionCallBody && parent.type.id !== MatrixSelector) { - // it's too avoid to autocomplete a number in situation where it shouldn't. + // it's to avoid to autocomplete a number in situation where it shouldn't. // Like with `sum by(rat)` result.push({ kind: ContextKind.Number }); } diff --git a/web/ui/module/codemirror-promql/src/parser/parser.ts b/web/ui/module/codemirror-promql/src/parser/parser.ts index 2156be2715..f6e6fa50b6 100644 --- a/web/ui/module/codemirror-promql/src/parser/parser.ts +++ b/web/ui/module/codemirror-promql/src/parser/parser.ts @@ -15,11 +15,14 @@ import { Diagnostic } from '@codemirror/lint'; import { SyntaxNode, Tree } from '@lezer/common'; import { AggregateExpr, + AnchoredExpr, And, BinaryExpr, BoolModifier, Bottomk, + Changes, CountValues, + Delta, Eql, EqlSingle, FunctionCall, @@ -27,6 +30,7 @@ import { Gte, Gtr, Identifier, + Increase, LabelMatchers, LimitK, LimitRatio, @@ -39,6 +43,9 @@ import { Quantile, QuotedLabelMatcher, QuotedLabelName, + Rate, + Resets, + SmoothedExpr, StepInvariantExpr, SubqueryExpr, Topk, @@ -52,7 +59,7 @@ import { getType } from './type'; import { buildLabelMatchers } from './matcher'; import { EditorState } from '@codemirror/state'; import { syntaxTree } from '@codemirror/language'; -import { getFunction, Matcher, VectorMatchCardinality, ValueType } from '../types'; +import { getFunction, Matcher, ValueType, VectorMatchCardinality } from '../types'; import { buildVectorMatching } from './vector'; export class Parser { @@ -123,6 +130,14 @@ export class Parser { } break; } + case SmoothedExpr: { + this.checkAnchoredSmoothedExpr(node, [Rate, Increase, Delta]); + break; + } + case AnchoredExpr: { + this.checkAnchoredSmoothedExpr(node, [Resets, Changes, Rate, Increase, Delta]); + break; + } case SubqueryExpr: { const subQueryExprType = this.checkAST(node.getChild('Expr')); if (subQueryExprType !== ValueType.vector) { @@ -300,6 +315,31 @@ export class Parser { } } + private checkAnchoredSmoothedExpr(node: SyntaxNode, allowedFunctions: number[]): void { + // A smoothed/anchored expression is supposed to work with range vectors or instant vectors. + // So first thing to do is to check the type of the child. + // Then, if this is used inside a function call, we need to check that the function is one of the given allowedFunctions. + const nodeType = getType(node); + if (nodeType !== ValueType.vector && nodeType !== ValueType.matrix) { + this.addDiagnostic(node, `smoothed/anchored expression only allowed on instant vector or range vector selector, got ${nodeType} instead`); + return; + } + const parent = node.parent?.parent; + if (!parent || parent.type.id !== FunctionCall) { + // Since the anchored/smoothed expression is not inside a function call, we cannot check the function name. + // This is an acceptable case as the anchored/smoothed expression can be used on any vector expression. + return; + } + const funcID = parent.firstChild?.firstChild; + if (!funcID) { + this.addDiagnostic(node, 'function not defined'); + return; + } + if (!allowedFunctions.includes(funcID.type.id)) { + this.addDiagnostic(node, 'smoothed/anchored expression can only be used in specific functions'); + } + } + private checkVectorSelector(node: SyntaxNode): void { const matchList = node.getChild(LabelMatchers); const labelMatcherOpts = [QuotedLabelName, QuotedLabelMatcher, UnquotedLabelMatcher]; diff --git a/web/ui/module/codemirror-promql/src/parser/type.ts b/web/ui/module/codemirror-promql/src/parser/type.ts index 0682e17ace..89f53df5b0 100644 --- a/web/ui/module/codemirror-promql/src/parser/type.ts +++ b/web/ui/module/codemirror-promql/src/parser/type.ts @@ -14,12 +14,14 @@ import { SyntaxNode } from '@lezer/common'; import { AggregateExpr, + AnchoredExpr, BinaryExpr, FunctionCall, MatrixSelector, NumberDurationLiteral, OffsetExpr, ParenExpr, + SmoothedExpr, StepInvariantExpr, StringLiteral, SubqueryExpr, @@ -48,6 +50,10 @@ export function getType(node: SyntaxNode | null): ValueType { return ValueType.matrix; case SubqueryExpr: return ValueType.matrix; + case SmoothedExpr: + return getType(node.firstChild); + case AnchoredExpr: + return getType(node.firstChild); case ParenExpr: return getType(node.getChild('Expr')); case UnaryExpr: diff --git a/web/ui/module/lezer-promql/src/highlight.js b/web/ui/module/lezer-promql/src/highlight.js index 364c4e39ab..9c1b5601a3 100644 --- a/web/ui/module/lezer-promql/src/highlight.js +++ b/web/ui/module/lezer-promql/src/highlight.js @@ -23,7 +23,7 @@ export const promQLHighLight = styleTags({ 'Abs Absent AbsentOverTime Acos Acosh Asin Asinh Atan Atanh AvgOverTime Ceil Changes Clamp ClampMax ClampMin Cos Cosh CountOverTime DaysInMonth DayOfMonth DayOfWeek DayOfYear Deg Delta Deriv Exp Floor HistogramAvg HistogramCount HistogramFraction HistogramQuantile HistogramSum DoubleExponentialSmoothing Hour Idelta Increase Irate LabelReplace LabelJoin LastOverTime Ln Log10 Log2 MaxOverTime MinOverTime Minute Month Pi PredictLinear PresentOverTime QuantileOverTime Rad Rate Resets Round Scalar Sgn Sin Sinh Sort SortDesc SortByLabel SortByLabelDesc Sqrt StddevOverTime StdvarOverTime SumOverTime Tan Tanh Time Timestamp Vector Year': tags.function(tags.variableName), 'Avg Bottomk Count Count_values Group LimitK LimitRatio Max Min Quantile Stddev Stdvar Sum Topk': tags.operatorKeyword, - 'By Without Bool On Ignoring GroupLeft GroupRight Offset Start End': tags.modifier, + 'By Without Bool On Ignoring GroupLeft GroupRight Offset Start End Smoothed Anchored': tags.modifier, 'And Unless Or': tags.logicOperator, 'Sub Add Mul Mod Div Atan2 Eql Neq Lte Lss Gte Gtr EqlRegex EqlSingle NeqRegex Pow At': tags.operator, UnaryOp: tags.arithmeticOperator, diff --git a/web/ui/module/lezer-promql/src/promql.grammar b/web/ui/module/lezer-promql/src/promql.grammar index b919ef2683..5fe8d4d025 100644 --- a/web/ui/module/lezer-promql/src/promql.grammar +++ b/web/ui/module/lezer-promql/src/promql.grammar @@ -31,6 +31,8 @@ expr[@isGroup=Expr] { MatrixSelector | NumberDurationLiteral | OffsetExpr | + AnchoredExpr | + SmoothedExpr | ParenExpr | StringLiteral | SubqueryExpr | @@ -39,6 +41,14 @@ expr[@isGroup=Expr] { StepInvariantExpr } +AnchoredExpr { + expr Anchored +} + +SmoothedExpr { + expr Smoothed +} + AggregateExpr { AggregateOp AggregateModifier FunctionCallBody | AggregateOp FunctionCallBody AggregateModifier | @@ -354,7 +364,9 @@ NumberDurationLiteralInDurationContext { Or, Unless, Start, - End + End, + Smoothed, + Anchored } @external propSource promQLHighLight from "./highlight" diff --git a/web/ui/module/lezer-promql/src/tokens.js b/web/ui/module/lezer-promql/src/tokens.js index d9e7b1d9b4..1695ae1d87 100644 --- a/web/ui/module/lezer-promql/src/tokens.js +++ b/web/ui/module/lezer-promql/src/tokens.js @@ -42,6 +42,8 @@ import { Topk, Unless, Without, + Smoothed, + Anchored, } from './parser.terms.js'; const keywordTokens = { @@ -82,6 +84,8 @@ const contextualKeywordTokens = { unless: Unless, start: Start, end: End, + smoothed: Smoothed, + anchored: Anchored, }; export const extendIdentifier = (value, stack) => { diff --git a/web/ui/module/lezer-promql/test/expression.txt b/web/ui/module/lezer-promql/test/expression.txt index 04b4aab68c..109eb7af15 100644 --- a/web/ui/module/lezer-promql/test/expression.txt +++ b/web/ui/module/lezer-promql/test/expression.txt @@ -702,3 +702,17 @@ PromQL(VectorSelector(LabelMatchers(QuotedLabelMatcher(QuotedLabelName(StringLit ==> PromQL(VectorSelector(LabelMatchers(QuotedLabelName(StringLiteral), QuotedLabelMatcher(QuotedLabelName(StringLiteral), MatchOp(EqlSingle), StringLiteral)))) + +# Testing anchored keyword + +increase(caddy_http_requests_total[5m] anchored) + +==> +PromQL(FunctionCall(FunctionIdentifier(Increase),FunctionCallBody(AnchoredExpr(MatrixSelector(VectorSelector(Identifier),NumberDurationLiteralInDurationContext),Anchored)))) + +# Testing smoothed keyword + +rate(caddy_http_requests_total[5m] smoothed) + +==> +PromQL(FunctionCall(FunctionIdentifier(Rate),FunctionCallBody(SmoothedExpr(MatrixSelector(VectorSelector(Identifier),NumberDurationLiteralInDurationContext),Smoothed))))