cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.74.6

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/parser/utils/source.go

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