mirror of
https://github.com/prometheus/prometheus.git
synced 2026-02-03 20:39:32 -05:00
PromQL: Add fill*() binop modifiers to provide default values for missing series
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
ccb7468b09
commit
af3277f832
22 changed files with 1296 additions and 702 deletions
|
|
@ -2862,7 +2862,8 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
|
|||
if matching.Card == parser.CardManyToMany {
|
||||
panic("many-to-many only allowed for set operators")
|
||||
}
|
||||
if len(lhs) == 0 || len(rhs) == 0 {
|
||||
if (len(lhs) == 0 && len(rhs) == 0) ||
|
||||
((len(lhs) == 0 || len(rhs) == 0) && matching.FillValues.RHS == nil && matching.FillValues.LHS == nil) {
|
||||
return nil, nil // Short-circuit: nothing is going to match.
|
||||
}
|
||||
|
||||
|
|
@ -2910,17 +2911,9 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
|
|||
}
|
||||
matchedSigs := enh.matchedSigs
|
||||
|
||||
// For all lhs samples find a respective rhs sample and perform
|
||||
// the binary operation.
|
||||
var lastErr error
|
||||
for i, ls := range lhs {
|
||||
sigOrd := lhsh[i].sigOrdinal
|
||||
|
||||
rs, found := rightSigs[sigOrd] // Look for a match in the rhs Vector.
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
|
||||
doBinOp := func(ls, rs Sample, sigOrd int) {
|
||||
// Account for potentially swapped sidedness.
|
||||
fl, fr := ls.F, rs.F
|
||||
hl, hr := ls.H, rs.H
|
||||
|
|
@ -2931,7 +2924,7 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
|
|||
floatValue, histogramValue, keep, info, err := vectorElemBinop(op, fl, fr, hl, hr, pos)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
return
|
||||
}
|
||||
if info != nil {
|
||||
lastErr = info
|
||||
|
|
@ -2971,7 +2964,7 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
|
|||
}
|
||||
|
||||
if !keep && !returnBool {
|
||||
continue
|
||||
return
|
||||
}
|
||||
|
||||
enh.Out = append(enh.Out, Sample{
|
||||
|
|
@ -2981,6 +2974,43 @@ func (ev *evaluator) VectorBinop(op parser.ItemType, lhs, rhs Vector, matching *
|
|||
DropName: returnBool,
|
||||
})
|
||||
}
|
||||
|
||||
// For all lhs samples, find a respective rhs sample and perform
|
||||
// the binary operation.
|
||||
for i, ls := range lhs {
|
||||
sigOrd := lhsh[i].sigOrdinal
|
||||
|
||||
rs, found := rightSigs[sigOrd] // Look for a match in the rhs Vector.
|
||||
if !found {
|
||||
fill := matching.FillValues.RHS
|
||||
if fill == nil {
|
||||
continue
|
||||
}
|
||||
rs = Sample{
|
||||
Metric: ls.Metric.MatchLabels(matching.On, matching.MatchingLabels...),
|
||||
F: *fill,
|
||||
}
|
||||
}
|
||||
|
||||
doBinOp(ls, rs, sigOrd)
|
||||
}
|
||||
|
||||
// For any rhs samples which have not been matched, check if we need to
|
||||
// perform the operation with a fill value from the lhs.
|
||||
if fill := matching.FillValues.LHS; fill != nil {
|
||||
for sigOrd, rs := range rightSigs {
|
||||
if _, matched := matchedSigs[sigOrd]; matched {
|
||||
continue // Already matched.
|
||||
}
|
||||
ls := Sample{
|
||||
Metric: rs.Metric.MatchLabels(matching.On, matching.MatchingLabels...),
|
||||
F: *fill,
|
||||
}
|
||||
|
||||
doBinOp(ls, rs, sigOrd)
|
||||
}
|
||||
}
|
||||
|
||||
return enh.Out, lastErr
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -318,6 +318,19 @@ type VectorMatching struct {
|
|||
// Include contains additional labels that should be included in
|
||||
// the result from the side with the lower cardinality.
|
||||
Include []string
|
||||
// Fill-in values to use when a series from one side does not find a match on the other side.
|
||||
FillValues VectorMatchFillValues
|
||||
}
|
||||
|
||||
// VectorMatchFillValues contains the fill values to use for Vector matching
|
||||
// when one side does not find a match on the other side.
|
||||
// When a fill value is nil, no fill is applied for that side, and there
|
||||
// is no output for the match group if there is no match.
|
||||
type VectorMatchFillValues struct {
|
||||
// RHS is the fill value to use for the right-hand side.
|
||||
RHS *float64
|
||||
// LHS is the fill value to use for the left-hand side.
|
||||
LHS *float64
|
||||
}
|
||||
|
||||
// Visitor allows visiting a Node and its child nodes. The Visit method is
|
||||
|
|
|
|||
|
|
@ -139,6 +139,9 @@ BOOL
|
|||
BY
|
||||
GROUP_LEFT
|
||||
GROUP_RIGHT
|
||||
FILL
|
||||
FILL_LEFT
|
||||
FILL_RIGHT
|
||||
IGNORING
|
||||
OFFSET
|
||||
SMOOTHED
|
||||
|
|
@ -190,7 +193,7 @@ START_METRIC_SELECTOR
|
|||
%type <int> int
|
||||
%type <uint> uint
|
||||
%type <float> number series_value signed_number signed_or_unsigned_number
|
||||
%type <node> step_invariant_expr aggregate_expr aggregate_modifier bin_modifier binary_expr bool_modifier expr function_call function_call_args function_call_body group_modifiers label_matchers matrix_selector number_duration_literal offset_expr anchored_expr smoothed_expr on_or_ignoring paren_expr string_literal subquery_expr unary_expr vector_selector duration_expr paren_duration_expr positive_duration_expr offset_duration_expr
|
||||
%type <node> step_invariant_expr aggregate_expr aggregate_modifier bin_modifier fill_modifiers binary_expr bool_modifier expr function_call function_call_args function_call_body group_modifiers fill_value label_matchers matrix_selector number_duration_literal offset_expr anchored_expr smoothed_expr on_or_ignoring paren_expr string_literal subquery_expr unary_expr vector_selector duration_expr paren_duration_expr positive_duration_expr offset_duration_expr
|
||||
|
||||
%start start
|
||||
|
||||
|
|
@ -302,7 +305,7 @@ binary_expr : expr ADD bin_modifier expr { $$ = yylex.(*parser).newBinar
|
|||
|
||||
// Using left recursion for the modifier rules, helps to keep the parser stack small and
|
||||
// reduces allocations.
|
||||
bin_modifier : group_modifiers;
|
||||
bin_modifier : fill_modifiers;
|
||||
|
||||
bool_modifier : /* empty */
|
||||
{ $$ = &BinaryExpr{
|
||||
|
|
@ -346,6 +349,47 @@ group_modifiers: bool_modifier /* empty */
|
|||
}
|
||||
;
|
||||
|
||||
fill_modifiers: group_modifiers /* empty */
|
||||
/* Only fill() */
|
||||
| group_modifiers FILL fill_value
|
||||
{
|
||||
$$ = $1
|
||||
fill := $3.(*NumberLiteral).Val
|
||||
$$.(*BinaryExpr).VectorMatching.FillValues.LHS = &fill
|
||||
$$.(*BinaryExpr).VectorMatching.FillValues.RHS = &fill
|
||||
}
|
||||
/* Only fill_left() */
|
||||
| group_modifiers FILL_LEFT fill_value
|
||||
{
|
||||
$$ = $1
|
||||
fill := $3.(*NumberLiteral).Val
|
||||
$$.(*BinaryExpr).VectorMatching.FillValues.LHS = &fill
|
||||
}
|
||||
/* Only fill_right() */
|
||||
| group_modifiers FILL_RIGHT fill_value
|
||||
{
|
||||
$$ = $1
|
||||
fill := $3.(*NumberLiteral).Val
|
||||
$$.(*BinaryExpr).VectorMatching.FillValues.RHS = &fill
|
||||
}
|
||||
/* fill_left() fill_right() */
|
||||
| group_modifiers FILL_LEFT fill_value FILL_RIGHT fill_value
|
||||
{
|
||||
$$ = $1
|
||||
fill_left := $3.(*NumberLiteral).Val
|
||||
fill_right := $5.(*NumberLiteral).Val
|
||||
$$.(*BinaryExpr).VectorMatching.FillValues.LHS = &fill_left
|
||||
$$.(*BinaryExpr).VectorMatching.FillValues.RHS = &fill_right
|
||||
}
|
||||
/* fill_right() fill_left() */
|
||||
| group_modifiers FILL_RIGHT fill_value FILL_LEFT fill_value
|
||||
{
|
||||
fill_right := $3.(*NumberLiteral).Val
|
||||
fill_left := $5.(*NumberLiteral).Val
|
||||
$$.(*BinaryExpr).VectorMatching.FillValues.LHS = &fill_left
|
||||
$$.(*BinaryExpr).VectorMatching.FillValues.RHS = &fill_right
|
||||
}
|
||||
;
|
||||
|
||||
grouping_labels : LEFT_PAREN grouping_label_list RIGHT_PAREN
|
||||
{ $$ = $2 }
|
||||
|
|
@ -387,6 +431,21 @@ grouping_label : maybe_label
|
|||
{ yylex.(*parser).unexpected("grouping opts", "label"); $$ = Item{} }
|
||||
;
|
||||
|
||||
fill_value : LEFT_PAREN number_duration_literal RIGHT_PAREN
|
||||
{
|
||||
$$ = $2.(*NumberLiteral)
|
||||
}
|
||||
| LEFT_PAREN unary_op number_duration_literal RIGHT_PAREN
|
||||
{
|
||||
nl := $3.(*NumberLiteral)
|
||||
if $2.Typ == SUB {
|
||||
nl.Val *= -1
|
||||
}
|
||||
nl.PosRange.Start = $2.Pos
|
||||
$$ = nl
|
||||
}
|
||||
;
|
||||
|
||||
/*
|
||||
* Function calls.
|
||||
*/
|
||||
|
|
@ -697,7 +756,7 @@ metric : metric_identifier label_set
|
|||
;
|
||||
|
||||
|
||||
metric_identifier: AVG | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | IDENTIFIER | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | QUANTILE | STDDEV | STDVAR | SUM | TOPK | WITHOUT | START | END | LIMITK | LIMIT_RATIO | STEP | RANGE | ANCHORED | SMOOTHED;
|
||||
metric_identifier: AVG | BOTTOMK | BY | COUNT | COUNT_VALUES | FILL | FILL_LEFT | FILL_RIGHT | GROUP | IDENTIFIER | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | QUANTILE | STDDEV | STDVAR | SUM | TOPK | WITHOUT | START | END | LIMITK | LIMIT_RATIO | STEP | RANGE | ANCHORED | SMOOTHED;
|
||||
|
||||
label_set : LEFT_BRACE label_set_list RIGHT_BRACE
|
||||
{ $$ = labels.New($2...) }
|
||||
|
|
@ -954,7 +1013,7 @@ counter_reset_hint : UNKNOWN_COUNTER_RESET | COUNTER_RESET | NOT_COUNTER_RESET |
|
|||
aggregate_op : AVG | BOTTOMK | COUNT | COUNT_VALUES | GROUP | MAX | MIN | QUANTILE | STDDEV | STDVAR | SUM | TOPK | LIMITK | LIMIT_RATIO;
|
||||
|
||||
// Inside of grouping options label names can be recognized as keywords by the lexer. This is a list of keywords that could also be a label name.
|
||||
maybe_label : AVG | BOOL | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | GROUP_LEFT | GROUP_RIGHT | IDENTIFIER | IGNORING | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | ON | QUANTILE | STDDEV | STDVAR | SUM | TOPK | START | END | ATAN2 | LIMITK | LIMIT_RATIO | STEP | RANGE | ANCHORED | SMOOTHED;
|
||||
maybe_label : AVG | BOOL | BOTTOMK | BY | COUNT | COUNT_VALUES | GROUP | GROUP_LEFT | GROUP_RIGHT | FILL | FILL_LEFT | FILL_RIGHT | IDENTIFIER | IGNORING | LAND | LOR | LUNLESS | MAX | METRIC_IDENTIFIER | MIN | OFFSET | ON | QUANTILE | STDDEV | STDVAR | SUM | TOPK | START | END | ATAN2 | LIMITK | LIMIT_RATIO | STEP | RANGE | ANCHORED | SMOOTHED;
|
||||
|
||||
unary_op : ADD | SUB;
|
||||
|
||||
|
|
@ -1162,7 +1221,7 @@ offset_duration_expr : number_duration_literal
|
|||
}
|
||||
| duration_expr
|
||||
;
|
||||
|
||||
|
||||
min_max: MIN | MAX ;
|
||||
|
||||
duration_expr : number_duration_literal
|
||||
|
|
@ -1277,14 +1336,14 @@ duration_expr : number_duration_literal
|
|||
;
|
||||
|
||||
paren_duration_expr : LEFT_PAREN duration_expr RIGHT_PAREN
|
||||
{
|
||||
{
|
||||
yylex.(*parser).experimentalDurationExpr($2.(Expr))
|
||||
if durationExpr, ok := $2.(*DurationExpr); ok {
|
||||
durationExpr.Wrapped = true
|
||||
$$ = durationExpr
|
||||
break
|
||||
}
|
||||
$$ = $2
|
||||
$$ = $2
|
||||
}
|
||||
;
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -137,6 +137,9 @@ var key = map[string]ItemType{
|
|||
"ignoring": IGNORING,
|
||||
"group_left": GROUP_LEFT,
|
||||
"group_right": GROUP_RIGHT,
|
||||
"fill": FILL,
|
||||
"fill_left": FILL_LEFT,
|
||||
"fill_right": FILL_RIGHT,
|
||||
"bool": BOOL,
|
||||
|
||||
// Preprocessors.
|
||||
|
|
@ -1083,6 +1086,17 @@ Loop:
|
|||
word := l.input[l.start:l.pos]
|
||||
switch kw, ok := key[strings.ToLower(word)]; {
|
||||
case ok:
|
||||
// For fill/fill_left/fill_right, only treat as keyword if followed by '('
|
||||
// This allows using these as metric names (e.g., "fill + fill").
|
||||
// This could be done for other keywords as well, but for the new fill
|
||||
// modifiers this is especially important so we don't break any existing
|
||||
// queries.
|
||||
if kw == FILL || kw == FILL_LEFT || kw == FILL_RIGHT {
|
||||
if !l.peekFollowedByLeftParen() {
|
||||
l.emit(IDENTIFIER)
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
l.emit(kw)
|
||||
case !strings.Contains(word, ":"):
|
||||
l.emit(IDENTIFIER)
|
||||
|
|
@ -1098,6 +1112,23 @@ Loop:
|
|||
return lexStatements
|
||||
}
|
||||
|
||||
// peekFollowedByLeftParen checks if the next non-whitespace character is '('.
|
||||
// This is used for context-sensitive keywords like fill/fill_left/fill_right
|
||||
// that should only be treated as keywords when followed by '('.
|
||||
func (l *Lexer) peekFollowedByLeftParen() bool {
|
||||
pos := l.pos
|
||||
for {
|
||||
if int(pos) >= len(l.input) {
|
||||
return false
|
||||
}
|
||||
r, w := utf8.DecodeRuneInString(l.input[pos:])
|
||||
if !isSpace(r) {
|
||||
return r == '('
|
||||
}
|
||||
pos += posrange.Pos(w)
|
||||
}
|
||||
}
|
||||
|
||||
func isSpace(r rune) bool {
|
||||
return r == ' ' || r == '\t' || r == '\n' || r == '\r'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -768,6 +768,9 @@ func (p *parser) checkAST(node Node) (typ ValueType) {
|
|||
if len(n.VectorMatching.MatchingLabels) > 0 {
|
||||
p.addParseErrf(n.PositionRange(), "vector matching only allowed between instant vectors")
|
||||
}
|
||||
if n.VectorMatching.FillValues.LHS != nil || n.VectorMatching.FillValues.RHS != nil {
|
||||
p.addParseErrf(n.PositionRange(), "filling in missing series only allowed between instant vectors")
|
||||
}
|
||||
n.VectorMatching = nil
|
||||
case n.Op.IsSetOperator(): // Both operands are Vectors.
|
||||
if n.VectorMatching.Card == CardOneToMany || n.VectorMatching.Card == CardManyToOne {
|
||||
|
|
@ -776,6 +779,9 @@ func (p *parser) checkAST(node Node) (typ ValueType) {
|
|||
if n.VectorMatching.Card != CardManyToMany {
|
||||
p.addParseErrf(n.PositionRange(), "set operations must always be many-to-many")
|
||||
}
|
||||
if n.VectorMatching.FillValues.LHS != nil || n.VectorMatching.FillValues.RHS != nil {
|
||||
p.addParseErrf(n.PositionRange(), "filling in missing series not allowed for set operators")
|
||||
}
|
||||
}
|
||||
|
||||
if (lt == ValueTypeScalar || rt == ValueTypeScalar) && n.Op.IsSetOperator() {
|
||||
|
|
|
|||
|
|
@ -172,6 +172,19 @@ func (node *BinaryExpr) getMatchingStr() string {
|
|||
b.WriteString(")")
|
||||
matching += b.String()
|
||||
}
|
||||
|
||||
if vm.FillValues.LHS != nil || vm.FillValues.RHS != nil {
|
||||
if vm.FillValues.LHS == vm.FillValues.RHS {
|
||||
matching += fmt.Sprintf(" fill (%v)", *vm.FillValues.LHS)
|
||||
} else {
|
||||
if vm.FillValues.LHS != nil {
|
||||
matching += fmt.Sprintf(" fill_left (%v)", *vm.FillValues.LHS)
|
||||
}
|
||||
if vm.FillValues.RHS != nil {
|
||||
matching += fmt.Sprintf(" fill_right (%v)", *vm.FillValues.RHS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return matching
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,26 @@ func TestExprString(t *testing.T) {
|
|||
in: `a - ignoring() group_left c`,
|
||||
out: `a - ignoring () group_left () c`,
|
||||
},
|
||||
{
|
||||
in: `a + fill(-23) b`,
|
||||
out: `a + fill (-23) b`,
|
||||
},
|
||||
{
|
||||
in: `a + fill_left(-23) b`,
|
||||
out: `a + fill_left (-23) b`,
|
||||
},
|
||||
{
|
||||
in: `a + fill_right(42) b`,
|
||||
out: `a + fill_right (42) b`,
|
||||
},
|
||||
{
|
||||
in: `a + fill_left(-23) fill_right(42) b`,
|
||||
out: `a + fill_left (-23) fill_right (42) b`,
|
||||
},
|
||||
{
|
||||
in: `a + on(b) group_left fill(-23) c`,
|
||||
out: `a + on (b) group_left () fill (-23) c`,
|
||||
},
|
||||
{
|
||||
in: `up > bool 0`,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ func translateAST(node parser.Expr) any {
|
|||
"labels": sanitizeList(m.MatchingLabels),
|
||||
"on": m.On,
|
||||
"include": sanitizeList(m.Include),
|
||||
"fillValues": map[string]*float64{
|
||||
"lhs": m.FillValues.LHS,
|
||||
"rhs": m.FillValues.RHS,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
MatchErrorType,
|
||||
computeVectorVectorBinOp,
|
||||
filteredSampleValue,
|
||||
MaybeFilledInstantSample,
|
||||
} from "../../../../promql/binOp";
|
||||
import { formatNode, labelNameList } from "../../../../promql/format";
|
||||
import {
|
||||
|
|
@ -177,11 +178,10 @@ const explanationText = (node: BinaryExpr): React.ReactNode => {
|
|||
</List.Item>
|
||||
) : (
|
||||
<List.Item>
|
||||
<span className="promql-code promql-keyword">
|
||||
group_{manySide}({labelNameList(matching.include)})
|
||||
</span>
|
||||
: {matching.card} match. Each series from the {oneSide}-hand side is
|
||||
allowed to match with multiple series from the {manySide}-hand side.
|
||||
<span className="promql-code promql-keyword">group_{manySide}</span>
|
||||
({labelNameList(matching.include)}) : {matching.card} match. Each
|
||||
series from the {oneSide}-hand side is allowed to match with
|
||||
multiple series from the {manySide}-hand side.
|
||||
{matching.include.length !== 0 && (
|
||||
<>
|
||||
{" "}
|
||||
|
|
@ -192,6 +192,55 @@ const explanationText = (node: BinaryExpr): React.ReactNode => {
|
|||
)}
|
||||
</List.Item>
|
||||
)}
|
||||
{(matching.fillValues.lhs !== null ||
|
||||
matching.fillValues.rhs !== null) &&
|
||||
(matching.fillValues.lhs === matching.fillValues.rhs ? (
|
||||
<List.Item>
|
||||
<span className="promql-code promql-keyword">fill</span>(
|
||||
<span className="promql-code promql-number">
|
||||
{matching.fillValues.lhs}
|
||||
</span>
|
||||
) : For series on either side missing a match, fill in the sample
|
||||
value{" "}
|
||||
<span className="promql-code promql-number">
|
||||
{matching.fillValues.lhs}
|
||||
</span>
|
||||
.
|
||||
</List.Item>
|
||||
) : (
|
||||
<>
|
||||
{matching.fillValues.lhs !== null && (
|
||||
<List.Item>
|
||||
<span className="promql-code promql-keyword">fill_left</span>(
|
||||
<span className="promql-code promql-number">
|
||||
{matching.fillValues.lhs}
|
||||
</span>
|
||||
) : For series on the left-hand side missing a match, fill in
|
||||
the sample value{" "}
|
||||
<span className="promql-code promql-number">
|
||||
{matching.fillValues.lhs}
|
||||
</span>
|
||||
.
|
||||
</List.Item>
|
||||
)}
|
||||
|
||||
{matching.fillValues.rhs !== null && (
|
||||
<List.Item>
|
||||
<span className="promql-code promql-keyword">fill_right</span>
|
||||
(
|
||||
<span className="promql-code promql-number">
|
||||
{matching.fillValues.rhs}
|
||||
</span>
|
||||
) : For series on the right-hand side missing a match, fill in
|
||||
the sample value{" "}
|
||||
<span className="promql-code promql-number">
|
||||
{matching.fillValues.rhs}
|
||||
</span>
|
||||
.
|
||||
</List.Item>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
{node.bool && (
|
||||
<List.Item>
|
||||
<span className="promql-code promql-keyword">bool</span>: Instead of
|
||||
|
|
@ -239,7 +288,12 @@ const explainError = (
|
|||
matching: {
|
||||
...(binOp.matching
|
||||
? binOp.matching
|
||||
: { labels: [], on: false, include: [] }),
|
||||
: {
|
||||
labels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
}),
|
||||
card:
|
||||
err.dupeSide === "left"
|
||||
? vectorMatchCardinality.manyToOne
|
||||
|
|
@ -403,7 +457,7 @@ const VectorVectorBinaryExprExplainView: FC<
|
|||
);
|
||||
|
||||
const matchGroupTable = (
|
||||
series: InstantSample[],
|
||||
series: MaybeFilledInstantSample[],
|
||||
seriesCount: number,
|
||||
color: string,
|
||||
colorOffset?: number
|
||||
|
|
@ -458,6 +512,11 @@ const VectorVectorBinaryExprExplainView: FC<
|
|||
)}
|
||||
format={true}
|
||||
/>
|
||||
{s.filled && (
|
||||
<Text size="sm" c="dimmed">
|
||||
no match, filling in default value
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
</Table.Td>
|
||||
{showSampleValues && (
|
||||
|
|
|
|||
|
|
@ -104,11 +104,16 @@ export interface LabelMatcher {
|
|||
value: string;
|
||||
}
|
||||
|
||||
export interface FillValues {
|
||||
lhs: number | null;
|
||||
rhs: number | null;
|
||||
}
|
||||
export interface VectorMatching {
|
||||
card: vectorMatchCardinality;
|
||||
labels: string[];
|
||||
on: boolean;
|
||||
include: string[];
|
||||
fillValues: FillValues;
|
||||
}
|
||||
|
||||
export type StartOrEnd = "start" | "end" | null;
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ const testCases: TestCase[] = [
|
|||
on: false,
|
||||
include: [],
|
||||
labels: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA,
|
||||
rhs: testMetricB,
|
||||
|
|
@ -247,6 +248,7 @@ const testCases: TestCase[] = [
|
|||
on: true,
|
||||
include: [],
|
||||
labels: ["label1", "label2"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA,
|
||||
rhs: testMetricB,
|
||||
|
|
@ -413,6 +415,7 @@ const testCases: TestCase[] = [
|
|||
on: false,
|
||||
include: [],
|
||||
labels: ["same"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA,
|
||||
rhs: testMetricB,
|
||||
|
|
@ -579,6 +582,7 @@ const testCases: TestCase[] = [
|
|||
on: false,
|
||||
include: [],
|
||||
labels: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricB,
|
||||
rhs: testMetricC,
|
||||
|
|
@ -701,6 +705,7 @@ const testCases: TestCase[] = [
|
|||
on: true,
|
||||
include: [],
|
||||
labels: ["label1"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricB,
|
||||
rhs: testMetricC,
|
||||
|
|
@ -791,6 +796,7 @@ const testCases: TestCase[] = [
|
|||
on: true,
|
||||
include: [],
|
||||
labels: ["label1"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricB,
|
||||
rhs: testMetricC,
|
||||
|
|
@ -905,6 +911,7 @@ const testCases: TestCase[] = [
|
|||
on: true,
|
||||
include: [],
|
||||
labels: ["label1"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricC,
|
||||
rhs: testMetricB,
|
||||
|
|
@ -1019,6 +1026,7 @@ const testCases: TestCase[] = [
|
|||
on: true,
|
||||
include: [],
|
||||
labels: ["label1"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricC,
|
||||
rhs: testMetricB,
|
||||
|
|
@ -1107,6 +1115,7 @@ const testCases: TestCase[] = [
|
|||
on: true,
|
||||
include: [],
|
||||
labels: ["label1"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA,
|
||||
rhs: testMetricB,
|
||||
|
|
@ -1223,6 +1232,7 @@ const testCases: TestCase[] = [
|
|||
on: false,
|
||||
include: [],
|
||||
labels: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA,
|
||||
rhs: testMetricB,
|
||||
|
|
@ -1409,6 +1419,7 @@ const testCases: TestCase[] = [
|
|||
on: false,
|
||||
include: [],
|
||||
labels: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA,
|
||||
rhs: testMetricB,
|
||||
|
|
@ -1596,6 +1607,7 @@ const testCases: TestCase[] = [
|
|||
on: false,
|
||||
include: [],
|
||||
labels: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA,
|
||||
rhs: testMetricB,
|
||||
|
|
@ -1763,6 +1775,7 @@ const testCases: TestCase[] = [
|
|||
on: false,
|
||||
include: [],
|
||||
labels: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA,
|
||||
rhs: testMetricB,
|
||||
|
|
@ -1929,6 +1942,7 @@ const testCases: TestCase[] = [
|
|||
on: false,
|
||||
include: [],
|
||||
labels: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA,
|
||||
rhs: testMetricB,
|
||||
|
|
@ -2022,6 +2036,7 @@ const testCases: TestCase[] = [
|
|||
on: true,
|
||||
include: [],
|
||||
labels: ["label1"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricB,
|
||||
rhs: testMetricC,
|
||||
|
|
@ -2105,6 +2120,7 @@ const testCases: TestCase[] = [
|
|||
on: true,
|
||||
include: [],
|
||||
labels: ["label1"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricB,
|
||||
rhs: testMetricC,
|
||||
|
|
@ -2156,6 +2172,7 @@ const testCases: TestCase[] = [
|
|||
on: false,
|
||||
include: [],
|
||||
labels: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA,
|
||||
rhs: testMetricB,
|
||||
|
|
@ -2342,6 +2359,7 @@ const testCases: TestCase[] = [
|
|||
on: true,
|
||||
include: [],
|
||||
labels: ["label1"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA.slice(0, 3),
|
||||
rhs: testMetricB.slice(1, 4),
|
||||
|
|
@ -2474,6 +2492,7 @@ const testCases: TestCase[] = [
|
|||
on: true,
|
||||
include: [],
|
||||
labels: ["label1"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA.slice(0, 3),
|
||||
rhs: testMetricB.slice(1, 4),
|
||||
|
|
@ -2568,6 +2587,7 @@ const testCases: TestCase[] = [
|
|||
on: true,
|
||||
include: [],
|
||||
labels: ["label1"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA.slice(0, 3),
|
||||
rhs: testMetricB.slice(1, 4),
|
||||
|
|
@ -2700,6 +2720,7 @@ const testCases: TestCase[] = [
|
|||
on: false,
|
||||
include: [],
|
||||
labels: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
lhs: testMetricA.slice(0, 3),
|
||||
rhs: testMetricB.slice(1, 4),
|
||||
|
|
@ -2886,6 +2907,7 @@ describe("binOp", () => {
|
|||
on: true,
|
||||
labels: ["label1"],
|
||||
include: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
};
|
||||
|
||||
const result = resultMetric(lhs, rhs, op, matching);
|
||||
|
|
@ -2911,6 +2933,7 @@ describe("binOp", () => {
|
|||
on: true,
|
||||
labels: ["label1"],
|
||||
include: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
};
|
||||
|
||||
const result = resultMetric(lhs, rhs, op, matching);
|
||||
|
|
@ -2931,6 +2954,7 @@ describe("binOp", () => {
|
|||
on: true,
|
||||
labels: ["label1"],
|
||||
include: ["label2"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
};
|
||||
|
||||
const result = resultMetric(lhs, rhs, op, matching);
|
||||
|
|
|
|||
|
|
@ -45,13 +45,18 @@ export type VectorMatchError =
|
|||
| MultipleMatchesOnBothSidesError
|
||||
| MultipleMatchesOnOneSideError;
|
||||
|
||||
export type MaybeFilledInstantSample = InstantSample & {
|
||||
// If the sample was filled in via a fill(...) modifier, this is true.
|
||||
filled?: boolean;
|
||||
};
|
||||
|
||||
// A single match group as produced by a vector-to-vector binary operation, with all of its
|
||||
// left-hand side and right-hand side series, as well as a result and error, if applicable.
|
||||
export type BinOpMatchGroup = {
|
||||
groupLabels: Metric;
|
||||
rhs: InstantSample[];
|
||||
rhs: MaybeFilledInstantSample[];
|
||||
rhsCount: number; // Number of samples before applying limits.
|
||||
lhs: InstantSample[];
|
||||
lhs: MaybeFilledInstantSample[];
|
||||
lhsCount: number; // Number of samples before applying limits.
|
||||
result: {
|
||||
sample: InstantSample;
|
||||
|
|
@ -338,6 +343,26 @@ export const computeVectorVectorBinOp = (
|
|||
groups[sig].lhsCount++;
|
||||
});
|
||||
|
||||
// Check for any LHS / RHS with no series and fill in default values, if specified.
|
||||
Object.values(groups).forEach((mg) => {
|
||||
if (mg.lhs.length === 0 && matching.fillValues.lhs !== null) {
|
||||
mg.lhs.push({
|
||||
metric: {},
|
||||
value: [0, formatPrometheusFloat(matching.fillValues.lhs as number)],
|
||||
filled: true,
|
||||
});
|
||||
mg.lhsCount = 1;
|
||||
}
|
||||
if (mg.rhs.length === 0 && matching.fillValues.rhs !== null) {
|
||||
mg.rhs.push({
|
||||
metric: {},
|
||||
value: [0, formatPrometheusFloat(matching.fillValues.rhs as number)],
|
||||
filled: true,
|
||||
});
|
||||
mg.rhsCount = 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Annotate the match groups with errors (if any) and populate the results.
|
||||
Object.values(groups).forEach((mg) => {
|
||||
switch (matching.card) {
|
||||
|
|
|
|||
|
|
@ -265,6 +265,7 @@ const formatNodeInternal = (
|
|||
case nodeType.binaryExpr: {
|
||||
let matching = <></>;
|
||||
let grouping = <></>;
|
||||
let fill = <></>;
|
||||
const vm = node.matching;
|
||||
if (vm !== null) {
|
||||
if (
|
||||
|
|
@ -305,6 +306,45 @@ const formatNodeInternal = (
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const lfill = vm.fillValues.lhs;
|
||||
const rfill = vm.fillValues.rhs;
|
||||
if (lfill !== null || rfill !== null) {
|
||||
if (lfill === rfill) {
|
||||
fill = (
|
||||
<>
|
||||
{" "}
|
||||
<span className="promql-keyword">fill</span>
|
||||
<span className="promql-paren">(</span>
|
||||
<span className="promql-number">{lfill}</span>
|
||||
<span className="promql-paren">)</span>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
fill = (
|
||||
<>
|
||||
{lfill !== null && (
|
||||
<>
|
||||
{" "}
|
||||
<span className="promql-keyword">fill_left</span>
|
||||
<span className="promql-paren">(</span>
|
||||
<span className="promql-number">{lfill}</span>
|
||||
<span className="promql-paren">)</span>
|
||||
</>
|
||||
)}
|
||||
{rfill !== null && (
|
||||
<>
|
||||
{" "}
|
||||
<span className="promql-keyword">fill_right</span>
|
||||
<span className="promql-paren">(</span>
|
||||
<span className="promql-number">{rfill}</span>
|
||||
<span className="promql-paren">)</span>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -327,7 +367,8 @@ const formatNodeInternal = (
|
|||
</>
|
||||
)}
|
||||
{matching}
|
||||
{grouping}{" "}
|
||||
{grouping}
|
||||
{fill}{" "}
|
||||
{showChildren &&
|
||||
formatNode(
|
||||
maybeParenthesizeBinopChild(node.op, node.rhs),
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ const serializeNode = (
|
|||
case nodeType.binaryExpr: {
|
||||
let matching = "";
|
||||
let grouping = "";
|
||||
let fill = "";
|
||||
const vm = node.matching;
|
||||
if (vm !== null) {
|
||||
if (
|
||||
|
|
@ -152,11 +153,26 @@ const serializeNode = (
|
|||
) {
|
||||
grouping = ` group_${vm.card === vectorMatchCardinality.manyToOne ? "left" : "right"}(${labelNameList(vm.include)})`;
|
||||
}
|
||||
|
||||
const lfill = vm.fillValues.lhs;
|
||||
const rfill = vm.fillValues.rhs;
|
||||
if (lfill !== null || rfill !== null) {
|
||||
if (lfill === rfill) {
|
||||
fill = ` fill(${lfill})`;
|
||||
} else {
|
||||
if (lfill !== null) {
|
||||
fill += ` fill_left(${lfill})`;
|
||||
}
|
||||
if (rfill !== null) {
|
||||
fill += ` fill_right(${rfill})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return `${serializeNode(maybeParenthesizeBinopChild(node.op, node.lhs), childIndent, pretty)}${childSeparator}${ind}${
|
||||
node.op
|
||||
}${node.bool ? " bool" : ""}${matching}${grouping}${childSeparator}${serializeNode(
|
||||
}${node.bool ? " bool" : ""}${matching}${grouping}${fill}${childSeparator}${serializeNode(
|
||||
maybeParenthesizeBinopChild(node.op, node.rhs),
|
||||
childIndent,
|
||||
pretty
|
||||
|
|
|
|||
|
|
@ -658,6 +658,7 @@ describe("serializeNode and formatNode", () => {
|
|||
labels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
bool: false,
|
||||
},
|
||||
|
|
@ -677,6 +678,7 @@ describe("serializeNode and formatNode", () => {
|
|||
labels: [],
|
||||
on: true,
|
||||
include: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
bool: false,
|
||||
},
|
||||
|
|
@ -696,6 +698,7 @@ describe("serializeNode and formatNode", () => {
|
|||
labels: ["label1", "label2"],
|
||||
on: true,
|
||||
include: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
bool: false,
|
||||
},
|
||||
|
|
@ -715,6 +718,7 @@ describe("serializeNode and formatNode", () => {
|
|||
labels: ["label1", "label2"],
|
||||
on: false,
|
||||
include: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
bool: false,
|
||||
},
|
||||
|
|
@ -735,6 +739,7 @@ describe("serializeNode and formatNode", () => {
|
|||
labels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
bool: false,
|
||||
},
|
||||
|
|
@ -755,6 +760,7 @@ describe("serializeNode and formatNode", () => {
|
|||
labels: [],
|
||||
on: false,
|
||||
include: ["__name__"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
bool: false,
|
||||
},
|
||||
|
|
@ -774,6 +780,7 @@ describe("serializeNode and formatNode", () => {
|
|||
labels: ["label1", "label2"],
|
||||
on: true,
|
||||
include: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
bool: false,
|
||||
},
|
||||
|
|
@ -793,6 +800,7 @@ describe("serializeNode and formatNode", () => {
|
|||
labels: ["label1", "label2"],
|
||||
on: true,
|
||||
include: ["label3"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
bool: false,
|
||||
},
|
||||
|
|
@ -812,6 +820,7 @@ describe("serializeNode and formatNode", () => {
|
|||
labels: ["label1", "label2"],
|
||||
on: true,
|
||||
include: [],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
bool: false,
|
||||
},
|
||||
|
|
@ -831,6 +840,7 @@ describe("serializeNode and formatNode", () => {
|
|||
labels: ["label1", "label2"],
|
||||
on: true,
|
||||
include: ["label3"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
bool: false,
|
||||
},
|
||||
|
|
@ -864,6 +874,7 @@ describe("serializeNode and formatNode", () => {
|
|||
labels: ["label1", "label2"],
|
||||
on: true,
|
||||
include: ["label3"],
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
bool: true,
|
||||
},
|
||||
|
|
@ -911,6 +922,7 @@ describe("serializeNode and formatNode", () => {
|
|||
include: ["c", "ü"],
|
||||
labels: ["b", "ö"],
|
||||
on: true,
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
op: binaryOperatorType.div,
|
||||
rhs: {
|
||||
|
|
@ -948,6 +960,7 @@ describe("serializeNode and formatNode", () => {
|
|||
include: [],
|
||||
labels: ["e", "ö"],
|
||||
on: false,
|
||||
fillValues: { lhs: null, rhs: null },
|
||||
},
|
||||
op: binaryOperatorType.add,
|
||||
rhs: {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ export const binOpModifierTerms = [
|
|||
{ label: 'ignoring', info: 'Ignore specified labels for matching', type: 'keyword' },
|
||||
{ label: 'group_left', info: 'Allow many-to-one matching', type: 'keyword' },
|
||||
{ label: 'group_right', info: 'Allow one-to-many matching', type: 'keyword' },
|
||||
{ label: 'bool', info: 'Return boolean result (0 or 1) instead of filtering', type: 'keyword' },
|
||||
{ label: 'fill', info: 'Fill in missing series on both sides', type: 'keyword' },
|
||||
{ label: 'fill_left', info: 'Fill in missing series on the left side', type: 'keyword' },
|
||||
{ label: 'fill_right', info: 'Fill in missing series on the right side', type: 'keyword' },
|
||||
];
|
||||
|
||||
export const atModifierTerms = [
|
||||
|
|
|
|||
|
|
@ -15,29 +15,31 @@ import { buildVectorMatching } from './vector';
|
|||
import { createEditorState } from '../test/utils-test';
|
||||
import { BinaryExpr } from '@prometheus-io/lezer-promql';
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import { VectorMatchCardinality } from '../types';
|
||||
import { VectorMatchCardinality, VectorMatching } from '../types';
|
||||
|
||||
const noFill = { fill: { lhs: null, rhs: null } };
|
||||
|
||||
describe('buildVectorMatching test', () => {
|
||||
const testCases = [
|
||||
const testCases: { binaryExpr: string; expectedVectorMatching: VectorMatching }[] = [
|
||||
{
|
||||
binaryExpr: 'foo * bar',
|
||||
expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [] },
|
||||
expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [], ...noFill },
|
||||
},
|
||||
{
|
||||
binaryExpr: 'foo * sum',
|
||||
expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [] },
|
||||
expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [], ...noFill },
|
||||
},
|
||||
{
|
||||
binaryExpr: 'foo == 1',
|
||||
expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [] },
|
||||
expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [], ...noFill },
|
||||
},
|
||||
{
|
||||
binaryExpr: 'foo == bool 1',
|
||||
expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [] },
|
||||
expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [], ...noFill },
|
||||
},
|
||||
{
|
||||
binaryExpr: '2.5 / bar',
|
||||
expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [] },
|
||||
expectedVectorMatching: { card: VectorMatchCardinality.CardOneToOne, matchingLabels: [], on: false, include: [], ...noFill },
|
||||
},
|
||||
{
|
||||
binaryExpr: 'foo and bar',
|
||||
|
|
@ -46,6 +48,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -55,6 +58,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -64,6 +68,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -75,6 +80,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -86,6 +92,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -95,6 +102,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: ['test', 'blub'],
|
||||
on: true,
|
||||
include: [],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -104,6 +112,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: ['test', 'blub'],
|
||||
on: true,
|
||||
include: [],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -113,6 +122,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: ['test', 'blub'],
|
||||
on: true,
|
||||
include: [],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -122,6 +132,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: [],
|
||||
on: true,
|
||||
include: [],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -131,6 +142,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: ['test', 'blub'],
|
||||
on: false,
|
||||
include: [],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -140,6 +152,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -149,6 +162,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: ['bar'],
|
||||
on: true,
|
||||
include: [],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -158,6 +172,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: ['test', 'blub'],
|
||||
on: true,
|
||||
include: ['bar'],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -167,6 +182,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: ['test', 'blub'],
|
||||
on: false,
|
||||
include: ['blub'],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -176,6 +192,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: ['test', 'blub'],
|
||||
on: false,
|
||||
include: ['bar'],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -185,6 +202,7 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: ['test', 'blub'],
|
||||
on: true,
|
||||
include: ['bar', 'foo'],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -194,6 +212,57 @@ describe('buildVectorMatching test', () => {
|
|||
matchingLabels: ['test', 'blub'],
|
||||
on: false,
|
||||
include: ['bar', 'foo'],
|
||||
...noFill,
|
||||
},
|
||||
},
|
||||
{
|
||||
binaryExpr: 'foo + fill(23) bar',
|
||||
expectedVectorMatching: {
|
||||
card: VectorMatchCardinality.CardOneToOne,
|
||||
matchingLabels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
fill: { lhs: 23, rhs: 23 },
|
||||
},
|
||||
},
|
||||
{
|
||||
binaryExpr: 'foo + fill_left(23) bar',
|
||||
expectedVectorMatching: {
|
||||
card: VectorMatchCardinality.CardOneToOne,
|
||||
matchingLabels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
fill: { lhs: 23, rhs: null },
|
||||
},
|
||||
},
|
||||
{
|
||||
binaryExpr: 'foo + fill_right(23) bar',
|
||||
expectedVectorMatching: {
|
||||
card: VectorMatchCardinality.CardOneToOne,
|
||||
matchingLabels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
fill: { lhs: null, rhs: 23 },
|
||||
},
|
||||
},
|
||||
{
|
||||
binaryExpr: 'foo + fill_left(23) fill_right(42) bar',
|
||||
expectedVectorMatching: {
|
||||
card: VectorMatchCardinality.CardOneToOne,
|
||||
matchingLabels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
fill: { lhs: 23, rhs: 42 },
|
||||
},
|
||||
},
|
||||
{
|
||||
binaryExpr: 'foo + fill_right(23) fill_left(42) bar',
|
||||
expectedVectorMatching: {
|
||||
card: VectorMatchCardinality.CardOneToOne,
|
||||
matchingLabels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
fill: { lhs: 42, rhs: 23 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -203,7 +272,7 @@ describe('buildVectorMatching test', () => {
|
|||
const node = syntaxTree(state).topNode.getChild(BinaryExpr);
|
||||
expect(node).toBeTruthy();
|
||||
if (node) {
|
||||
expect(value.expectedVectorMatching).toEqual(buildVectorMatching(state, node));
|
||||
expect(buildVectorMatching(state, node)).toEqual(value.expectedVectorMatching);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@ import {
|
|||
On,
|
||||
Or,
|
||||
Unless,
|
||||
NumberDurationLiteral,
|
||||
FillModifier,
|
||||
FillClause,
|
||||
FillLeftClause,
|
||||
FillRightClause,
|
||||
} from '@prometheus-io/lezer-promql';
|
||||
import { VectorMatchCardinality, VectorMatching } from '../types';
|
||||
import { containsAtLeastOneChild } from './path-finder';
|
||||
|
|
@ -37,6 +42,10 @@ export function buildVectorMatching(state: EditorState, binaryNode: SyntaxNode):
|
|||
matchingLabels: [],
|
||||
on: false,
|
||||
include: [],
|
||||
fill: {
|
||||
lhs: null,
|
||||
rhs: null,
|
||||
},
|
||||
};
|
||||
const modifierClause = binaryNode.getChild(MatchingModifierClause);
|
||||
if (modifierClause) {
|
||||
|
|
@ -60,6 +69,32 @@ export function buildVectorMatching(state: EditorState, binaryNode: SyntaxNode):
|
|||
}
|
||||
}
|
||||
|
||||
const fillModifier = binaryNode.getChild(FillModifier);
|
||||
if (fillModifier) {
|
||||
const fill = fillModifier.getChild(FillClause);
|
||||
const fillLeft = fillModifier.getChild(FillLeftClause);
|
||||
const fillRight = fillModifier.getChild(FillRightClause);
|
||||
|
||||
const getFillValue = (node: SyntaxNode) => {
|
||||
const valueNode = node.getChild(NumberDurationLiteral);
|
||||
return valueNode ? parseFloat(state.sliceDoc(valueNode.from, valueNode.to)) : null;
|
||||
};
|
||||
|
||||
if (fill) {
|
||||
const value = getFillValue(fill);
|
||||
result.fill.lhs = value;
|
||||
result.fill.rhs = value;
|
||||
}
|
||||
|
||||
if (fillLeft) {
|
||||
result.fill.lhs = getFillValue(fillLeft);
|
||||
}
|
||||
|
||||
if (fillRight) {
|
||||
result.fill.rhs = getFillValue(fillRight);
|
||||
}
|
||||
}
|
||||
|
||||
const isSetOperator = containsAtLeastOneChild(binaryNode, And, Or, Unless);
|
||||
if (isSetOperator && result.card === VectorMatchCardinality.CardOneToOne) {
|
||||
result.card = VectorMatchCardinality.CardManyToMany;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ export enum VectorMatchCardinality {
|
|||
CardManyToMany = 'many-to-many',
|
||||
}
|
||||
|
||||
export interface FillValues {
|
||||
lhs: number | null;
|
||||
rhs: number | null;
|
||||
}
|
||||
|
||||
export interface VectorMatching {
|
||||
// The cardinality of the two Vectors.
|
||||
card: VectorMatchCardinality;
|
||||
|
|
@ -30,4 +35,6 @@ export interface VectorMatching {
|
|||
// Include contains additional labels that should be included in
|
||||
// the result from the side with the lower cardinality.
|
||||
include: string[];
|
||||
// Fill contains optional fill values for missing elements.
|
||||
fill: FillValues;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,11 +101,30 @@ MatchingModifierClause {
|
|||
((GroupLeft | GroupRight) (!group GroupingLabels)?)?
|
||||
}
|
||||
|
||||
FillClause {
|
||||
Fill "(" NumberDurationLiteral ")"
|
||||
}
|
||||
|
||||
FillLeftClause {
|
||||
FillLeft "(" NumberDurationLiteral ")"
|
||||
}
|
||||
|
||||
FillRightClause {
|
||||
FillRight "(" NumberDurationLiteral ")"
|
||||
}
|
||||
|
||||
FillModifier {
|
||||
(FillClause | FillLeftClause | FillRightClause) |
|
||||
(FillLeftClause FillRightClause) |
|
||||
(FillRightClause FillLeftClause)
|
||||
}
|
||||
|
||||
BoolModifier { Bool }
|
||||
|
||||
binModifiers {
|
||||
BoolModifier?
|
||||
MatchingModifierClause?
|
||||
FillModifier?
|
||||
}
|
||||
|
||||
GroupingLabels {
|
||||
|
|
@ -366,7 +385,10 @@ NumberDurationLiteralInDurationContext {
|
|||
Start,
|
||||
End,
|
||||
Smoothed,
|
||||
Anchored
|
||||
Anchored,
|
||||
Fill,
|
||||
FillLeft,
|
||||
FillRight
|
||||
}
|
||||
|
||||
@external propSource promQLHighLight from "./highlight"
|
||||
|
|
|
|||
|
|
@ -12,82 +12,88 @@
|
|||
// limitations under the License.
|
||||
|
||||
import {
|
||||
And,
|
||||
Avg,
|
||||
Atan2,
|
||||
Bool,
|
||||
Bottomk,
|
||||
By,
|
||||
Count,
|
||||
CountValues,
|
||||
End,
|
||||
Group,
|
||||
GroupLeft,
|
||||
GroupRight,
|
||||
Ignoring,
|
||||
inf,
|
||||
Max,
|
||||
Min,
|
||||
nan,
|
||||
Offset,
|
||||
On,
|
||||
Or,
|
||||
Quantile,
|
||||
LimitK,
|
||||
LimitRatio,
|
||||
Start,
|
||||
Stddev,
|
||||
Stdvar,
|
||||
Sum,
|
||||
Topk,
|
||||
Unless,
|
||||
Without,
|
||||
Smoothed,
|
||||
Anchored,
|
||||
} from './parser.terms.js';
|
||||
And,
|
||||
Avg,
|
||||
Atan2,
|
||||
Bool,
|
||||
Bottomk,
|
||||
By,
|
||||
Count,
|
||||
CountValues,
|
||||
End,
|
||||
Group,
|
||||
GroupLeft,
|
||||
GroupRight,
|
||||
Ignoring,
|
||||
inf,
|
||||
Max,
|
||||
Min,
|
||||
nan,
|
||||
Offset,
|
||||
On,
|
||||
Or,
|
||||
Quantile,
|
||||
LimitK,
|
||||
LimitRatio,
|
||||
Start,
|
||||
Stddev,
|
||||
Stdvar,
|
||||
Sum,
|
||||
Topk,
|
||||
Unless,
|
||||
Without,
|
||||
Smoothed,
|
||||
Anchored,
|
||||
Fill,
|
||||
FillLeft,
|
||||
FillRight,
|
||||
} from "./parser.terms.js";
|
||||
|
||||
const keywordTokens = {
|
||||
inf: inf,
|
||||
nan: nan,
|
||||
bool: Bool,
|
||||
ignoring: Ignoring,
|
||||
on: On,
|
||||
group_left: GroupLeft,
|
||||
group_right: GroupRight,
|
||||
offset: Offset,
|
||||
inf: inf,
|
||||
nan: nan,
|
||||
bool: Bool,
|
||||
ignoring: Ignoring,
|
||||
on: On,
|
||||
group_left: GroupLeft,
|
||||
group_right: GroupRight,
|
||||
offset: Offset,
|
||||
};
|
||||
|
||||
export const specializeIdentifier = (value, stack) => {
|
||||
return keywordTokens[value.toLowerCase()] || -1;
|
||||
return keywordTokens[value.toLowerCase()] || -1;
|
||||
};
|
||||
|
||||
const contextualKeywordTokens = {
|
||||
avg: Avg,
|
||||
atan2: Atan2,
|
||||
bottomk: Bottomk,
|
||||
count: Count,
|
||||
count_values: CountValues,
|
||||
group: Group,
|
||||
max: Max,
|
||||
min: Min,
|
||||
quantile: Quantile,
|
||||
limitk: LimitK,
|
||||
limit_ratio: LimitRatio,
|
||||
stddev: Stddev,
|
||||
stdvar: Stdvar,
|
||||
sum: Sum,
|
||||
topk: Topk,
|
||||
by: By,
|
||||
without: Without,
|
||||
and: And,
|
||||
or: Or,
|
||||
unless: Unless,
|
||||
start: Start,
|
||||
end: End,
|
||||
smoothed: Smoothed,
|
||||
anchored: Anchored,
|
||||
avg: Avg,
|
||||
atan2: Atan2,
|
||||
bottomk: Bottomk,
|
||||
count: Count,
|
||||
count_values: CountValues,
|
||||
group: Group,
|
||||
max: Max,
|
||||
min: Min,
|
||||
quantile: Quantile,
|
||||
limitk: LimitK,
|
||||
limit_ratio: LimitRatio,
|
||||
stddev: Stddev,
|
||||
stdvar: Stdvar,
|
||||
sum: Sum,
|
||||
topk: Topk,
|
||||
by: By,
|
||||
without: Without,
|
||||
and: And,
|
||||
or: Or,
|
||||
unless: Unless,
|
||||
start: Start,
|
||||
end: End,
|
||||
smoothed: Smoothed,
|
||||
anchored: Anchored,
|
||||
fill: Fill,
|
||||
fill_left: FillLeft,
|
||||
fill_right: FillRight,
|
||||
};
|
||||
|
||||
export const extendIdentifier = (value, stack) => {
|
||||
return contextualKeywordTokens[value.toLowerCase()] || -1;
|
||||
return contextualKeywordTokens[value.toLowerCase()] || -1;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue