cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
b13a87cdb942543011caed30a8da0abe98670667

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/parser/source/source.go

1782lines · modecode

1package source
2
3import (
4 "fmt"
5 "math"
6 "slices"
7 "strconv"
8 "strings"
9
10 "github.com/prometheus/common/model"
11 "github.com/prometheus/prometheus/model/labels"
12 promParser "github.com/prometheus/prometheus/promql/parser"
13 "github.com/prometheus/prometheus/promql/parser/posrange"
14)
15
16var guaranteedLabelsMatches = []labels.MatchType{labels.MatchEqual, labels.MatchRegexp}
17
18type Type uint8
19
20const (
21 UnknownSource Type = iota
22 NumberSource
23 StringSource
24 SelectorSource
25 FuncSource
26 AggregateSource
27)
28
29type RangeSelectorMode uint8
30
31const (
32 RangeSelectorDefault RangeSelectorMode = iota
33 RangeSelectorAnchored // VectorSelector uses the anchored modifier.
34 RangeSelectorSmoothed // VectorSelector uses the smoothed modifier.
35)
36
37const (
38 FeatureExperimentalFunctions = "promql-experimental-functions"
39 FeatureDurationExpr = "promql-duration-expr"
40 FeatureExtendedRangeSelectors = "promql-extended-range-selectors"
41 FeatureBinopFillModifiers = "promql-binop-fill-modifiers"
42)
43
44// Used for test snapshots.
45func (st Type) MarshalYAML() (any, error) {
46 var name string
47 switch st { // nolint: exhaustive
48 case NumberSource:
49 name = "number"
50 case StringSource:
51 name = "string"
52 case SelectorSource:
53 name = "selector"
54 case FuncSource:
55 name = "function"
56 case AggregateSource:
57 name = "aggregation"
58 }
59 return name, nil
60}
61
62// Used for test snapshots.
63func (rsm RangeSelectorMode) MarshalYAML() (any, error) {
64 switch rsm { // nolint: exhaustive
65 case RangeSelectorAnchored:
66 return "anchored", nil
67 case RangeSelectorSmoothed:
68 return "smoothed", nil
69 }
70 return "default", nil
71}
72
73type LabelPromiseType uint8
74
75const (
76 ImpossibleLabel LabelPromiseType = iota
77 PossibleLabel
78 GuaranteedLabel
79)
80
81// Used for test snapshots.
82func (lpt LabelPromiseType) MarshalYAML() (any, error) {
83 var name string
84 switch lpt {
85 case ImpossibleLabel:
86 name = "excluded"
87 case PossibleLabel:
88 name = "included"
89 case GuaranteedLabel:
90 name = "guaranteed"
91 }
92 return name, nil
93}
94
95type LabelTransform struct {
96 Reason string
97 Kind LabelPromiseType
98 Fragment posrange.PositionRange
99}
100
101type DeadInfo struct {
102 Reason string
103 Fragment posrange.PositionRange
104}
105
106type DeadLabelKind uint8
107
108const (
109 ImpossibleDeadLabel DeadLabelKind = iota
110 OrphanedLabel
111 DuplicatedJoin
112 UnusedLabel
113)
114
115func (dlk DeadLabelKind) String() string {
116 switch dlk {
117 case ImpossibleDeadLabel:
118 return "impossible label"
119 case OrphanedLabel:
120 return "orphaned label"
121 case DuplicatedJoin:
122 return "redundant label"
123 case UnusedLabel:
124 return "unused label"
125 }
126 return "unknown"
127}
128
129type DeadLabel struct {
130 Name string
131 Reason string
132 LabelReason string
133 UsageFragment posrange.PositionRange
134 LabelFragment posrange.PositionRange
135 Kind DeadLabelKind
136}
137
138type ReturnInfo struct {
139 LogicalExpr string
140 ValuePosition posrange.PositionRange
141 ReturnedNumber float64 // If AlwaysReturns=true this is the number that's returned
142 AlwaysReturns bool // True if this source always returns results.
143 KnownReturn bool // True if we always know the return value.
144 IsReturnBool bool // True if this source uses the 'bool' modifier.
145}
146
147type Condition struct {
148 Op promParser.ItemType `yaml:"op,omitempty"`
149 Value float64 `yaml:"value,omitempty"`
150 Present bool `yaml:"present,omitempty"`
151 KnownValue bool `yaml:"knownValue,omitempty"`
152}
153
154type Operation struct {
155 Node promParser.Node
156 Operation string
157 Arguments []string
158}
159
160// Used for test snapshots.
161func (so Operation) MarshalYAML() (any, error) {
162 y := map[string]any{
163 "op": so.Operation,
164 "node": fmt.Sprintf("[%T] %s", so.Node, so.Node.String()),
165 }
166 if so.Arguments != nil {
167 y["args"] = so.Arguments
168 }
169 return y, nil
170}
171
172type Operations []Operation
173
174func MostOuterOperation[T promParser.Node](s *Source) (T, bool) {
175 for _, op := range slices.Backward(s.Operations) {
176 if o, ok := op.Node.(T); ok {
177 return o, true
178 }
179 }
180 return *new(T), false
181}
182
183type Fill struct {
184 LHS float64
185 RHS float64
186}
187
188type Join struct {
189 Fill *Fill // Fill values for unmatched series (binop fill modifiers).
190 DeadInfo *DeadInfo
191 Src *Source // The source we're joining with.
192 DeadLabels []DeadLabel // Per-pairing dead labels from checkJoinedLabels/checkIncludedLabels.
193 MatchingLabels []string
194 AddedLabels []string
195 Op promParser.ItemType // The binary operation used for this join.
196 Depth int // Zero if this is a direct join, non-zero otherwise. sum(foo * bar) would be in-direct join.
197 IsOn bool
198}
199
200// Used for test snapshots.
201func (j Join) MarshalYAML() (any, error) {
202 m := map[string]any{
203 "matchinglabels": j.MatchingLabels,
204 "addedlabels": j.AddedLabels,
205 "src": j.Src,
206 "op": promParser.ItemTypeStr[j.Op],
207 "depth": j.Depth,
208 "ison": j.IsOn,
209 }
210 if j.Fill != nil {
211 m["fill"] = j.Fill
212 }
213 if j.DeadInfo != nil {
214 m["deadinfo"] = j.DeadInfo
215 }
216 if len(j.DeadLabels) > 0 {
217 m["deadlabels"] = j.DeadLabels
218 }
219 return m, nil
220}
221
222// Used for test snapshots.
223func (u Unless) MarshalYAML() (any, error) {
224 m := map[string]any{
225 "matchinglabels": u.MatchingLabels,
226 "src": u.Src,
227 "ison": u.IsOn,
228 }
229 if u.DeadInfo != nil {
230 m["deadinfo"] = u.DeadInfo
231 }
232 return m, nil
233}
234
235type Unless struct {
236 DeadInfo *DeadInfo
237 Src *Source
238 MatchingLabels []string
239 IsOn bool
240}
241
242type Side uint8
243
244const (
245 LHS Side = iota
246 RHS
247)
248
249type Indirect struct {
250 Src *Source `yaml:"src"`
251 Op promParser.ItemType `yaml:"op"`
252 Side Side `yaml:"side"`
253}
254
255type FeatureRequirement struct {
256 Feature string `yaml:"feature"`
257 Name string `yaml:"name"`
258 Fragments []posrange.PositionRange `yaml:"fragments"`
259}
260
261type Source struct {
262 Labels map[string]LabelTransform `yaml:"labels,omitempty"`
263 DeadInfo *DeadInfo `yaml:"deadInfo,omitempty"`
264 Returns promParser.ValueType `yaml:"returns"`
265 DeadLabels []DeadLabel `yaml:"deadLabels,omitempty"`
266 Operations Operations `yaml:"operations,omitempty"`
267 // Any other sources this source joins with.
268 Joins []Join `yaml:"joins,omitempty"`
269 // Any other sources this source is suppressed by.
270 Unless []Unless `yaml:"unless,omitempty"`
271 // Any other sources used indirectly, not contributing labels.
272 Indirect []Indirect `yaml:"indirect,omitempty"`
273 NeedsFeatures []FeatureRequirement `yaml:"needsFeatures,omitempty"`
274 UsedLabels []string `yaml:"usedLabels,omitempty"`
275 ReturnInfo ReturnInfo `yaml:"returnInfo,omitempty"`
276 Condition Condition `yaml:"condition,omitempty"`
277 Position posrange.PositionRange `yaml:"position"`
278 Type Type `yaml:"type"`
279 // Labels are fixed and only allowed labels can be present.
280 FixedLabels bool `yaml:"fixedLabels,omitempty"`
281 RangeSelectorMode RangeSelectorMode `yaml:"rangeSelectorMode,omitempty"`
282}
283
284// requireFeature is called for every PromQL function, aggregation, and modifier.
285// It looks up the element name in the feature version map to determine whether
286// a feature flag is needed. If the name is not in the map then it's a standard
287// PromQL element that doesn't require any feature flag, so we skip it.
288// When a match is found, the query position is recorded so that diagnostics
289// can later highlight the exact fragment that requires the flag.
290func (s *Source) requireFeature(name string, pos posrange.PositionRange) {
291 // Only elements listed in the feature version map require a feature flag.
292 // Everything else is standard PromQL and can be ignored.
293 fv, ok := LookupFeatureVersion(name)
294 if !ok {
295 return
296 }
297 // If we already recorded a requirement for the same feature+name pair,
298 // just append the new position fragment to avoid duplicate entries.
299 for i, req := range s.NeedsFeatures {
300 if req.Feature == fv.Flag && req.Name == name {
301 s.NeedsFeatures[i].Fragments = append(s.NeedsFeatures[i].Fragments, pos)
302 return
303 }
304 }
305 s.NeedsFeatures = append(s.NeedsFeatures, FeatureRequirement{
306 Feature: fv.Flag,
307 Name: name,
308 Fragments: []posrange.PositionRange{pos},
309 })
310}
311
312func (s *Source) clone() *Source {
313 c := new(Source)
314 *c = *s
315 return c
316}
317
318func (s Source) Operation() string {
319 if len(s.Operations) > 0 {
320 return s.Operations[len(s.Operations)-1].Operation
321 }
322 return ""
323}
324
325func (s Source) CanHaveLabel(name string) bool {
326 if v, ok := s.Labels[name]; ok {
327 if v.Kind == ImpossibleLabel {
328 return false
329 }
330 if v.Kind == PossibleLabel || v.Kind == GuaranteedLabel {
331 return true
332 }
333 }
334 return !s.FixedLabels
335}
336
337func (s Source) TransformedLabels(kinds ...LabelPromiseType) []string {
338 names := make([]string, 0, len(s.Labels))
339 for name, l := range s.Labels {
340 if slices.Contains(kinds, l.Kind) {
341 names = append(names, name)
342 }
343 }
344 return names
345}
346
347func (s Source) LabelExcludeReason(name string) (string, posrange.PositionRange) {
348 if l, ok := s.Labels[name]; ok && l.Kind == ImpossibleLabel {
349 return l.Reason, l.Fragment
350 }
351 return s.Labels[""].Reason, s.Labels[""].Fragment
352}
353
354func (s *Source) excludeAllLabels(expr, reason string, fragment, allFragment posrange.PositionRange, except []string) {
355 // Everything that was included until now but will be removed needs an explicit stamp to mark it as gone.
356 for name, l := range s.Labels {
357 if slices.Contains(except, name) {
358 continue
359 }
360 if l.Kind == PossibleLabel || l.Kind == GuaranteedLabel {
361 s.Labels[name] = LabelTransform{
362 Kind: ImpossibleLabel,
363 Reason: reason,
364 Fragment: fragment,
365 }
366 }
367 }
368 s.UsedLabels = slices.DeleteFunc(s.UsedLabels, func(name string) bool {
369 return !slices.Contains(except, name)
370 })
371 // Mark except labels as possible, unless they are already guaranteed.
372 s.UsedLabels = appendToSlice(s.UsedLabels, except...)
373 for _, name := range except {
374 if l, ok := s.Labels[name]; ok && l.Kind == GuaranteedLabel {
375 continue
376 }
377
378 // We have grouping labels set, if they are possible mark them as such, if not mark as impossible.
379 if s.CanHaveLabel(name) {
380 s.Labels[name] = LabelTransform{
381 Kind: PossibleLabel,
382 Reason: reason,
383 Fragment: findArgumentPosition(expr, fragment, name),
384 }
385 } else {
386 r, f := s.LabelExcludeReason(name)
387 s.Labels[name] = LabelTransform{
388 Kind: ImpossibleLabel,
389 Reason: r,
390 Fragment: f,
391 }
392 }
393
394 }
395 s.Labels[""] = LabelTransform{
396 Kind: ImpossibleLabel,
397 Reason: reason,
398 Fragment: allFragment,
399 }
400 s.FixedLabels = true
401}
402
403func (s *Source) excludeLabel(reason string, fragment posrange.PositionRange, name string) {
404 s.Labels[name] = LabelTransform{
405 Kind: ImpossibleLabel,
406 Reason: reason,
407 Fragment: fragment,
408 }
409 s.UsedLabels = slices.DeleteFunc(s.UsedLabels, func(s string) bool {
410 return s == name
411 })
412}
413
414func (s *Source) joinLabels(expr string, within posrange.PositionRange, op promParser.ItemType, names []string, outside []posrange.PositionRange) {
415 for _, name := range names {
416 if l, ok := s.Labels[name]; ok && l.Kind == GuaranteedLabel {
417 s.DeadLabels = append(s.DeadLabels, DeadLabel{
418 Kind: DuplicatedJoin,
419 Name: name,
420 Reason: "Query is trying to join the `" + name + "` label that is already present on the other side of the query.",
421 UsageFragment: findArgumentPosition(
422 expr,
423 FindFuncPosition(expr, within, promParser.ItemTypeStr[op], outside),
424 name,
425 ),
426 LabelReason: l.Reason,
427 LabelFragment: l.Fragment,
428 })
429 return
430 }
431 s.Labels[name] = LabelTransform{
432 Kind: PossibleLabel,
433 Reason: fmt.Sprintf(
434 "Query is using `%s(%s)`, all labels included inside `%s(...)` will be joined to the results on the other side of the query.",
435 promParser.ItemTypeStr[op], strings.Join(names, ", "), promParser.ItemTypeStr[op],
436 ),
437 Fragment: findArgumentPosition(
438 expr,
439 FindFuncPosition(expr, within, promParser.ItemTypeStr[op], outside),
440 name,
441 ),
442 }
443 }
444}
445
446func (s *Source) includeLabel(expr, reason string, fragment posrange.PositionRange, name string) {
447 if l, ok := s.Labels[name]; ok && l.Kind == GuaranteedLabel {
448 return
449 }
450 s.Labels[name] = LabelTransform{
451 Kind: PossibleLabel,
452 Reason: reason,
453 Fragment: findArgumentPosition(expr, fragment, name),
454 }
455}
456
457func (s *Source) guaranteeLabel(reason string, fragment posrange.PositionRange, names ...string) {
458 for _, name := range names {
459 s.Labels[name] = LabelTransform{
460 Kind: GuaranteedLabel,
461 Reason: reason,
462 Fragment: fragment,
463 }
464 }
465}
466
467func findDeadIncludedLabels(s *Source, expr string, pos posrange.PositionRange, names []string) []DeadLabel {
468 var dead []DeadLabel
469 for _, name := range names {
470 if !s.CanHaveLabel(name) {
471 reason, fragment := s.LabelExcludeReason(name)
472 dead = append(dead, DeadLabel{
473 Kind: ImpossibleDeadLabel,
474 Name: name,
475 Reason: "You can't use `" + name + "` because this label is not possible here.",
476 UsageFragment: findArgumentPosition(expr, pos, name),
477 LabelReason: reason,
478 LabelFragment: fragment,
479 })
480 }
481 }
482 return dead
483}
484
485func (s *Source) checkIncludedLabels(expr string, pos posrange.PositionRange, names []string) {
486 s.DeadLabels = append(s.DeadLabels, findDeadIncludedLabels(s, expr, pos, names)...)
487}
488
489func (s *Source) checkAggregationLabels(expr string, n *promParser.AggregateExpr) {
490 var pos posrange.PositionRange
491 switch {
492 case len(n.Grouping) == 0:
493 pos = FindFuncNamePosition(expr, n.PosRange, promParser.ItemTypeStr[n.Op])
494 case n.Without:
495 pos = FindFuncPosition(expr, n.PosRange, promParser.ItemTypeStr[promParser.WITHOUT], nil)
496 default:
497 pos = FindFuncPosition(expr, n.PosRange, promParser.ItemTypeStr[promParser.BY], nil)
498 }
499
500 for _, j := range s.Joins {
501 for _, name := range j.AddedLabels {
502 if slices.Contains(s.UsedLabels, name) {
503 continue
504 }
505 if !n.Without && slices.Contains(n.Grouping, name) {
506 continue
507 }
508 if n.Without && !slices.Contains(n.Grouping, name) {
509 continue
510 }
511 var (
512 labelPos = pos
513 labelReason string
514 )
515 if t := s.findLabelTransform(name); t != nil {
516 labelPos = t.Fragment
517 labelReason = t.Reason
518 }
519 s.DeadLabels = append(s.DeadLabels, DeadLabel{
520 Kind: UnusedLabel,
521 Name: name,
522 Reason: fmt.Sprintf("Previously joined label `%s` is being removed from the results.", name),
523 UsageFragment: findArgumentPosition(expr, pos, name),
524 LabelReason: labelReason,
525 LabelFragment: labelPos,
526 })
527 }
528 }
529}
530
531func (s *Source) findLabelTransform(name string) *LabelTransform {
532 if t, ok := s.Labels[name]; ok {
533 return &t
534 }
535 for _, j := range s.Joins {
536 if t := j.Src.findLabelTransform(name); t != nil {
537 return t
538 }
539 }
540 return nil
541}
542
543func (s *Source) checkJoinedLabels(expr string, n *promParser.BinaryExpr, dst *Source) (dead []DeadLabel) {
544 pos := findBinOpsOperatorPosition(expr, n, promParser.ItemTypeStr[n.Op])
545 for _, j := range s.Joins {
546 for _, name := range j.AddedLabels {
547 if slices.Contains(n.VectorMatching.Include, name) {
548 // label is included in group_left or group_right
549 continue
550 }
551 if n.VectorMatching.On && slices.Contains(n.VectorMatching.MatchingLabels, name) {
552 // label is included inside on(...)
553 continue
554 }
555 if !n.VectorMatching.On && !slices.Contains(n.VectorMatching.MatchingLabels, name) {
556 // label is NOT included inside ignoring(...)
557 continue
558 }
559 var (
560 labelPos = pos
561 labelReason string
562 )
563 if t := dst.findLabelTransform(name); t != nil {
564 labelPos = t.Fragment
565 labelReason = t.Reason
566 }
567 dead = append(dead, DeadLabel{
568 Kind: OrphanedLabel,
569 Name: name,
570 Reason: fmt.Sprintf("This binary operation prevents previously joined label `%s` from being added to the results.", name),
571 UsageFragment: pos,
572 LabelReason: labelReason,
573 LabelFragment: labelPos,
574 })
575 }
576 }
577 return dead
578}
579
580func (s *Source) useLabelsNotExcluded(excluded []string) {
581 // Iterating over a map can yield labels in different order each time
582 // so append labels to an extra slice, sort it, and then append the
583 // sorted results to UsedLabels.
584 // Without this tests might show a diff sometimes.
585 toAdd := make([]string, 0, len(s.Labels))
586 for name, lt := range s.Labels {
587 if lt.Kind == ImpossibleLabel {
588 continue
589 }
590 if !slices.Contains(excluded, name) {
591 toAdd = appendToSlice(toAdd, name)
592 }
593 }
594 slices.Sort(toAdd)
595 s.UsedLabels = appendToSlice(s.UsedLabels, toAdd...)
596}
597
598type Visitor func(s *Source, j *Join, u *Unless)
599
600func innerWalk(fn Visitor, s *Source, j *Join, u *Unless) {
601 fn(s, j, u)
602 for i := range s.Joins {
603 innerWalk(fn, s.Joins[i].Src, &s.Joins[i], nil)
604 }
605 for i := range s.Unless {
606 innerWalk(fn, s.Unless[i].Src, nil, &s.Unless[i])
607 }
608 for _, ind := range s.Indirect {
609 innerWalk(fn, ind.Src, nil, nil)
610 }
611}
612
613func (s *Source) WalkSources(fn Visitor) {
614 innerWalk(fn, s, nil, nil)
615}
616
617func LabelsSource(expr string, node promParser.Node) (src []*Source) {
618 return walkNode(expr, node)
619}
620
621func walkNode(expr string, node promParser.Node) (src []*Source) {
622 switch n := node.(type) {
623 case *promParser.AggregateExpr:
624 src = append(src, walkAggregation(expr, n)...)
625
626 case *promParser.BinaryExpr:
627 src = append(src, parseBinOps(expr, n)...)
628
629 case *promParser.Call:
630 src = append(src, parseCall(expr, n)...)
631
632 case *promParser.MatrixSelector:
633 for _, s := range walkNode(expr, n.VectorSelector) {
634 s.Returns = promParser.ValueTypeMatrix
635 // Prepend Matrix operation
636 s.Operations = append(
637 Operations{
638 {
639 Operation: "",
640 Node: node,
641 Arguments: nil,
642 },
643 },
644 s.Operations...,
645 )
646 s.requireDurationExprFeatures(n.RangeExpr)
647 src = append(src, s)
648 }
649
650 case *promParser.SubqueryExpr:
651 for _, s := range walkNode(expr, n.Expr) {
652 // Prepend Subquery operation
653 s.Operations = append(
654 Operations{
655 {
656 Operation: "",
657 Node: node,
658 Arguments: nil,
659 },
660 },
661 s.Operations...,
662 )
663 s.requireDurationExprFeatures(n.RangeExpr)
664 s.requireDurationExprFeatures(n.StepExpr)
665 s.requireDurationExprFeatures(n.OriginalOffsetExpr)
666 s.requireStartOrEndFeature(expr, n.StartOrEnd, n.PositionRange())
667 src = append(src, s)
668 }
669
670 case *promParser.NumberLiteral:
671 s := new(Source)
672 s.Labels = map[string]LabelTransform{}
673 s.Type = NumberSource
674 s.Returns = promParser.ValueTypeScalar
675 s.ReturnInfo.KnownReturn = true
676 s.ReturnInfo.ReturnedNumber = n.Val
677 s.ReturnInfo.AlwaysReturns = true
678 s.ReturnInfo.ValuePosition = n.PosRange
679 s.excludeAllLabels(expr, "This query returns a number value with no labels.", n.PosRange, n.PosRange, nil)
680 s.Position = n.PosRange
681 src = append(src, s)
682
683 case *promParser.ParenExpr:
684 src = append(src, walkNode(expr, n.Expr)...)
685
686 case *promParser.StringLiteral:
687 s := new(Source)
688 s.Labels = map[string]LabelTransform{}
689 s.Type = StringSource
690 s.Returns = promParser.ValueTypeString
691 s.ReturnInfo.AlwaysReturns = true
692 s.excludeAllLabels(expr, "This query returns a string value with no labels.", n.PosRange, n.PosRange, nil)
693 s.Position = n.PosRange
694 src = append(src, s)
695
696 case *promParser.UnaryExpr:
697 src = append(src, walkNode(expr, n.Expr)...)
698
699 // case *promParser.StepInvariantExpr:
700 // Not possible to get this from the parser.
701
702 case *promParser.VectorSelector:
703 s := new(Source)
704 s.Labels = map[string]LabelTransform{}
705 s.Type = SelectorSource
706 s.Returns = promParser.ValueTypeVector
707 switch {
708 case n.Anchored:
709 s.RangeSelectorMode = RangeSelectorAnchored
710 s.requireFeature(promParser.ItemTypeStr[promParser.ANCHORED], n.PosRange)
711 case n.Smoothed:
712 s.RangeSelectorMode = RangeSelectorSmoothed
713 s.requireFeature(promParser.ItemTypeStr[promParser.SMOOTHED], n.PosRange)
714 }
715 s.requireDurationExprFeatures(n.OriginalOffsetExpr)
716 s.requireStartOrEndFeature(expr, n.StartOrEnd, n.PosRange)
717 s.Operations = append(s.Operations, Operation{
718 Operation: "",
719 Node: n,
720 Arguments: nil,
721 })
722 s.guaranteeLabel(
723 "Query will only return series where these labels are present.",
724 n.PosRange,
725 labelsFromSelectors(guaranteedLabelsMatches, n)...,
726 )
727 for _, name := range labelsWithEmptyValueSelector(n) {
728 s.excludeLabel(
729 "Query uses `{"+name+"=\"\"}` selector which will filter out any time series with the `"+name+"` label set.",
730 n.PosRange,
731 name,
732 )
733 }
734 s.Position = n.PosRange
735 src = append(src, s)
736 }
737 return src
738}
739
740func appendToSlice(dst []string, values ...string) []string {
741 for _, v := range values {
742 if !slices.Contains(dst, v) {
743 dst = append(dst, v)
744 }
745 }
746 return dst
747}
748
749func labelsFromSelectors(matches []labels.MatchType, selector *promParser.VectorSelector) (names []string) {
750 if selector == nil {
751 return nil
752 }
753 // Any label used in positive filters is gurnateed to be present.
754 for _, lm := range selector.LabelMatchers {
755 if lm.Name == model.MetricNameLabel {
756 continue
757 }
758 if !slices.Contains(matches, lm.Type) {
759 continue
760 }
761 names = appendToSlice(names, lm.Name)
762 }
763 return names
764}
765
766func labelsWithEmptyValueSelector(selector *promParser.VectorSelector) (names []string) {
767 for _, lm := range selector.LabelMatchers {
768 if lm.Name == model.MetricNameLabel {
769 continue
770 }
771 if lm.Type == labels.MatchEqual && lm.Value == "" {
772 names = appendToSlice(names, lm.Name)
773 }
774 }
775 return names
776}
777
778func walkAggregation(expr string, n *promParser.AggregateExpr) (src []*Source) {
779 var s *Source
780
781 var args []string
782 if n.Param != nil {
783 args = append(args, n.Param.String())
784 }
785
786 switch n.Op {
787 case promParser.COUNT_VALUES:
788 for _, s = range parseAggregation(expr, n) {
789 s.Operations = append(s.Operations, Operation{
790 Operation: promParser.ItemTypeStr[n.Op],
791 Node: n,
792 Arguments: args,
793 })
794 // Param is the label to store the count value in.
795 s.guaranteeLabel(
796 "This label will be added to the results by the count_values() call.",
797 n.PosRange,
798 n.Param.(*promParser.StringLiteral).Val,
799 )
800 if n.Without || !slices.Contains(n.Grouping, model.MetricNameLabel) {
801 s.excludeLabel("Aggregation removes metric name.", n.PosRange, model.MetricNameLabel)
802 }
803 src = append(src, s)
804 }
805 case promParser.TOPK, promParser.BOTTOMK:
806 for _, s = range walkNode(expr, n.Expr) {
807 for i := range s.Joins {
808 s.Joins[i].Depth++
809 }
810 s.Type = AggregateSource
811 s.Operations = append(s.Operations, Operation{
812 Operation: promParser.ItemTypeStr[n.Op],
813 Node: n,
814 Arguments: args,
815 })
816 src = append(src, s)
817 }
818 default:
819 for _, s = range parseAggregation(expr, n) {
820 s.Operations = append(s.Operations, Operation{
821 Operation: promParser.ItemTypeStr[n.Op],
822 Node: n,
823 Arguments: args,
824 })
825 if n.Without || !slices.Contains(n.Grouping, model.MetricNameLabel) {
826 s.excludeLabel("Aggregation removes metric name.", n.PosRange, model.MetricNameLabel)
827 }
828 {
829 name := promParser.ItemTypeStr[n.Op]
830 s.requireFeature(name, FindFuncNamePosition(expr, n.PosRange, name))
831 }
832 src = append(src, s)
833 }
834 }
835 return src
836}
837
838func parseAggregation(expr string, n *promParser.AggregateExpr) (src []*Source) {
839 var s *Source
840 for _, s = range walkNode(expr, n.Expr) {
841 // If we have sum(foo * bar) then we start with:
842 // - source: foo
843 // joins: bar
844 // Then we parse aggregation and end up with:
845 // - source: sum(foo)
846 // joins: bar
847 // Which is misleading and wrong, so we bump depth value to know about it.
848 for i := range s.Joins {
849 s.Joins[i].Depth++
850 }
851
852 s.checkAggregationLabels(expr, n)
853 if n.Without {
854 for _, name := range n.Grouping {
855 s.excludeLabel(
856 fmt.Sprintf("Query is using aggregation with `%s(%s)`, all labels included inside `%s(...)` will be removed from the results.",
857 promParser.ItemTypeStr[promParser.WITHOUT], strings.Join(n.Grouping, ", "), promParser.ItemTypeStr[promParser.WITHOUT]),
858 findArgumentPosition(
859 expr,
860 FindFuncPosition(expr, n.PosRange, promParser.ItemTypeStr[promParser.WITHOUT], nil),
861 name,
862 ),
863 name,
864 )
865 }
866 } else {
867 if len(n.Grouping) == 0 {
868 funcNamePos := FindFuncNamePosition(expr, n.PosRange, promParser.ItemTypeStr[n.Op])
869 s.excludeAllLabels(
870 expr,
871 "Query is using aggregation that removes all labels.",
872 funcNamePos,
873 funcNamePos,
874 nil,
875 )
876 } else {
877 s.UsedLabels = appendToSlice(s.UsedLabels, n.Grouping...)
878 s.checkIncludedLabels(
879 expr,
880 FindFuncPosition(expr, n.PosRange, promParser.ItemTypeStr[promParser.BY], nil),
881 n.Grouping,
882 )
883 s.excludeAllLabels(
884 expr,
885 fmt.Sprintf("Query is using aggregation with `%s(%s)`, only labels included inside `%s(...)` will be present on the results.",
886 promParser.ItemTypeStr[promParser.BY], strings.Join(n.Grouping, ", "), promParser.ItemTypeStr[promParser.BY]),
887 FindFuncPosition(expr, n.PosRange, promParser.ItemTypeStr[promParser.BY], nil),
888 FindFuncNamePosition(expr, n.PosRange, promParser.ItemTypeStr[promParser.BY]),
889 n.Grouping,
890 )
891 }
892 }
893 s.Type = AggregateSource
894 s.Returns = promParser.ValueTypeVector
895 src = append(src, s)
896 }
897 return src
898}
899
900func parsePromQLFunc(s *Source, expr string, n *promParser.Call) *Source {
901 s.requireFeature(n.Func.Name, FindFuncNamePosition(expr, n.PositionRange(), n.Func.Name))
902 switch n.Func.Name {
903 case "abs", "sgn",
904 "acos", "acosh", "asin", "asinh", "atan", "atanh",
905 "cos", "cosh", "sin", "sinh", "tan", "tanh":
906 // No change to labels.
907 s.Returns = promParser.ValueTypeVector
908
909 case "ceil", "floor", "round":
910 // No change to labels.
911 s.Returns = promParser.ValueTypeVector
912
913 case "changes", "resets":
914 // No change to labels.
915 s.Returns = promParser.ValueTypeVector
916
917 case "clamp", "clamp_max", "clamp_min":
918 // No change to labels.
919 s.Returns = promParser.ValueTypeVector
920
921 case "absent", "absent_over_time":
922 s.Returns = promParser.ValueTypeVector
923 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
924 names := labelsFromSelectors([]labels.MatchType{labels.MatchEqual}, vs)
925 funcNamePos := FindFuncNamePosition(expr, n.PosRange, n.Func.Name)
926 s.excludeAllLabels(
927 expr,
928 fmt.Sprintf(`The [%s()](https://prometheus.io/docs/prometheus/latest/querying/functions/#%s) function is used to check if the provided query doesn't match any time series.
929You will only get any results back if the metric selector you pass doesn't match anything.
930Since there are no matching time series there are also no labels. If some time series is missing you cannot read its labels.
931This means that the only labels you can get back from absent call are the ones you pass to it.
932If you're hoping to get instance specific labels this way and alert when some target is down then that won't work, use the `+"`up`"+` metric instead.`,
933 n.Func.Name, n.Func.Name),
934 funcNamePos,
935 funcNamePos,
936 names,
937 )
938 s.guaranteeLabel(
939 fmt.Sprintf("All labels passed to a %s() call will be present on the results if the query doesn't match anything.", n.Func.Name),
940 n.PosRange,
941 names...,
942 )
943
944 case "avg_over_time",
945 "count_over_time",
946 "first_over_time",
947 "last_over_time",
948 "mad_over_time",
949 "max_over_time",
950 "min_over_time",
951 "present_over_time",
952 "quantile_over_time",
953 "stddev_over_time",
954 "stdvar_over_time",
955 "sum_over_time":
956 // No change to labels.
957 s.Returns = promParser.ValueTypeVector
958
959 case "days_in_month", "day_of_month", "day_of_week", "day_of_year", "hour", "minute", "month", "year":
960 s.Returns = promParser.ValueTypeVector
961 // No labels if we don't pass any arguments.
962 // Otherwise no change to labels.
963 if len(n.Args) == 0 {
964 s.ReturnInfo.AlwaysReturns = true
965 s.excludeAllLabels(
966 expr,
967 fmt.Sprintf("Calling `%s()` with no arguments will return an empty time series with no labels.",
968 n.Func.Name),
969 n.PosRange,
970 n.PosRange,
971 nil,
972 )
973 }
974
975 case "deg", "rad", "ln", "log10", "log2", "sqrt", "exp":
976 // No change to labels.
977 s.Returns = promParser.ValueTypeVector
978
979 case "delta", "idelta", "increase", "deriv", "irate", "rate":
980 // No change to labels.
981 s.Returns = promParser.ValueTypeVector
982
983 case "histogram_avg",
984 "histogram_count",
985 "histogram_fraction",
986 "histogram_quantile",
987 "histogram_quantiles",
988 "histogram_stddev",
989 "histogram_stdvar",
990 "histogram_sum":
991 // No change to labels.
992 s.Returns = promParser.ValueTypeVector
993
994 case "double_exponential_smoothing", "holt_winters", "predict_linear":
995 // No change to labels.
996 s.Returns = promParser.ValueTypeVector
997
998 case "label_join":
999 // One label added to the results.
1000 // label_join(v instant-vector, dst_label string, separator string, src_label_1 string, src_label_2 string, ...)
1001 s.Returns = promParser.ValueTypeVector
1002 s.guaranteeLabel(
1003 fmt.Sprintf("This label will be added to the result by a %s() call.", n.Func.Name),
1004 n.PosRange,
1005 n.Args[1].(*promParser.StringLiteral).Val,
1006 )
1007 for i := 3; i < len(n.Args); i++ {
1008 s.UsedLabels = appendToSlice(s.UsedLabels, n.Args[i].(*promParser.StringLiteral).Val)
1009 }
1010
1011 case "label_replace":
1012 // One label added to the results.
1013 // label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)
1014 s.Returns = promParser.ValueTypeVector
1015 s.guaranteeLabel(
1016 fmt.Sprintf("This label will be added to the result by a %s() call.", n.Func.Name),
1017 n.PosRange,
1018 n.Args[1].(*promParser.StringLiteral).Val,
1019 )
1020 s.UsedLabels = appendToSlice(s.UsedLabels, n.Args[3].(*promParser.StringLiteral).Val)
1021
1022 case "pi":
1023 s.Returns = promParser.ValueTypeScalar
1024 s.ReturnInfo.AlwaysReturns = true
1025 s.ReturnInfo.KnownReturn = true
1026 s.ReturnInfo.ReturnedNumber = math.Pi
1027 s.ReturnInfo.ValuePosition = n.PosRange
1028 s.excludeAllLabels(
1029 expr,
1030 fmt.Sprintf("Calling `%s()` will return a scalar value with no labels.", n.Func.Name),
1031 n.PosRange,
1032 n.PosRange,
1033 nil,
1034 )
1035
1036 case "scalar":
1037 s.Returns = promParser.ValueTypeScalar
1038 s.ReturnInfo.AlwaysReturns = true
1039 funcPos := FindFuncPosition(expr, n.PositionRange(), n.Func.Name, nil)
1040 s.excludeAllLabels(
1041 expr,
1042 fmt.Sprintf("Calling `%s()` will return a scalar value with no labels.", n.Func.Name),
1043 funcPos,
1044 funcPos,
1045 nil,
1046 )
1047
1048 case "sort", "sort_desc", "sort_by_label", "sort_by_label_desc":
1049 // No change to labels.
1050 s.Returns = promParser.ValueTypeVector
1051
1052 case "time", "start", "end", "range", "step":
1053 s.Returns = promParser.ValueTypeScalar
1054 s.ReturnInfo.AlwaysReturns = true
1055 s.ReturnInfo.ValuePosition = n.PosRange
1056 s.excludeAllLabels(
1057 expr,
1058 fmt.Sprintf("Calling `%s()` will return a scalar value with no labels.", n.Func.Name),
1059 n.PosRange,
1060 n.PosRange,
1061 nil,
1062 )
1063
1064 case "timestamp",
1065 "ts_of_first_over_time",
1066 "ts_of_last_over_time",
1067 "ts_of_max_over_time",
1068 "ts_of_min_over_time":
1069 // No change to labels.
1070 s.Returns = promParser.ValueTypeVector
1071
1072 case "info":
1073 // info() joins labels from an info-series onto the input vector.
1074 // The joined labels are dynamic so we treat this as no label change.
1075 s.Returns = promParser.ValueTypeVector
1076
1077 case "vector":
1078 s.Returns = promParser.ValueTypeVector
1079 s.ReturnInfo.AlwaysReturns = true
1080 s.ReturnInfo.ValuePosition = n.PosRange
1081 for _, vs := range walkNode(expr, n.Args[0]) {
1082 if vs.ReturnInfo.KnownReturn {
1083 s.ReturnInfo.ReturnedNumber = vs.ReturnInfo.ReturnedNumber
1084 s.ReturnInfo.KnownReturn = true
1085 }
1086 }
1087 funcNamePos := FindFuncNamePosition(expr, n.PosRange, n.Func.Name)
1088 s.excludeAllLabels(
1089 expr,
1090 fmt.Sprintf("Calling `%s()` will return a vector value with no labels.", n.Func.Name),
1091 funcNamePos,
1092 funcNamePos,
1093 nil,
1094 )
1095
1096 default:
1097 // Unsupported function
1098 return new(Source)
1099 }
1100 return s
1101}
1102
1103func parseCall(expr string, n *promParser.Call) (src []*Source) {
1104 var args []string
1105 var exprs []promParser.Expr
1106 var indirect []Indirect
1107
1108 var vt promParser.ValueType
1109 for i, e := range n.Args {
1110 if i >= len(n.Func.ArgTypes) {
1111 vt = n.Func.ArgTypes[len(n.Func.ArgTypes)-1]
1112 } else {
1113 vt = n.Func.ArgTypes[i]
1114 }
1115
1116 switch vt {
1117 case promParser.ValueTypeVector, promParser.ValueTypeMatrix:
1118 exprs = append(exprs, e)
1119 case promParser.ValueTypeNone, promParser.ValueTypeScalar, promParser.ValueTypeString:
1120 args = append(args, e.String())
1121 for _, s := range walkNode(expr, e) {
1122 indirect = append(indirect, Indirect{
1123 Src: s,
1124 Op: 0,
1125 Side: LHS,
1126 })
1127 }
1128 }
1129 }
1130
1131 for _, e := range exprs {
1132 for _, es := range walkNode(expr, e) {
1133 es.Type = FuncSource
1134 es.Operations = append(es.Operations, Operation{
1135 Operation: n.Func.Name,
1136 Node: n,
1137 Arguments: args,
1138 })
1139 es.Position = e.PositionRange()
1140 src = append(src, parsePromQLFunc(es, expr, n))
1141 }
1142 }
1143 for i := range src {
1144 src[i].Indirect = append(src[i].Indirect, indirect...)
1145 }
1146
1147 if len(src) == 0 {
1148 s := new(Source)
1149 s.Labels = map[string]LabelTransform{}
1150 s.Type = FuncSource
1151 s.Indirect = indirect
1152 s.Operations = Operations{
1153 {
1154 Operation: n.Func.Name,
1155 Node: n,
1156 Arguments: args,
1157 },
1158 }
1159 s.Position = n.PosRange
1160 src = append(src, parsePromQLFunc(s, expr, n))
1161 }
1162
1163 return src
1164}
1165
1166func fillFromMatching(vm *promParser.VectorMatching) *Fill {
1167 if vm.FillValues.LHS == nil && vm.FillValues.RHS == nil {
1168 return nil
1169 }
1170 var f Fill
1171 if vm.FillValues.LHS != nil {
1172 f.LHS = *vm.FillValues.LHS
1173 }
1174 if vm.FillValues.RHS != nil {
1175 f.RHS = *vm.FillValues.RHS
1176 }
1177 return &f
1178}
1179
1180func (s *Source) requireDurationExprFeatures(de *promParser.DurationExpr) {
1181 if de == nil {
1182 return
1183 }
1184 s.requireFeature("duration_expr", de.PositionRange())
1185 walkDurationExprFeatures(s, de)
1186}
1187
1188func (s *Source) requireStartOrEndFeature(expr string, startOrEnd promParser.ItemType, pos posrange.PositionRange) {
1189 switch startOrEnd {
1190 case promParser.START:
1191 s.requireFeature("start", FindFuncPosition(expr, pos, "start", nil))
1192 case promParser.END:
1193 s.requireFeature("end", FindFuncPosition(expr, pos, "end", nil))
1194 }
1195}
1196
1197func walkDurationExprFeatures(s *Source, de *promParser.DurationExpr) {
1198 switch de.Op {
1199 case promParser.RANGE:
1200 s.requireFeature("range", de.PositionRange())
1201 case promParser.STEP:
1202 s.requireFeature("step", de.PositionRange())
1203 }
1204 if lhs, ok := de.LHS.(*promParser.DurationExpr); ok {
1205 walkDurationExprFeatures(s, lhs)
1206 }
1207 if rhs, ok := de.RHS.(*promParser.DurationExpr); ok {
1208 walkDurationExprFeatures(s, rhs)
1209 }
1210}
1211
1212func parseBinOps(expr string, n *promParser.BinaryExpr) (src []*Source) {
1213 pos := n.PositionRange()
1214 src = make([]*Source, 0, 2)
1215 switch {
1216 // foo{} + 1
1217 // 1 + foo{}
1218 // foo{} > 1
1219 // 1 > foo{}
1220 case n.VectorMatching == nil:
1221 lhs := walkNode(expr, n.LHS)
1222 rhs := walkNode(expr, n.RHS)
1223 for _, ls := range lhs {
1224 for _, rs := range rhs {
1225 var side, other *Source
1226 switch {
1227 case ls.Returns == promParser.ValueTypeVector, ls.Returns == promParser.ValueTypeMatrix:
1228 // Use labels from LHS
1229 side = ls
1230 other = rs
1231 case rs.Returns == promParser.ValueTypeVector, rs.Returns == promParser.ValueTypeMatrix:
1232 // Use labels from RHS
1233 side = rs
1234 other = ls
1235 default:
1236 side = ls
1237 other = rs
1238 }
1239 // We walk RHS multiple times because we might have query like:
1240 // foo or bar > 0
1241 // Where LHS is [foo, bar] while RHS is [0]
1242 // And we end up with multiple sources:
1243 // - foo > 0
1244 // - bar > 0
1245 // As we walk RHS multiple times we might end up mutating the same
1246 // Source, and so to avoid that we need to clone it first.
1247 result := side.clone()
1248 indSide := RHS
1249 if other == ls {
1250 indSide = LHS
1251 }
1252 result.Indirect = append(result.Indirect, Indirect{
1253 Src: other,
1254 Op: n.Op,
1255 Side: indSide,
1256 })
1257 result.Condition, result.ReturnInfo.IsReturnBool = checkConditions(result, n.Op, n.ReturnBool)
1258 if ls.ReturnInfo.AlwaysReturns && rs.ReturnInfo.AlwaysReturns && ls.ReturnInfo.KnownReturn && rs.ReturnInfo.KnownReturn {
1259 // Both sides always return something
1260 result.ReturnInfo, result.DeadInfo = calculateStaticReturn(expr, ls, rs, n)
1261 }
1262 src = append(src, result)
1263 }
1264 }
1265
1266 // foo{} + bar{}
1267 // foo{} + on(...) bar{}
1268 // foo{} * ignoring(...) bar{}
1269 // foo{} / bar{}
1270 case n.VectorMatching.Card == promParser.CardOneToOne:
1271 rhs := walkNode(expr, n.RHS)
1272 for _, ls := range walkNode(expr, n.LHS) {
1273 if n.VectorMatching.On {
1274 ls.UsedLabels = appendToSlice(ls.UsedLabels, n.VectorMatching.MatchingLabels...)
1275 ls.checkIncludedLabels(expr, pos, n.VectorMatching.MatchingLabels)
1276 funcPos := FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.ON], []posrange.PositionRange{
1277 n.LHS.PositionRange(), n.RHS.PositionRange(),
1278 })
1279 ls.excludeAllLabels(
1280 expr,
1281 fmt.Sprintf(
1282 "Query is using %s vector matching with `%s(%s)`, only labels included inside `%s(...)` will be present on the results.",
1283 n.VectorMatching.Card, promParser.ItemTypeStr[promParser.ON], strings.Join(n.VectorMatching.MatchingLabels, ", "), promParser.ItemTypeStr[promParser.ON],
1284 ),
1285 funcPos,
1286 funcPos,
1287 n.VectorMatching.MatchingLabels,
1288 )
1289 } else {
1290 ls.useLabelsNotExcluded(n.VectorMatching.MatchingLabels)
1291 for _, name := range n.VectorMatching.MatchingLabels {
1292 ls.excludeLabel(
1293 fmt.Sprintf(
1294 "Query is using %s vector matching with `%s(%s)`, all labels included inside `%s(...)` will be removed on the results.",
1295 n.VectorMatching.Card, promParser.ItemTypeStr[promParser.IGNORING], strings.Join(n.VectorMatching.MatchingLabels, ", "), promParser.ItemTypeStr[promParser.IGNORING],
1296 ),
1297 findArgumentPosition(
1298 expr,
1299 FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.IGNORING], []posrange.PositionRange{
1300 n.LHS.PositionRange(), n.RHS.PositionRange(),
1301 }),
1302 name,
1303 ),
1304 name,
1305 )
1306 }
1307 for _, rs := range rhs {
1308 if ls.ReturnInfo.AlwaysReturns && rs.ReturnInfo.AlwaysReturns && ls.ReturnInfo.KnownReturn && rs.ReturnInfo.KnownReturn {
1309 // Both sides always return something
1310 ls.ReturnInfo, ls.DeadInfo = calculateStaticReturn(expr, ls, rs, n)
1311 }
1312 }
1313 }
1314 for _, rs := range rhs {
1315 deadLabels := rs.checkJoinedLabels(expr, n, rs)
1316 var deadInfo *DeadInfo
1317 if ok, s, p := canJoin(ls, rs, n.VectorMatching); !ok {
1318 deadInfo = &DeadInfo{
1319 Reason: s,
1320 Fragment: p,
1321 }
1322 }
1323 fill := fillFromMatching(n.VectorMatching)
1324 ls.Joins = append(ls.Joins, Join{
1325 Src: rs,
1326 Fill: fill,
1327 DeadInfo: deadInfo,
1328 DeadLabels: deadLabels,
1329 Op: n.Op,
1330 Depth: 0,
1331 MatchingLabels: n.VectorMatching.MatchingLabels,
1332 AddedLabels: nil,
1333 IsOn: n.VectorMatching.On,
1334 })
1335 if fill != nil {
1336 name := promParser.ItemTypeStr[promParser.FILL]
1337 ls.requireFeature(name, findBinOpsOperatorPosition(expr, n, name))
1338 }
1339 }
1340 ls.DeadLabels = append(ls.DeadLabels, ls.checkJoinedLabels(expr, n, ls)...)
1341 ls.excludeLabel("Binary operation between two vectors removes metric names.", pos, model.MetricNameLabel)
1342 ls.Condition, ls.ReturnInfo.IsReturnBool = checkConditions(ls, n.Op, n.ReturnBool)
1343 src = append(src, ls)
1344 }
1345
1346 // foo{} + on(...) group_right(...) bar{}
1347 // foo{} + ignoring(...) group_right(...) bar{}
1348 case n.VectorMatching.Card == promParser.CardOneToMany:
1349 lhs := walkNode(expr, n.LHS)
1350 for _, rs := range walkNode(expr, n.RHS) {
1351 rs.joinLabels(expr, pos, promParser.GROUP_RIGHT, n.VectorMatching.Include, []posrange.PositionRange{
1352 n.LHS.PositionRange(), n.RHS.PositionRange(),
1353 })
1354 // If we have:
1355 // foo * on(instance) group_left(a,b) bar{x="y"}
1356 // then only group_left() labels will be included.
1357 if n.VectorMatching.On {
1358 rs.UsedLabels = appendToSlice(rs.UsedLabels, n.VectorMatching.MatchingLabels...)
1359 rs.checkIncludedLabels(expr, pos, n.VectorMatching.MatchingLabels)
1360 for _, name := range n.VectorMatching.MatchingLabels {
1361 rs.includeLabel(
1362 expr,
1363 fmt.Sprintf(
1364 "Query is using %s vector matching with `on(%s)`, labels included inside `on(...)` will be present on the results.",
1365 n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "),
1366 ),
1367 findArgumentPosition(
1368 expr,
1369 FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.ON], []posrange.PositionRange{
1370 n.LHS.PositionRange(), n.RHS.PositionRange(),
1371 }),
1372 name,
1373 ),
1374 name,
1375 )
1376 }
1377 } else {
1378 rs.useLabelsNotExcluded(n.VectorMatching.MatchingLabels)
1379 }
1380 for _, ls := range lhs {
1381 deadLabels := findDeadIncludedLabels(
1382 ls,
1383 expr,
1384 FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.GROUP_RIGHT], []posrange.PositionRange{
1385 n.LHS.PositionRange(),
1386 n.RHS.PositionRange(),
1387 }),
1388 n.VectorMatching.Include,
1389 )
1390 rs.DeadLabels = append(rs.DeadLabels, ls.checkJoinedLabels(expr, n, rs)...)
1391 var deadInfo *DeadInfo
1392 if ok, s, p := canJoin(rs, ls, n.VectorMatching); !ok {
1393 deadInfo = &DeadInfo{
1394 Reason: s,
1395 Fragment: p,
1396 }
1397 }
1398 fill := fillFromMatching(n.VectorMatching)
1399 rs.Joins = append(rs.Joins, Join{
1400 Src: ls,
1401 Fill: fill,
1402 DeadInfo: deadInfo,
1403 DeadLabels: deadLabels,
1404 Op: n.Op,
1405 Depth: 0,
1406 MatchingLabels: n.VectorMatching.MatchingLabels,
1407 AddedLabels: n.VectorMatching.Include,
1408 IsOn: n.VectorMatching.On,
1409 })
1410 if fill != nil {
1411 name := promParser.ItemTypeStr[promParser.FILL]
1412 rs.requireFeature(name, findBinOpsOperatorPosition(expr, n, name))
1413 }
1414 }
1415 rs.excludeLabel("Binary operation between two vectors removes metric names.", pos, model.MetricNameLabel)
1416 rs.Condition, rs.ReturnInfo.IsReturnBool = checkConditions(rs, n.Op, n.ReturnBool)
1417 src = append(src, rs)
1418 }
1419
1420 // foo{} + on(...) group_left(...) bar{}
1421 // foo{} + ignoring(...) group_left(...) bar{}
1422 case n.VectorMatching.Card == promParser.CardManyToOne:
1423 rhs := walkNode(expr, n.RHS)
1424 for _, ls := range walkNode(expr, n.LHS) {
1425 ls.joinLabels(expr, pos, promParser.GROUP_LEFT, n.VectorMatching.Include, []posrange.PositionRange{
1426 n.LHS.PositionRange(), n.RHS.PositionRange(),
1427 })
1428 if n.VectorMatching.On {
1429 ls.UsedLabels = appendToSlice(ls.UsedLabels, n.VectorMatching.MatchingLabels...)
1430 ls.checkIncludedLabels(expr, pos, n.VectorMatching.MatchingLabels)
1431 for _, name := range n.VectorMatching.MatchingLabels {
1432 ls.includeLabel(
1433 expr,
1434 fmt.Sprintf(
1435 "Query is using %s vector matching with `on(%s)`, labels included inside `on(...)` will be present on the results.",
1436 n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "),
1437 ),
1438 findArgumentPosition(
1439 expr,
1440 FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.ON], []posrange.PositionRange{
1441 n.LHS.PositionRange(), n.RHS.PositionRange(),
1442 }),
1443 name,
1444 ),
1445 name,
1446 )
1447 }
1448 } else {
1449 ls.useLabelsNotExcluded(n.VectorMatching.MatchingLabels)
1450 }
1451 for _, rs := range rhs {
1452 deadLabels := findDeadIncludedLabels(
1453 rs,
1454 expr,
1455 FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.GROUP_LEFT], []posrange.PositionRange{
1456 n.LHS.PositionRange(),
1457 n.RHS.PositionRange(),
1458 }),
1459 n.VectorMatching.Include,
1460 )
1461 ls.DeadLabels = append(ls.DeadLabels, rs.checkJoinedLabels(expr, n, ls)...)
1462 var deadInfo *DeadInfo
1463 if ok, s, p := canJoin(ls, rs, n.VectorMatching); !ok {
1464 deadInfo = &DeadInfo{
1465 Reason: s,
1466 Fragment: p,
1467 }
1468 }
1469 fill := fillFromMatching(n.VectorMatching)
1470 ls.Joins = append(ls.Joins, Join{
1471 Src: rs,
1472 Fill: fill,
1473 DeadInfo: deadInfo,
1474 DeadLabels: deadLabels,
1475 Op: n.Op,
1476 Depth: 0,
1477 MatchingLabels: n.VectorMatching.MatchingLabels,
1478 AddedLabels: n.VectorMatching.Include,
1479 IsOn: n.VectorMatching.On,
1480 })
1481 if fill != nil {
1482 name := promParser.ItemTypeStr[promParser.FILL]
1483 ls.requireFeature(name, findBinOpsOperatorPosition(expr, n, name))
1484 }
1485 }
1486 ls.excludeLabel("Binary operation between two vectors removes metric names.", pos, model.MetricNameLabel)
1487 ls.Condition, ls.ReturnInfo.IsReturnBool = checkConditions(ls, n.Op, n.ReturnBool)
1488 src = append(src, ls)
1489 }
1490
1491 // foo{} and on(...) bar{}
1492 // foo{} and ignoring(...) bar{}
1493 // foo{} and bar{}
1494 // foo{} unless bar{}
1495 // foo{} or bar{}
1496 // foo{} or on(...) bar{}
1497 // foo{} or ignoring(...) bar{}
1498 //
1499 // 'foo or bar' means:
1500 // - take all foo{} results
1501 // - take all bar{} results
1502 // - remove any bar{} result if there's a foo{} result with same labels (except __name__)
1503 //
1504 // 'foo or on(a) bar' means we only compare 'a' label and remove any bar{} results with 'a' value
1505 // already provided by foo{}
1506 //
1507 // 'foo or ignoring(a) bar' means we ignore the 'a' label when comparing foo{} and bar{} results,
1508 // so (for example) bar{} results with 'a' labels present where foo{} doesn't have any 'a' label
1509 // will still be excluded if all other labels match.
1510 case n.VectorMatching.Card == promParser.CardManyToMany:
1511 var lhsCanBeEmpty bool // true if any of the LHS query can produce empty results.
1512 rhs := walkNode(expr, n.RHS)
1513 for _, ls := range walkNode(expr, n.LHS) {
1514 var rhsConditional bool
1515 // With many-to-many on/ignore is only used for matching series, it doesn't impact
1516 // returned labels.
1517 if n.VectorMatching.On {
1518 ls.UsedLabels = appendToSlice(ls.UsedLabels, n.VectorMatching.MatchingLabels...)
1519 ls.checkIncludedLabels(expr, pos, n.VectorMatching.MatchingLabels)
1520 for _, name := range n.VectorMatching.MatchingLabels {
1521 ls.includeLabel(
1522 expr,
1523 fmt.Sprintf(
1524 "Query is using %s vector matching with `on(%s)`, labels included inside `on(...)` will be present on the results if matched time series have them.",
1525 n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "),
1526 ),
1527 findArgumentPosition(
1528 expr,
1529 FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.ON], []posrange.PositionRange{
1530 n.LHS.PositionRange(), n.RHS.PositionRange(),
1531 }),
1532 name,
1533 ),
1534 name,
1535 )
1536 }
1537 } else {
1538 ls.useLabelsNotExcluded(n.VectorMatching.MatchingLabels)
1539 }
1540 if !ls.ReturnInfo.AlwaysReturns || ls.Condition.Present {
1541 lhsCanBeEmpty = true
1542 }
1543 for _, rs := range rhs {
1544 condition, _ := checkConditions(rs, n.Op, n.ReturnBool)
1545 if condition.Present {
1546 rhsConditional = true
1547 }
1548 var deadInfo *DeadInfo
1549 if ok, s, p := canJoin(ls, rs, n.VectorMatching); !ok {
1550 deadInfo = &DeadInfo{
1551 Reason: s,
1552 Fragment: p,
1553 }
1554 }
1555 switch {
1556 case n.Op == promParser.LUNLESS:
1557 if n.VectorMatching.On && len(n.VectorMatching.MatchingLabels) == 0 && rs.ReturnInfo.AlwaysReturns && !rs.Condition.Present {
1558 ls.DeadInfo = &DeadInfo{
1559 Reason: "This query will never return anything because the `unless` query always returns something.",
1560 Fragment: rs.Position,
1561 }
1562 }
1563 ls.Unless = append(ls.Unless, Unless{
1564 DeadInfo: deadInfo,
1565 Src: rs,
1566 MatchingLabels: n.VectorMatching.MatchingLabels,
1567 IsOn: n.VectorMatching.On,
1568 })
1569 case n.Op != promParser.LOR:
1570 ls.Joins = append(ls.Joins, Join{
1571 Src: rs,
1572 Fill: nil,
1573 DeadInfo: deadInfo,
1574 DeadLabels: nil,
1575 Op: n.Op,
1576 Depth: 0,
1577 MatchingLabels: n.VectorMatching.MatchingLabels,
1578 AddedLabels: nil,
1579 IsOn: n.VectorMatching.On,
1580 })
1581 }
1582 ls.DeadLabels = append(ls.DeadLabels, rs.checkJoinedLabels(expr, n, ls)...)
1583 }
1584 if n.Op == promParser.LAND && rhsConditional {
1585 ls.Condition = Condition{
1586 Present: true,
1587 Op: n.Op,
1588 Value: 0,
1589 KnownValue: false,
1590 }
1591 }
1592 src = append(src, ls)
1593 }
1594 if n.Op == promParser.LOR {
1595 for _, rs := range rhs {
1596 // If LHS can NOT be empty then RHS is dead code.
1597 if !lhsCanBeEmpty {
1598 rs.DeadInfo = &DeadInfo{
1599 Reason: "The left hand side always returns something and so the right hand side is never used.",
1600 Fragment: rs.Position,
1601 }
1602 }
1603 src = append(src, rs)
1604 }
1605 }
1606 }
1607 return src
1608}
1609
1610func checkConditions(s *Source, op promParser.ItemType, isBool bool) (condition Condition, isReturnBool bool) {
1611 if !s.Condition.Present && isBool {
1612 isReturnBool = isBool
1613 }
1614 if s.Condition.Present {
1615 return s.Condition, isReturnBool
1616 }
1617 if !op.IsComparisonOperator() {
1618 return condition, isReturnBool
1619 }
1620 condition = Condition{
1621 Present: true,
1622 Op: op,
1623 Value: 0,
1624 KnownValue: false,
1625 }
1626 for _, j := range s.Joins {
1627 if j.Op != op || j.Src == nil || !j.Src.ReturnInfo.KnownReturn {
1628 continue
1629 }
1630 condition.Value = j.Src.ReturnInfo.ReturnedNumber
1631 condition.KnownValue = true
1632 return condition, isReturnBool
1633 }
1634 for _, ind := range s.Indirect {
1635 if ind.Op != op || ind.Src == nil || !ind.Src.ReturnInfo.KnownReturn {
1636 continue
1637 }
1638 condition.Value = ind.Src.ReturnInfo.ReturnedNumber
1639 condition.KnownValue = true
1640 return condition, isReturnBool
1641 }
1642 return condition, isReturnBool
1643}
1644
1645func canJoin(ls, rs *Source, vm *promParser.VectorMatching) (bool, string, posrange.PositionRange) {
1646 var side string
1647 if vm.Card == promParser.CardOneToMany {
1648 side = "left"
1649 } else {
1650 side = "right"
1651 }
1652
1653 switch {
1654 case vm.On && len(vm.MatchingLabels) == 0: // ls on() unless rs
1655 return true, "", posrange.PositionRange{}
1656 case vm.On: // ls on(...) unless rs
1657 for _, name := range vm.MatchingLabels {
1658 if ls.CanHaveLabel(name) && !rs.CanHaveLabel(name) {
1659 reason, fragment := rs.LabelExcludeReason(name)
1660 return false, fmt.Sprintf("The %s hand side will never be matched because it doesn't have the `%s` label from `on(...)`. %s",
1661 side, name, reason), fragment
1662 }
1663 }
1664 default: // ls unless rs
1665 for name, l := range ls.Labels {
1666 if l.Kind != GuaranteedLabel {
1667 continue
1668 }
1669 if slices.Contains(vm.MatchingLabels, name) {
1670 continue
1671 }
1672 if ls.CanHaveLabel(name) && !rs.CanHaveLabel(name) {
1673 otherSide := "left"
1674 if side == "left" {
1675 otherSide = "right"
1676 }
1677 reason, fragment := rs.LabelExcludeReason(name)
1678 return false, fmt.Sprintf("The %s hand side will never be matched because it doesn't have the `%s` label while the %s hand side will. %s",
1679 side, name, otherSide, reason), fragment
1680 }
1681 }
1682 }
1683 return true, "", posrange.PositionRange{}
1684}
1685
1686func describeDeadCode(expr string, ls, rs *Source, op *promParser.BinaryExpr, match string) *DeadInfo {
1687 var lse, rse string
1688 if ls.ReturnInfo.LogicalExpr != "" {
1689 lse = ls.ReturnInfo.LogicalExpr
1690 } else {
1691 lse = GetQueryFragment(expr, ls.ReturnInfo.ValuePosition)
1692 }
1693 if rs.ReturnInfo.LogicalExpr != "" {
1694 rse = rs.ReturnInfo.LogicalExpr
1695 } else {
1696 rse = GetQueryFragment(expr, rs.ReturnInfo.ValuePosition)
1697 }
1698
1699 cmpPrefix := fmt.Sprintf("`%s %s %s` always evaluates to", lse, op.Op, rse)
1700
1701 var cmpSuffix string
1702 if op.ReturnBool {
1703 cmpSuffix = "and uses the `bool` modifier which means it will always return 0"
1704 } else {
1705 cmpSuffix = "which is not possible, so it will never return anything."
1706 }
1707 return &DeadInfo{
1708 Reason: fmt.Sprintf(
1709 "%s `%s %s %s` %s",
1710 cmpPrefix,
1711 strconv.FormatFloat(ls.ReturnInfo.ReturnedNumber, 'f', -1, 64),
1712 match,
1713 strconv.FormatFloat(rs.ReturnInfo.ReturnedNumber, 'f', -1, 64),
1714 cmpSuffix,
1715 ),
1716 Fragment: ls.Position,
1717 }
1718}
1719
1720func calculateStaticReturn(expr string, ls, rs *Source, op *promParser.BinaryExpr) (ret ReturnInfo, deadinfo *DeadInfo) {
1721 ret = ls.ReturnInfo
1722 switch op.Op {
1723 case promParser.EQLC:
1724 if ls.ReturnInfo.ReturnedNumber != rs.ReturnInfo.ReturnedNumber {
1725 deadinfo = describeDeadCode(expr, ls, rs, op, "==")
1726 }
1727 case promParser.NEQ:
1728 if ls.ReturnInfo.ReturnedNumber == rs.ReturnInfo.ReturnedNumber {
1729 deadinfo = describeDeadCode(expr, ls, rs, op, "!=")
1730 }
1731 case promParser.LTE:
1732 if ls.ReturnInfo.ReturnedNumber > rs.ReturnInfo.ReturnedNumber {
1733 deadinfo = describeDeadCode(expr, ls, rs, op, "<=")
1734 }
1735 case promParser.LSS:
1736 if ls.ReturnInfo.ReturnedNumber >= rs.ReturnInfo.ReturnedNumber {
1737 deadinfo = describeDeadCode(expr, ls, rs, op, "<")
1738 }
1739 case promParser.GTE:
1740 if ls.ReturnInfo.ReturnedNumber < rs.ReturnInfo.ReturnedNumber {
1741 deadinfo = describeDeadCode(expr, ls, rs, op, ">=")
1742 }
1743 case promParser.GTR:
1744 if ls.ReturnInfo.ReturnedNumber <= rs.ReturnInfo.ReturnedNumber {
1745 deadinfo = describeDeadCode(expr, ls, rs, op, ">")
1746 }
1747 case promParser.ADD:
1748 ret.ReturnedNumber = ls.ReturnInfo.ReturnedNumber + rs.ReturnInfo.ReturnedNumber
1749 ret.LogicalExpr = formatDesc(expr, ls, rs, "+")
1750 case promParser.SUB:
1751 ret.ReturnedNumber = ls.ReturnInfo.ReturnedNumber - rs.ReturnInfo.ReturnedNumber
1752 ret.LogicalExpr = formatDesc(expr, ls, rs, "-")
1753 case promParser.MUL:
1754 ret.ReturnedNumber = ls.ReturnInfo.ReturnedNumber * rs.ReturnInfo.ReturnedNumber
1755 ret.LogicalExpr = formatDesc(expr, ls, rs, "*")
1756 case promParser.DIV:
1757 ret.ReturnedNumber = ls.ReturnInfo.ReturnedNumber / rs.ReturnInfo.ReturnedNumber
1758 ret.LogicalExpr = formatDesc(expr, ls, rs, "/")
1759 case promParser.MOD:
1760 ret.ReturnedNumber = math.Mod(ls.ReturnInfo.ReturnedNumber, rs.ReturnInfo.ReturnedNumber)
1761 ret.LogicalExpr = formatDesc(expr, ls, rs, "%")
1762 case promParser.POW:
1763 ret.ReturnedNumber = math.Pow(ls.ReturnInfo.ReturnedNumber, rs.ReturnInfo.ReturnedNumber)
1764 ret.LogicalExpr = formatDesc(expr, ls, rs, "^")
1765 }
1766 return ret, deadinfo
1767}
1768
1769func formatDesc(expr string, ls, rs *Source, op string) string {
1770 var lse, rse string
1771 if ls.ReturnInfo.LogicalExpr != "" {
1772 lse = ls.ReturnInfo.LogicalExpr
1773 } else {
1774 lse = GetQueryFragment(expr, ls.ReturnInfo.ValuePosition)
1775 }
1776 if rs.ReturnInfo.LogicalExpr != "" {
1777 rse = rs.ReturnInfo.LogicalExpr
1778 } else {
1779 rse = GetQueryFragment(expr, rs.ReturnInfo.ValuePosition)
1780 }
1781 return lse + " " + op + " " + rse
1782}
1783