cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.74.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/utils/source.go

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