cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
c86a0506fa570d8eec77d66f9acd76319459ec00

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/utils/source.go

1269lines · modecode

1package utils
2
3import (
4 "fmt"
5 "math"
6 "regexp"
7 "slices"
8 "strconv"
9 "strings"
10
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 SourceType uint8
19
20const (
21 UnknownSource SourceType = iota
22 NumberSource
23 StringSource
24 SelectorSource
25 FuncSource
26 AggregateSource
27)
28
29// Used for test snapshots.
30func (st SourceType) MarshalYAML() (any, error) {
31 var name string
32 switch st { // nolint: exhaustive
33 case NumberSource:
34 name = "number"
35 case StringSource:
36 name = "string"
37 case SelectorSource:
38 name = "selector"
39 case FuncSource:
40 name = "function"
41 case AggregateSource:
42 name = "aggregation"
43 }
44 return name, nil
45}
46
47type LabelPromiseType uint8
48
49const (
50 ImpossibleLabel LabelPromiseType = iota
51 PossibleLabel
52 GuaranteedLabel
53)
54
55// Used for test snapshots.
56func (lpt LabelPromiseType) MarshalYAML() (any, error) {
57 var name string
58 switch lpt {
59 case ImpossibleLabel:
60 name = "excluded"
61 case PossibleLabel:
62 name = "included"
63 case GuaranteedLabel:
64 name = "guaranteed"
65 }
66 return name, nil
67}
68
69type LabelTransform struct {
70 Reason string
71 Kind LabelPromiseType
72 Fragment posrange.PositionRange
73}
74
75type DeadInfo struct {
76 Reason string
77 Fragment posrange.PositionRange
78}
79
80type ReturnInfo struct {
81 LogicalExpr string
82 ValuePosition posrange.PositionRange
83 ReturnedNumber float64 // If AlwaysReturns=true this is the number that's returned
84 AlwaysReturns bool // True if this source always returns results.
85 KnownReturn bool // True if we always know the return value.
86 IsReturnBool bool // True if this source uses the 'bool' modifier.
87}
88
89type SourceOperation struct {
90 Node promParser.Node
91 Operation string
92}
93
94// Used for test snapshots.
95func (so SourceOperation) MarshalYAML() (any, error) {
96 return map[string]any{
97 "op": so.Operation,
98 "node": fmt.Sprintf("[%T] %s", so.Node, so.Node.String()),
99 }, nil
100}
101
102type SourceOperations []SourceOperation
103
104func MostOuterOperation[T promParser.Node](s Source) (T, bool) {
105 for i := len(s.Operations) - 1; i >= 0; i-- {
106 op := s.Operations[i]
107 if o, ok := op.Node.(T); ok {
108 return o, true
109 }
110 }
111 return *new(T), false
112}
113
114func newSource() Source {
115 return Source{ // nolint: exhaustruct
116 Labels: map[string]LabelTransform{},
117 }
118}
119
120type Join struct {
121 On []string
122 Ignoring []string
123 Src Source // The source we're joining with.
124 Op promParser.ItemType // The binary operation used for this join.
125 Depth int // Zero if this is a direct join, non-zero otherwise. sum(foo * bar) would be in-direct join.
126}
127
128type Unless struct {
129 On []string
130 Ignoring []string
131 Src Source
132}
133
134type Source struct {
135 Labels map[string]LabelTransform
136 DeadInfo *DeadInfo
137 Returns promParser.ValueType
138 Operations SourceOperations
139 Joins []Join // Any other sources this source joins with.
140 Unless []Unless // Any other sources this source is suppressed by.
141 ReturnInfo ReturnInfo
142 Position posrange.PositionRange
143 Type SourceType
144 FixedLabels bool // Labels are fixed and only allowed labels can be present.
145 IsConditional bool // True if this source is guarded by 'foo > 5' or other condition.
146}
147
148func (s Source) Operation() string {
149 if len(s.Operations) > 0 {
150 return s.Operations[len(s.Operations)-1].Operation
151 }
152 return ""
153}
154
155func (s Source) CanHaveLabel(name string) bool {
156 if v, ok := s.Labels[name]; ok {
157 if v.Kind == ImpossibleLabel {
158 return false
159 }
160 if v.Kind == PossibleLabel || v.Kind == GuaranteedLabel {
161 return true
162 }
163 }
164 return !s.FixedLabels
165}
166
167func (s Source) TransformedLabels(kinds ...LabelPromiseType) []string {
168 names := make([]string, 0, len(s.Labels))
169 for name, l := range s.Labels {
170 if slices.Contains(kinds, l.Kind) {
171 names = append(names, name)
172 }
173 }
174 return names
175}
176
177func (s Source) LabelExcludeReason(name string) (string, posrange.PositionRange) {
178 if l, ok := s.Labels[name]; ok && l.Kind == ImpossibleLabel {
179 return l.Reason, l.Fragment
180 }
181 return s.Labels[""].Reason, s.Labels[""].Fragment
182}
183
184func (s *Source) excludeAllLabels(reason string, fragment posrange.PositionRange, except []string) {
185 // Everything that was included until now but will be removed needs an explicit stamp to mark it as gone.
186 for name, l := range s.Labels {
187 if slices.Contains(except, name) {
188 continue
189 }
190 if l.Kind == PossibleLabel || l.Kind == GuaranteedLabel {
191 s.Labels[name] = LabelTransform{
192 Kind: ImpossibleLabel,
193 Reason: reason,
194 Fragment: fragment,
195 }
196 }
197 }
198 // Mark except labels as possible, unless they are already guaranteed.
199 for _, name := range except {
200 if l, ok := s.Labels[name]; ok && l.Kind == GuaranteedLabel {
201 continue
202 }
203
204 // We have grouping labels set, if they are possible mark them as such, if not mark as impossible.
205 if s.CanHaveLabel(name) {
206 s.Labels[name] = LabelTransform{
207 Kind: PossibleLabel,
208 Reason: reason,
209 Fragment: fragment,
210 }
211 } else {
212 r, f := s.LabelExcludeReason(name)
213 s.Labels[name] = LabelTransform{
214 Kind: ImpossibleLabel,
215 Reason: r,
216 Fragment: f,
217 }
218 }
219
220 }
221 s.Labels[""] = LabelTransform{
222 Kind: ImpossibleLabel,
223 Reason: reason,
224 Fragment: fragment,
225 }
226 s.FixedLabels = true
227}
228
229func (s *Source) excludeLabel(reason string, fragment posrange.PositionRange, names ...string) {
230 for _, name := range names {
231 s.Labels[name] = LabelTransform{
232 Kind: ImpossibleLabel,
233 Reason: reason,
234 Fragment: fragment,
235 }
236 }
237}
238
239func (s *Source) includeLabel(reason string, fragment posrange.PositionRange, names ...string) {
240 for _, name := range names {
241 if l, ok := s.Labels[name]; ok && l.Kind == GuaranteedLabel {
242 continue
243 }
244 s.Labels[name] = LabelTransform{
245 Kind: PossibleLabel,
246 Reason: reason,
247 Fragment: fragment,
248 }
249 }
250}
251
252func (s *Source) guaranteeLabel(reason string, fragment posrange.PositionRange, names ...string) {
253 for _, name := range names {
254 s.Labels[name] = LabelTransform{
255 Kind: GuaranteedLabel,
256 Reason: reason,
257 Fragment: fragment,
258 }
259 }
260}
261
262type Visitor func(s Source, j *Join, u *Unless)
263
264func innerWalk(fn Visitor, s Source, j *Join, u *Unless) {
265 fn(s, j, u)
266 for _, j := range s.Joins {
267 innerWalk(fn, j.Src, &j, nil)
268 }
269 for _, u := range s.Unless {
270 innerWalk(fn, u.Src, nil, &u)
271 }
272}
273
274func (s Source) WalkSources(fn Visitor) {
275 innerWalk(fn, s, nil, nil)
276}
277
278func LabelsSource(expr string, node promParser.Node) (src []Source) {
279 return walkNode(expr, node)
280}
281
282func walkNode(expr string, node promParser.Node) (src []Source) {
283 s := newSource()
284 switch n := node.(type) {
285 case *promParser.AggregateExpr:
286 src = append(src, walkAggregation(expr, n)...)
287
288 case *promParser.BinaryExpr:
289 src = append(src, parseBinOps(expr, n)...)
290
291 case *promParser.Call:
292 src = append(src, parseCall(expr, n)...)
293
294 case *promParser.MatrixSelector:
295 for _, s := range walkNode(expr, n.VectorSelector) {
296 s.Returns = promParser.ValueTypeMatrix
297 src = append(src, s)
298 }
299
300 case *promParser.SubqueryExpr:
301 src = append(src, walkNode(expr, n.Expr)...)
302
303 case *promParser.NumberLiteral:
304 s.Type = NumberSource
305 s.Returns = promParser.ValueTypeScalar
306 s.ReturnInfo.KnownReturn = true
307 s.ReturnInfo.ReturnedNumber = n.Val
308 s.ReturnInfo.AlwaysReturns = true
309 s.ReturnInfo.ValuePosition = n.PosRange
310 s.excludeAllLabels("This query returns a number value with no labels.", n.PosRange, nil)
311 s.Position = n.PosRange
312 src = append(src, s)
313
314 case *promParser.ParenExpr:
315 src = append(src, walkNode(expr, n.Expr)...)
316
317 case *promParser.StringLiteral:
318 s.Type = StringSource
319 s.Returns = promParser.ValueTypeString
320 s.ReturnInfo.AlwaysReturns = true
321 s.excludeAllLabels("This query returns a string value with no labels.", n.PosRange, nil)
322 s.Position = n.PosRange
323 src = append(src, s)
324
325 case *promParser.UnaryExpr:
326 src = append(src, walkNode(expr, n.Expr)...)
327
328 case *promParser.StepInvariantExpr:
329 // Not possible to get this from the parser.
330
331 case *promParser.VectorSelector:
332 s.Type = SelectorSource
333 s.Returns = promParser.ValueTypeVector
334 s.Operations = append(s.Operations, SourceOperation{
335 Operation: "",
336 Node: n,
337 })
338 s.guaranteeLabel(
339 "Query will only return series where these labels are present.",
340 n.PosRange,
341 labelsFromSelectors(guaranteedLabelsMatches, n)...,
342 )
343 for _, name := range labelsWithEmptyValueSelector(n) {
344 s.excludeLabel(
345 fmt.Sprintf("Query uses `{%s=\"\"}` selector which will filter out any time series with the `%s` label set.", name, name),
346 n.PosRange,
347 name,
348 )
349 }
350 s.Position = n.PosRange
351 src = append(src, s)
352
353 default:
354 // unhandled type
355 }
356 return src
357}
358
359func appendToSlice(dst []string, values ...string) []string {
360 for _, v := range values {
361 if !slices.Contains(dst, v) {
362 dst = append(dst, v)
363 }
364 }
365 return dst
366}
367
368func labelsFromSelectors(matches []labels.MatchType, selector *promParser.VectorSelector) (names []string) {
369 if selector == nil {
370 return nil
371 }
372 // Any label used in positive filters is gurnateed to be present.
373 for _, lm := range selector.LabelMatchers {
374 if lm.Name == labels.MetricName {
375 continue
376 }
377 if !slices.Contains(matches, lm.Type) {
378 continue
379 }
380 names = appendToSlice(names, lm.Name)
381 }
382 return names
383}
384
385func labelsWithEmptyValueSelector(selector *promParser.VectorSelector) (names []string) {
386 for _, lm := range selector.LabelMatchers {
387 if lm.Name == labels.MetricName {
388 continue
389 }
390 if lm.Type == labels.MatchEqual && lm.Value == "" {
391 names = appendToSlice(names, lm.Name)
392 }
393 }
394 return names
395}
396
397func GetQueryFragment(expr string, pos posrange.PositionRange) string {
398 return expr[pos.Start:pos.End]
399}
400
401func walkAggregation(expr string, n *promParser.AggregateExpr) (src []Source) {
402 s := newSource()
403 switch n.Op {
404 case promParser.SUM:
405 for _, s = range parseAggregation(expr, n) {
406 s.Operations = append(s.Operations, SourceOperation{
407 Operation: "sum",
408 Node: n,
409 })
410 if n.Without || !slices.Contains(n.Grouping, labels.MetricName) {
411 s.excludeLabel("Aggregation removes metric name.", n.PosRange, labels.MetricName)
412 }
413 src = append(src, s)
414 }
415 case promParser.MIN:
416 for _, s = range parseAggregation(expr, n) {
417 s.Operations = append(s.Operations, SourceOperation{
418 Operation: "min",
419 Node: n,
420 })
421 if n.Without || !slices.Contains(n.Grouping, labels.MetricName) {
422 s.excludeLabel("Aggregation removes metric name.", n.PosRange, labels.MetricName)
423 }
424 src = append(src, s)
425 }
426 case promParser.MAX:
427 for _, s = range parseAggregation(expr, n) {
428 s.Operations = append(s.Operations, SourceOperation{
429 Operation: "max",
430 Node: n,
431 })
432 if n.Without || !slices.Contains(n.Grouping, labels.MetricName) {
433 s.excludeLabel("Aggregation removes metric name.", n.PosRange, labels.MetricName)
434 }
435 src = append(src, s)
436 }
437 case promParser.AVG:
438 for _, s = range parseAggregation(expr, n) {
439 s.Operations = append(s.Operations, SourceOperation{
440 Operation: "avg",
441 Node: n,
442 })
443 if n.Without || !slices.Contains(n.Grouping, labels.MetricName) {
444 s.excludeLabel("Aggregation removes metric name.", n.PosRange, labels.MetricName)
445 }
446 src = append(src, s)
447 }
448 case promParser.GROUP:
449 for _, s = range parseAggregation(expr, n) {
450 s.Operations = append(s.Operations, SourceOperation{
451 Operation: "group",
452 Node: n,
453 })
454 if n.Without || !slices.Contains(n.Grouping, labels.MetricName) {
455 s.excludeLabel("Aggregation removes metric name.", n.PosRange, labels.MetricName)
456 }
457 src = append(src, s)
458 }
459 case promParser.STDDEV:
460 for _, s = range parseAggregation(expr, n) {
461 s.Operations = append(s.Operations, SourceOperation{
462 Operation: "stddev",
463 Node: n,
464 })
465 if n.Without || !slices.Contains(n.Grouping, labels.MetricName) {
466 s.excludeLabel("Aggregation removes metric name.", n.PosRange, labels.MetricName)
467 }
468 src = append(src, s)
469 }
470 case promParser.STDVAR:
471 for _, s = range parseAggregation(expr, n) {
472 s.Operations = append(s.Operations, SourceOperation{
473 Operation: "stdvar",
474 Node: n,
475 })
476 if n.Without || !slices.Contains(n.Grouping, labels.MetricName) {
477 s.excludeLabel("Aggregation removes metric name.", n.PosRange, labels.MetricName)
478 }
479 src = append(src, s)
480 }
481 case promParser.COUNT:
482 for _, s = range parseAggregation(expr, n) {
483 s.Operations = append(s.Operations, SourceOperation{
484 Operation: "count",
485 Node: n,
486 })
487 if n.Without || !slices.Contains(n.Grouping, labels.MetricName) {
488 s.excludeLabel("Aggregation removes metric name.", n.PosRange, labels.MetricName)
489 }
490 src = append(src, s)
491 }
492 case promParser.COUNT_VALUES:
493 for _, s = range parseAggregation(expr, n) {
494 s.Operations = append(s.Operations, SourceOperation{
495 Operation: "count_values",
496 Node: n,
497 })
498 // Param is the label to store the count value in.
499 s.guaranteeLabel(
500 "This label will be added to the results by the count_values() call.",
501 n.PosRange,
502 n.Param.(*promParser.StringLiteral).Val,
503 )
504 if n.Without || !slices.Contains(n.Grouping, labels.MetricName) {
505 s.excludeLabel("Aggregation removes metric name.", n.PosRange, labels.MetricName)
506 }
507 src = append(src, s)
508 }
509 case promParser.QUANTILE:
510 for _, s = range parseAggregation(expr, n) {
511 s.Operations = append(s.Operations, SourceOperation{
512 Operation: "quantile",
513 Node: n,
514 })
515 if n.Without || !slices.Contains(n.Grouping, labels.MetricName) {
516 s.excludeLabel("Aggregation removes metric name.", n.PosRange, labels.MetricName)
517 }
518 src = append(src, s)
519 }
520 case promParser.TOPK:
521 for _, s = range walkNode(expr, n.Expr) {
522 for i := range s.Joins {
523 s.Joins[i].Depth++
524 }
525 s.Type = AggregateSource
526 s.Operations = append(s.Operations, SourceOperation{
527 Operation: "topk",
528 Node: n,
529 })
530 src = append(src, s)
531 }
532 case promParser.BOTTOMK:
533 for _, s = range walkNode(expr, n.Expr) {
534 for i := range s.Joins {
535 s.Joins[i].Depth++
536 }
537 s.Type = AggregateSource
538 s.Operations = append(s.Operations, SourceOperation{
539 Operation: "bottomk",
540 Node: n,
541 })
542 src = append(src, s)
543 }
544 /*
545 TODO these are experimental and promParser.EnableExperimentalFunctions must be set to true to enable parsing of these.
546 case promParser.LIMITK:
547 s = walkNode(expr, n.Expr)
548 s.Type = AggregateSource
549 s.Operation = "limitk"
550 case promParser.LIMIT_RATIO:
551 s = walkNode(expr, n.Expr)
552 s.Type = AggregateSource
553 s.Operation = "limit_ratio"
554 */
555 }
556 return src
557}
558
559func parseAggregation(expr string, n *promParser.AggregateExpr) (src []Source) {
560 s := newSource()
561 for _, s = range walkNode(expr, n.Expr) {
562 // If we have sum(foo * bar) then we start with:
563 // - source: foo
564 // joins: bar
565 // Then we parse aggregation and end up with:
566 // - source: sum(foo)
567 // joins: bar
568 // Which is misleading and wrong, so we bump depth value to know about it.
569 for i := range s.Joins {
570 s.Joins[i].Depth++
571 }
572
573 if n.Without {
574 s.excludeLabel(
575 fmt.Sprintf("Query is using aggregation with `without(%s)`, all labels included inside `without(...)` will be removed from the results.",
576 strings.Join(n.Grouping, ", ")),
577 FindPosition(expr, n.PosRange, "without"),
578 n.Grouping...,
579 )
580 } else {
581 if len(n.Grouping) == 0 {
582 s.excludeAllLabels(
583 "Query is using aggregation that removes all labels.",
584 FindPosition(expr, n.PosRange, "sum"),
585 nil,
586 )
587 } else {
588 s.excludeAllLabels(
589 fmt.Sprintf("Query is using aggregation with `by(%s)`, only labels included inside `by(...)` will be present on the results.",
590 strings.Join(n.Grouping, ", ")),
591 FindPosition(expr, n.PosRange, "by"),
592 n.Grouping,
593 )
594 }
595 }
596 s.Type = AggregateSource
597 s.Returns = promParser.ValueTypeVector
598 src = append(src, s)
599 }
600 return src
601}
602
603func parsePromQLFunc(s Source, expr string, n *promParser.Call) Source {
604 switch n.Func.Name {
605 case "abs", "sgn", "acos", "acosh", "asin", "asinh", "atan", "atanh", "cos", "cosh", "sin", "sinh", "tan", "tanh":
606 // No change to labels.
607 s.Returns = promParser.ValueTypeVector
608 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
609 s.guaranteeLabel(
610 "Query will only return series where these labels are present.",
611 n.PosRange,
612 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
613 )
614
615 case "ceil", "floor", "round":
616 // No change to labels.
617 s.Returns = promParser.ValueTypeVector
618 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
619 s.guaranteeLabel(
620 "Query will only return series where these labels are present.",
621 n.PosRange,
622 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
623 )
624
625 case "changes", "resets":
626 // No change to labels.
627 s.Returns = promParser.ValueTypeVector
628 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
629 s.guaranteeLabel(
630 "Query will only return series where these labels are present.",
631 n.PosRange,
632 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
633 )
634
635 case "clamp", "clamp_max", "clamp_min":
636 // No change to labels.
637 s.Returns = promParser.ValueTypeVector
638 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
639 s.guaranteeLabel(
640 "Query will only return series where these labels are present.",
641 n.PosRange,
642 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
643 )
644
645 case "absent", "absent_over_time":
646 s.Returns = promParser.ValueTypeVector
647 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
648 names := labelsFromSelectors([]labels.MatchType{labels.MatchEqual}, vs)
649 s.excludeAllLabels(
650 fmt.Sprintf(`The [%s()](https://prometheus.io/docs/prometheus/latest/querying/functions/#%s) function is used to check if provided query doesn't match any time series.
651You will only get any results back if the metric selector you pass doesn't match anything.
652Since there are no matching time series there are also no labels. If some time series is missing you cannot read its labels.
653This means that the only labels you can get back from absent call are the ones you pass to it.
654If 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.`,
655 n.Func.Name, n.Func.Name),
656 FindPosition(expr, n.PosRange, n.Func.Name),
657 names,
658 )
659 s.guaranteeLabel(
660 fmt.Sprintf("All labels passed to %s() call will be present on the results if the query doesn't match anything.", n.Func.Name),
661 n.PosRange,
662 names...,
663 )
664
665 case "avg_over_time", "count_over_time", "last_over_time", "max_over_time", "min_over_time", "present_over_time", "quantile_over_time", "stddev_over_time", "stdvar_over_time", "sum_over_time":
666 // No change to labels.
667 s.Returns = promParser.ValueTypeVector
668 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
669 s.guaranteeLabel(
670 "Query will only return series where these labels are present.",
671 n.PosRange,
672 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
673 )
674
675 case "days_in_month", "day_of_month", "day_of_week", "day_of_year", "hour", "minute", "month", "year":
676 s.Returns = promParser.ValueTypeVector
677 // No labels if we don't pass any arguments.
678 // Otherwise no change to labels.
679 if len(n.Args) == 0 {
680 s.ReturnInfo.AlwaysReturns = true
681 s.excludeAllLabels(
682 fmt.Sprintf("Calling `%s()` with no arguments will return an empty time series with no labels.",
683 n.Func.Name),
684 n.PosRange,
685 nil,
686 )
687 } else {
688 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
689 s.guaranteeLabel(
690 "Query will only return series where these labels are present.",
691 n.PosRange,
692 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
693 )
694 }
695
696 case "deg", "rad", "ln", "log10", "log2", "sqrt", "exp":
697 // No change to labels.
698 s.Returns = promParser.ValueTypeVector
699 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
700 s.guaranteeLabel(
701 "Query will only return series where these labels are present.",
702 n.PosRange,
703 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
704 )
705 case "delta", "idelta", "increase", "deriv", "irate", "rate":
706 // No change to labels.
707 s.Returns = promParser.ValueTypeVector
708 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
709 s.guaranteeLabel(
710 "Query will only return series where these labels are present.",
711 n.PosRange,
712 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
713 )
714
715 case "histogram_avg", "histogram_count", "histogram_sum", "histogram_stddev", "histogram_stdvar", "histogram_fraction", "histogram_quantile":
716 // No change to labels.
717 s.Returns = promParser.ValueTypeVector
718 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
719 s.guaranteeLabel(
720 "Query will only return series where these labels are present.",
721 n.PosRange,
722 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
723 )
724
725 case "holt_winters", "predict_linear":
726 // No change to labels.
727 s.Returns = promParser.ValueTypeVector
728 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
729 s.guaranteeLabel(
730 "Query will only return series where these labels are present.",
731 n.PosRange,
732 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
733 )
734 case "label_replace", "label_join":
735 // One label added to the results.
736 s.Returns = promParser.ValueTypeVector
737 s.guaranteeLabel(
738 fmt.Sprintf("This label will be added to the result by %s() call.", n.Func.Name),
739 n.PosRange,
740 n.Args[1].(*promParser.StringLiteral).Val,
741 )
742
743 case "pi":
744 s.Returns = promParser.ValueTypeScalar
745 s.ReturnInfo.AlwaysReturns = true
746 s.ReturnInfo.KnownReturn = true
747 s.ReturnInfo.ReturnedNumber = math.Pi
748 s.ReturnInfo.ValuePosition = n.PosRange
749 s.excludeAllLabels(
750 fmt.Sprintf("Calling `%s()` will return a scalar value with no labels.", n.Func.Name),
751 n.PosRange,
752 nil,
753 )
754
755 case "scalar":
756 s.Returns = promParser.ValueTypeScalar
757 s.ReturnInfo.AlwaysReturns = true
758 s.excludeAllLabels(
759 fmt.Sprintf("Calling `%s()` will return a scalar value with no labels.", n.Func.Name),
760 FindPosition(expr, n.PositionRange(), n.Func.Name),
761 nil,
762 )
763
764 case "sort", "sort_desc":
765 // No change to labels.
766 s.Returns = promParser.ValueTypeVector
767
768 case "time":
769 s.Returns = promParser.ValueTypeScalar
770 s.ReturnInfo.AlwaysReturns = true
771 s.ReturnInfo.ValuePosition = n.PosRange
772 s.excludeAllLabels(
773 fmt.Sprintf("Calling `%s()` will return a scalar value with no labels.", n.Func.Name),
774 n.PosRange,
775 nil,
776 )
777
778 case "timestamp":
779 // No change to labels.
780 s.Returns = promParser.ValueTypeVector
781 vs, _ := MostOuterOperation[*promParser.VectorSelector](s)
782 s.guaranteeLabel(
783 "Query will only return series where these labels are present.",
784 n.PosRange,
785 labelsFromSelectors(guaranteedLabelsMatches, vs)...,
786 )
787
788 case "vector":
789 s.Returns = promParser.ValueTypeVector
790 s.ReturnInfo.AlwaysReturns = true
791 s.ReturnInfo.ValuePosition = n.PosRange
792 for _, vs := range walkNode(expr, n.Args[0]) {
793 if vs.ReturnInfo.KnownReturn {
794 s.ReturnInfo.ReturnedNumber = vs.ReturnInfo.ReturnedNumber
795 s.ReturnInfo.KnownReturn = true
796 }
797 }
798 s.excludeAllLabels(
799 fmt.Sprintf("Calling `%s()` will return a vector value with no labels.", n.Func.Name),
800 FindPosition(expr, n.PosRange, n.Func.Name),
801 nil,
802 )
803
804 default:
805 // Unsupported function
806 return Source{} // nolint: exhaustruct
807 }
808 return s
809}
810
811func parseCall(expr string, n *promParser.Call) (src []Source) {
812 var vt promParser.ValueType
813 for i, e := range n.Args {
814 if i >= len(n.Func.ArgTypes) {
815 vt = n.Func.ArgTypes[len(n.Func.ArgTypes)-1]
816 } else {
817 vt = n.Func.ArgTypes[i]
818 }
819
820 switch vt {
821 case promParser.ValueTypeVector, promParser.ValueTypeMatrix:
822 for _, es := range walkNode(expr, e) {
823 es.Type = FuncSource
824 es.Operations = append(es.Operations, SourceOperation{
825 Operation: n.Func.Name,
826 Node: n,
827 })
828 es.Position = e.PositionRange()
829 src = append(src, parsePromQLFunc(es, expr, n))
830 }
831 case promParser.ValueTypeNone, promParser.ValueTypeScalar, promParser.ValueTypeString:
832 }
833 }
834
835 if len(src) == 0 {
836 s := Source{ // nolint: exhaustruct
837 Labels: map[string]LabelTransform{},
838 Type: FuncSource,
839 Operations: SourceOperations{
840 {
841 Operation: n.Func.Name,
842 Node: n,
843 },
844 },
845 Position: n.PosRange,
846 }
847 src = append(src, parsePromQLFunc(s, expr, n))
848 }
849
850 return src
851}
852
853func parseBinOps(expr string, n *promParser.BinaryExpr) (src []Source) {
854 switch {
855 // foo{} + 1
856 // 1 + foo{}
857 // foo{} > 1
858 // 1 > foo{}
859 case n.VectorMatching == nil:
860 lhs := walkNode(expr, n.LHS)
861 rhs := walkNode(expr, n.RHS)
862 for _, ls := range lhs {
863 ls.IsConditional, ls.ReturnInfo.IsReturnBool = checkConditions(ls, n.Op, n.ReturnBool)
864 for _, rs := range rhs {
865 rs.IsConditional, rs.ReturnInfo.IsReturnBool = checkConditions(rs, n.Op, n.ReturnBool)
866 var side Source
867 switch {
868 case ls.Returns == promParser.ValueTypeVector, ls.Returns == promParser.ValueTypeMatrix:
869 // Use labels from LHS
870 side = ls
871 case rs.Returns == promParser.ValueTypeVector, rs.Returns == promParser.ValueTypeMatrix:
872 // Use labels from RHS
873 side = rs
874 default:
875 side = ls
876 }
877 if ls.ReturnInfo.AlwaysReturns && rs.ReturnInfo.AlwaysReturns && ls.ReturnInfo.KnownReturn && rs.ReturnInfo.KnownReturn {
878 // Both sides always return something
879 side.ReturnInfo, side.DeadInfo = calculateStaticReturn(expr, ls, rs, n)
880 }
881 src = append(src, side)
882 }
883 }
884
885 // foo{} + bar{}
886 // foo{} + on(...) bar{}
887 // foo{} + ignoring(...) bar{}
888 // foo{} / bar{}
889 case n.VectorMatching.Card == promParser.CardOneToOne:
890 rhs := walkNode(expr, n.RHS)
891 for _, ls := range walkNode(expr, n.LHS) {
892 if n.VectorMatching.On {
893 ls.excludeAllLabels(
894 fmt.Sprintf(
895 "Query is using %s vector matching with `on(%s)`, only labels included inside `on(...)` will be present on the results.",
896 n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "),
897 ),
898 FindPosition(expr, n.PositionRange(), "on"),
899 n.VectorMatching.MatchingLabels,
900 )
901 } else {
902 ls.excludeLabel(
903 fmt.Sprintf(
904 "Query is using %s vector matching with `ignoring(%s)`, all labels included inside `ignoring(...)` will be removed on the results.",
905 n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "),
906 ),
907 FindPosition(expr, n.PositionRange(), "ignoring"),
908 n.VectorMatching.MatchingLabels...,
909 )
910 for _, rs := range rhs {
911 rs.IsConditional, rs.ReturnInfo.IsReturnBool = checkConditions(rs, n.Op, n.ReturnBool)
912 if ls.ReturnInfo.AlwaysReturns && rs.ReturnInfo.AlwaysReturns && ls.ReturnInfo.KnownReturn && rs.ReturnInfo.KnownReturn {
913 // Both sides always return something
914 ls.ReturnInfo, ls.DeadInfo = calculateStaticReturn(expr, ls, rs, n)
915 }
916 }
917 }
918 for _, rs := range rhs {
919 if ok, s, pos := canJoin(ls, rs, n.VectorMatching); !ok {
920 rs.DeadInfo = &DeadInfo{
921 Reason: s,
922 Fragment: pos,
923 }
924 }
925 ls.Joins = append(ls.Joins, Join{
926 Src: rs,
927 Op: n.Op,
928 Depth: 0,
929 On: onLabels(n.VectorMatching),
930 Ignoring: ignoringLabels(n.VectorMatching),
931 })
932 }
933 ls.IsConditional, ls.ReturnInfo.IsReturnBool = checkConditions(ls, n.Op, n.ReturnBool)
934 src = append(src, ls)
935 }
936
937 // foo{} + on(...) group_right(...) bar{}
938 // foo{} + ignoring(...) group_right(...) bar{}
939 case n.VectorMatching.Card == promParser.CardOneToMany:
940 lhs := walkNode(expr, n.LHS)
941 for _, rs := range walkNode(expr, n.RHS) {
942 rs.includeLabel(
943 fmt.Sprintf(
944 "Query is using %s vector matching with `group_right(%s)`, all labels included inside `group_right(...)` will be include on the results.",
945 n.VectorMatching.Card, strings.Join(n.VectorMatching.Include, ", "),
946 ),
947 FindPosition(expr, n.PositionRange(), "group_right"),
948 n.VectorMatching.Include...,
949 )
950 // If we have:
951 // foo * on(instance) group_left(a,b) bar{x="y"}
952 // then only group_left() labels will be included.
953 if n.VectorMatching.On {
954 rs.includeLabel(
955 fmt.Sprintf(
956 "Query is using %s vector matching with `on(%s)`, labels included inside `on(...)` will be present on the results.",
957 n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "),
958 ),
959 FindPosition(expr, n.PositionRange(), "on"),
960 n.VectorMatching.MatchingLabels...,
961 )
962 }
963 for _, ls := range lhs {
964 if ok, s, pos := canJoin(rs, ls, n.VectorMatching); !ok {
965 ls.DeadInfo = &DeadInfo{
966 Reason: s,
967 Fragment: pos,
968 }
969 }
970 rs.Joins = append(rs.Joins, Join{
971 Src: ls,
972 Op: n.Op,
973 Depth: 0,
974 On: onLabels(n.VectorMatching),
975 Ignoring: ignoringLabels(n.VectorMatching),
976 })
977 }
978 rs.IsConditional, rs.ReturnInfo.IsReturnBool = checkConditions(rs, n.Op, n.ReturnBool)
979 src = append(src, rs)
980 }
981
982 // foo{} + on(...) group_left(...) bar{}
983 // foo{} + ignoring(...) group_left(...) bar{}
984 case n.VectorMatching.Card == promParser.CardManyToOne:
985 rhs := walkNode(expr, n.RHS)
986 for _, ls := range walkNode(expr, n.LHS) {
987 ls.includeLabel(
988 fmt.Sprintf(
989 "Query is using %s vector matching with `group_left(%s)`, all labels included inside `group_left(...)` will be include on the results.",
990 n.VectorMatching.Card, strings.Join(n.VectorMatching.Include, ", "),
991 ),
992 FindPosition(expr, n.PositionRange(), "group_left"),
993 n.VectorMatching.Include...,
994 )
995 if n.VectorMatching.On {
996 ls.includeLabel(
997 fmt.Sprintf(
998 "Query is using %s vector matching with `on(%s)`, labels included inside `on(...)` will be present on the results.",
999 n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "),
1000 ),
1001 FindPosition(expr, n.PositionRange(), "on"),
1002 n.VectorMatching.MatchingLabels...,
1003 )
1004 }
1005 for _, rs := range rhs {
1006 if ok, s, pos := canJoin(ls, rs, n.VectorMatching); !ok {
1007 rs.DeadInfo = &DeadInfo{
1008 Reason: s,
1009 Fragment: pos,
1010 }
1011 }
1012 ls.Joins = append(ls.Joins, Join{
1013 Src: rs,
1014 Op: n.Op,
1015 Depth: 0,
1016 On: onLabels(n.VectorMatching),
1017 Ignoring: ignoringLabels(n.VectorMatching),
1018 })
1019 }
1020 ls.IsConditional, ls.ReturnInfo.IsReturnBool = checkConditions(ls, n.Op, n.ReturnBool)
1021 src = append(src, ls)
1022 }
1023
1024 // foo{} and on(...) bar{}
1025 // foo{} and ignoring(...) bar{}
1026 // foo{} unless bar{}
1027 case n.VectorMatching.Card == promParser.CardManyToMany:
1028 var lhsCanBeEmpty bool // true if any of the LHS query can produce empty results.
1029 rhs := walkNode(expr, n.RHS)
1030 for _, ls := range walkNode(expr, n.LHS) {
1031 var rhsConditional bool
1032 if n.VectorMatching.On {
1033 ls.includeLabel(
1034 fmt.Sprintf(
1035 "Query is using %s vector matching with `on(%s)`, labels included inside `on(...)` will be present on the results.",
1036 n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "),
1037 ),
1038 FindPosition(expr, n.PositionRange(), "on"),
1039 n.VectorMatching.MatchingLabels...,
1040 )
1041 }
1042 if !ls.ReturnInfo.AlwaysReturns || ls.IsConditional {
1043 lhsCanBeEmpty = true
1044 }
1045 for _, rs := range rhs {
1046 isConditional, _ := checkConditions(rs, n.Op, n.ReturnBool)
1047 if isConditional {
1048 rhsConditional = true
1049 }
1050 if ok, s, pos := canJoin(ls, rs, n.VectorMatching); !ok {
1051 rs.DeadInfo = &DeadInfo{
1052 Reason: s,
1053 Fragment: pos,
1054 }
1055 }
1056 switch {
1057 case n.Op == promParser.LUNLESS:
1058 if n.VectorMatching.On && len(n.VectorMatching.MatchingLabels) == 0 && rs.ReturnInfo.AlwaysReturns && !rs.IsConditional {
1059 ls.DeadInfo = &DeadInfo{
1060 Reason: "This query will never return anything because the `unless` query always returns something.",
1061 Fragment: rs.Position,
1062 }
1063 }
1064 ls.Unless = append(ls.Unless, Unless{
1065 Src: rs,
1066 On: onLabels(n.VectorMatching),
1067 Ignoring: ignoringLabels(n.VectorMatching),
1068 })
1069 case n.Op != promParser.LOR:
1070 ls.Joins = append(ls.Joins, Join{
1071 Src: rs,
1072 Op: n.Op,
1073 Depth: 0,
1074 On: onLabels(n.VectorMatching),
1075 Ignoring: ignoringLabels(n.VectorMatching),
1076 })
1077 }
1078 }
1079 if n.Op == promParser.LAND && rhsConditional {
1080 ls.IsConditional = true
1081 }
1082 src = append(src, ls)
1083 }
1084 if n.Op == promParser.LOR {
1085 for _, rs := range rhs {
1086 // If LHS can NOT be empty then RHS is dead code.
1087 if !lhsCanBeEmpty {
1088 rs.DeadInfo = &DeadInfo{
1089 Reason: "The left hand side always returs something and so the right hand side is never used.",
1090 Fragment: rs.Position,
1091 }
1092 }
1093 src = append(src, rs)
1094 }
1095 }
1096 }
1097 return src
1098}
1099
1100func onLabels(vm *promParser.VectorMatching) []string {
1101 if vm.On {
1102 return vm.MatchingLabels
1103 }
1104 return nil
1105}
1106
1107func ignoringLabels(vm *promParser.VectorMatching) []string {
1108 if !vm.On {
1109 return vm.MatchingLabels
1110 }
1111 return nil
1112}
1113
1114func checkConditions(s Source, op promParser.ItemType, isBool bool) (isConditional, isReturnBool bool) {
1115 if !s.IsConditional && isBool {
1116 isReturnBool = isBool
1117 }
1118 if s.IsConditional {
1119 isConditional = s.IsConditional
1120 } else {
1121 isConditional = op.IsComparisonOperator()
1122 }
1123 return isConditional, isReturnBool
1124}
1125
1126func canJoin(ls, rs Source, vm *promParser.VectorMatching) (bool, string, posrange.PositionRange) {
1127 var side string
1128 if vm.Card == promParser.CardOneToMany {
1129 side = "left"
1130 } else {
1131 side = "right"
1132 }
1133
1134 switch {
1135 case vm.On && len(vm.MatchingLabels) == 0: // ls on() unless rs
1136 return true, "", posrange.PositionRange{}
1137 case vm.On: // ls on(...) unless rs
1138 for _, name := range vm.MatchingLabels {
1139 if ls.CanHaveLabel(name) && !rs.CanHaveLabel(name) {
1140 reason, fragment := rs.LabelExcludeReason(name)
1141 return false, fmt.Sprintf("The %s hand side will never be matched because it doesn't have the `%s` label from `on(...)`. %s",
1142 side, name, reason), fragment
1143 }
1144 }
1145 default: // ls unless rs
1146 for name, l := range ls.Labels {
1147 if l.Kind != GuaranteedLabel {
1148 continue
1149 }
1150 if ls.CanHaveLabel(name) && !rs.CanHaveLabel(name) {
1151 reason, fragment := rs.LabelExcludeReason(name)
1152 return false, fmt.Sprintf("The %s hand side will never be matched because it doesn't have the `%s` label while the left hand side will. %s",
1153 side, name, reason), fragment
1154 }
1155 }
1156 }
1157 return true, "", posrange.PositionRange{}
1158}
1159
1160func describeDeadCode(expr string, ls, rs Source, op *promParser.BinaryExpr, match string) *DeadInfo {
1161 var lse, rse string
1162 if ls.ReturnInfo.LogicalExpr != "" {
1163 lse = ls.ReturnInfo.LogicalExpr
1164 } else {
1165 lse = GetQueryFragment(expr, ls.ReturnInfo.ValuePosition)
1166 }
1167 if rs.ReturnInfo.LogicalExpr != "" {
1168 rse = rs.ReturnInfo.LogicalExpr
1169 } else {
1170 rse = GetQueryFragment(expr, rs.ReturnInfo.ValuePosition)
1171 }
1172
1173 cmpPrefix := fmt.Sprintf("`%s %s %s` always evaluates to", lse, op.Op, rse)
1174
1175 var cmpSuffix string
1176 if op.ReturnBool {
1177 cmpSuffix = "and uses the `bool` modifier which means it will always return 0"
1178 } else {
1179 cmpSuffix = "which is not possible, so it will never return anything."
1180 }
1181 return &DeadInfo{
1182 Reason: fmt.Sprintf(
1183 "%s `%s %s %s` %s",
1184 cmpPrefix,
1185 strconv.FormatFloat(ls.ReturnInfo.ReturnedNumber, 'f', -1, 64),
1186 match,
1187 strconv.FormatFloat(rs.ReturnInfo.ReturnedNumber, 'f', -1, 64),
1188 cmpSuffix,
1189 ),
1190 Fragment: ls.Position,
1191 }
1192}
1193
1194func calculateStaticReturn(expr string, ls, rs Source, op *promParser.BinaryExpr) (ret ReturnInfo, deadinfo *DeadInfo) {
1195 ret = ls.ReturnInfo
1196 switch op.Op {
1197 case promParser.EQLC:
1198 if ls.ReturnInfo.ReturnedNumber != rs.ReturnInfo.ReturnedNumber {
1199 deadinfo = describeDeadCode(expr, ls, rs, op, "==")
1200 }
1201 case promParser.NEQ:
1202 if ls.ReturnInfo.ReturnedNumber == rs.ReturnInfo.ReturnedNumber {
1203 deadinfo = describeDeadCode(expr, ls, rs, op, "!=")
1204 }
1205 case promParser.LTE:
1206 if ls.ReturnInfo.ReturnedNumber > rs.ReturnInfo.ReturnedNumber {
1207 deadinfo = describeDeadCode(expr, ls, rs, op, "<=")
1208 }
1209 case promParser.LSS:
1210 if ls.ReturnInfo.ReturnedNumber >= rs.ReturnInfo.ReturnedNumber {
1211 deadinfo = describeDeadCode(expr, ls, rs, op, "<")
1212 }
1213 case promParser.GTE:
1214 if ls.ReturnInfo.ReturnedNumber < rs.ReturnInfo.ReturnedNumber {
1215 deadinfo = describeDeadCode(expr, ls, rs, op, ">=")
1216 }
1217 case promParser.GTR:
1218 if ls.ReturnInfo.ReturnedNumber <= rs.ReturnInfo.ReturnedNumber {
1219 deadinfo = describeDeadCode(expr, ls, rs, op, ">")
1220 }
1221 case promParser.ADD:
1222 ret.ReturnedNumber = ls.ReturnInfo.ReturnedNumber + rs.ReturnInfo.ReturnedNumber
1223 ret.LogicalExpr = formatDesc(expr, ls, rs, "+")
1224 case promParser.SUB:
1225 ret.ReturnedNumber = ls.ReturnInfo.ReturnedNumber - rs.ReturnInfo.ReturnedNumber
1226 ret.LogicalExpr = formatDesc(expr, ls, rs, "-")
1227 case promParser.MUL:
1228 ret.ReturnedNumber = ls.ReturnInfo.ReturnedNumber * rs.ReturnInfo.ReturnedNumber
1229 ret.LogicalExpr = formatDesc(expr, ls, rs, "*")
1230 case promParser.DIV:
1231 ret.ReturnedNumber = ls.ReturnInfo.ReturnedNumber / rs.ReturnInfo.ReturnedNumber
1232 ret.LogicalExpr = formatDesc(expr, ls, rs, "/")
1233 case promParser.MOD:
1234 ret.ReturnedNumber = math.Mod(ls.ReturnInfo.ReturnedNumber, rs.ReturnInfo.ReturnedNumber)
1235 ret.LogicalExpr = formatDesc(expr, ls, rs, "%")
1236 case promParser.POW:
1237 ret.ReturnedNumber = math.Pow(ls.ReturnInfo.ReturnedNumber, rs.ReturnInfo.ReturnedNumber)
1238 ret.LogicalExpr = formatDesc(expr, ls, rs, "^")
1239 }
1240 return ret, deadinfo
1241}
1242
1243func formatDesc(expr string, ls, rs Source, op string) string {
1244 var lse, rse string
1245 if ls.ReturnInfo.LogicalExpr != "" {
1246 lse = ls.ReturnInfo.LogicalExpr
1247 } else {
1248 lse = GetQueryFragment(expr, ls.ReturnInfo.ValuePosition)
1249 }
1250 if rs.ReturnInfo.LogicalExpr != "" {
1251 rse = rs.ReturnInfo.LogicalExpr
1252 } else {
1253 rse = GetQueryFragment(expr, rs.ReturnInfo.ValuePosition)
1254 }
1255 return lse + " " + op + " " + rse
1256}
1257
1258// FIXME sum() on ().
1259func FindPosition(expr string, within posrange.PositionRange, fn string) posrange.PositionRange {
1260 re := regexp.MustCompile("(?i)(" + regexp.QuoteMeta(fn) + ")[ \n\t]*\\(")
1261 idx := re.FindStringSubmatchIndex(GetQueryFragment(expr, within))
1262 if idx == nil {
1263 return within
1264 }
1265 return posrange.PositionRange{
1266 Start: within.Start + posrange.Pos(idx[0]),
1267 End: within.Start + posrange.Pos(idx[1]-1),
1268 }
1269}
1270