Skip to content

Commit bc26d9f

Browse files
authored
Merge pull request #1632 from cloudflare/issue-1631
Track labels usage in label_join and label_replace
2 parents e3c758a + 34d2a1f commit bc26d9f

File tree

7 files changed

+1097
-4663
lines changed

7 files changed

+1097
-4663
lines changed

docs/changelog.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## v0.78.0
4+
5+
### Fixed
6+
7+
- Fixed false positive reports from [promql/impossible](checks/promql/impossible.md) when
8+
`label_join` or `label_replace` is used - [#1631](https://github.com/cloudflare/pint/issues/1631).
9+
310
## v0.77.1
411

512
### Fixed

internal/checks/promql_impossible_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,30 @@ func TestImpossibleCheck(t *testing.T) {
657657
prometheus: newSimpleProm,
658658
problems: true,
659659
},
660+
{
661+
description: "label_join",
662+
content: `
663+
- record: foo
664+
expr: |
665+
group by (cluster, namespace, workload, workload_type, pod) (
666+
label_join(
667+
label_join(
668+
group by (cluster, namespace, job_name, pod) (
669+
label_join(
670+
kube_pod_owner{job="kube-state-metrics", owner_kind="Job"}
671+
, "job_name", "", "owner_name")
672+
)
673+
* on (cluster, namespace, job_name) group_left(owner_kind, owner_name)
674+
group by (cluster, namespace, job_name, owner_kind, owner_name) (
675+
kube_job_owner{job="kube-state-metrics", owner_kind!="Pod", owner_kind!=""}
676+
)
677+
, "workload", "", "owner_name")
678+
, "workload_type", "", "owner_kind")
679+
)
680+
`,
681+
checker: newImpossibleCheck,
682+
prometheus: newSimpleProm,
683+
},
660684
}
661685

662686
runTests(t, testCases)

internal/checks/promql_impossible_test.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2467,6 +2467,11 @@
24672467

24682468
---
24692469

2470+
[TestImpossibleCheck/label_join - 1]
2471+
[]
2472+
2473+
---
2474+
24702475
[TestImpossibleCheck/match_on_group_right_label - 1]
24712476
[]
24722477

internal/checks/rule_dependency_test.snap

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262

6363
---
6464

65+
[TestRuleDependencyCheck/ignores_rules_with_invalid_queries - 1]
66+
[]
67+
68+
---
69+
6570
[TestRuleDependencyCheck/ignores_rules_with_syntax_errors - 1]
6671
[]
6772

@@ -216,8 +221,3 @@
216221
anchor: 1
217222

218223
---
219-
220-
[TestRuleDependencyCheck/ignores_rules_with_invalid_queries - 1]
221-
[]
222-
223-
---

internal/parser/source/source.go

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -164,18 +164,23 @@ type Unless struct {
164164
}
165165

166166
type Source struct {
167-
Labels map[string]LabelTransform
168-
DeadInfo *DeadInfo
169-
DeadLabels []DeadLabel
170-
Returns promParser.ValueType
171-
Operations Operations
172-
Joins []Join // Any other sources this source joins with.
173-
Unless []Unless // Any other sources this source is suppressed by.
174-
ReturnInfo ReturnInfo
175-
Position posrange.PositionRange
176-
Type Type
177-
FixedLabels bool // Labels are fixed and only allowed labels can be present.
178-
IsConditional bool // True if this source is guarded by 'foo > 5' or other condition.
167+
Labels map[string]LabelTransform `yaml:"labels,omitempty"`
168+
DeadInfo *DeadInfo `yaml:"deadInfo,omitempty"`
169+
DeadLabels []DeadLabel `yaml:"deadLabels,omitempty"`
170+
Returns promParser.ValueType `yaml:"returns"`
171+
Operations Operations `yaml:"operations,omitempty"`
172+
// Any other sources this source joins with.
173+
Joins []Join `yaml:"joins,omitempty"`
174+
// Any other sources this source is suppressed by.
175+
Unless []Unless `yaml:"unless,omitempty"`
176+
UsedLabels []string `yaml:"usedLabels,omitempty"`
177+
ReturnInfo ReturnInfo `yaml:"returnInfo,omitempty"`
178+
Position posrange.PositionRange `yaml:"position"`
179+
Type Type `yaml:"type"`
180+
// Labels are fixed and only allowed labels can be present.
181+
FixedLabels bool `yaml:"fixedLabels,omitempty"`
182+
// True if this source is guarded by 'foo > 5' or other condition.
183+
IsConditional bool `yaml:"isConditional,omitempty"`
179184
}
180185

181186
func (s Source) Operation() string {
@@ -228,7 +233,11 @@ func (s *Source) excludeAllLabels(expr, reason string, fragment, allFragment pos
228233
}
229234
}
230235
}
236+
s.UsedLabels = slices.DeleteFunc(s.UsedLabels, func(name string) bool {
237+
return !slices.Contains(except, name)
238+
})
231239
// Mark except labels as possible, unless they are already guaranteed.
240+
s.UsedLabels = appendToSlice(s.UsedLabels, except...)
232241
for _, name := range except {
233242
if l, ok := s.Labels[name]; ok && l.Kind == GuaranteedLabel {
234243
continue
@@ -265,6 +274,9 @@ func (s *Source) excludeLabel(reason string, fragment posrange.PositionRange, na
265274
Reason: reason,
266275
Fragment: fragment,
267276
}
277+
s.UsedLabels = slices.DeleteFunc(s.UsedLabels, func(s string) bool {
278+
return s == name
279+
})
268280
}
269281

270282
func (s *Source) joinLabels(expr string, within posrange.PositionRange, op promParser.ItemType, names []string, outside []posrange.PositionRange) {
@@ -336,18 +348,6 @@ func (s *Source) checkIncludedLabels(expr string, pos posrange.PositionRange, na
336348
}
337349
}
338350

339-
func (s *Source) hasJoinUsingLabel(name string) bool {
340-
for _, j := range s.Joins {
341-
if j.IsOn && slices.Contains(j.MatchingLabels, name) {
342-
return true
343-
}
344-
if !j.IsOn && !slices.Contains(j.MatchingLabels, name) {
345-
return true
346-
}
347-
}
348-
return false
349-
}
350-
351351
func (s *Source) checkAggregationLabels(expr string, n *promParser.AggregateExpr) {
352352
var pos posrange.PositionRange
353353
switch {
@@ -361,8 +361,7 @@ func (s *Source) checkAggregationLabels(expr string, n *promParser.AggregateExpr
361361

362362
for _, j := range s.Joins {
363363
for _, name := range j.AddedLabels {
364-
if s.hasJoinUsingLabel(name) {
365-
// This label is used for some other join for our source.
364+
if slices.Contains(s.UsedLabels, name) {
366365
continue
367366
}
368367
if !n.Without && slices.Contains(n.Grouping, name) {
@@ -440,6 +439,24 @@ func (s *Source) checkJoinedLabels(expr string, n *promParser.BinaryExpr, dst So
440439
return dead
441440
}
442441

442+
func (s *Source) useLabelsNotExcluded(exluded []string) {
443+
// Iterating over a map can yield labels in different order each time
444+
// so append labels to an extra slice, sort it, and then append the
445+
// sorted reults to UsedLabels.
446+
// Without this tests might show a diff sometimes.
447+
toAdd := make([]string, 0, len(s.Labels))
448+
for name, lt := range s.Labels {
449+
if lt.Kind == ImpossibleLabel {
450+
continue
451+
}
452+
if !slices.Contains(exluded, name) {
453+
toAdd = appendToSlice(toAdd, name)
454+
}
455+
}
456+
slices.Sort(toAdd)
457+
s.UsedLabels = appendToSlice(s.UsedLabels, toAdd...)
458+
}
459+
443460
type Visitor func(s Source, j *Join, u *Unless)
444461

445462
func innerWalk(fn Visitor, s Source, j *Join, u *Unless) {
@@ -701,6 +718,7 @@ func parseAggregation(expr string, n *promParser.AggregateExpr) (src []Source) {
701718
nil,
702719
)
703720
} else {
721+
s.UsedLabels = appendToSlice(s.UsedLabels, n.Grouping...)
704722
s.checkIncludedLabels(
705723
expr,
706724
FindFuncPosition(expr, n.PosRange, promParser.ItemTypeStr[promParser.BY], nil),
@@ -859,14 +877,28 @@ If you're hoping to get instance specific labels this way and alert when some ta
859877
n.PosRange,
860878
labelsFromSelectors(guaranteedLabelsMatches, vs)...,
861879
)
862-
case "label_replace", "label_join":
880+
case "label_join":
881+
// One label added to the results.
882+
// label_join(v instant-vector, dst_label string, separator string, src_label_1 string, src_label_2 string, ...)
883+
s.Returns = promParser.ValueTypeVector
884+
s.guaranteeLabel(
885+
fmt.Sprintf("This label will be added to the result by %s() call.", n.Func.Name),
886+
n.PosRange,
887+
n.Args[1].(*promParser.StringLiteral).Val,
888+
)
889+
for i := 3; i < len(n.Args); i++ {
890+
s.UsedLabels = appendToSlice(s.UsedLabels, n.Args[i].(*promParser.StringLiteral).Val)
891+
}
892+
case "label_replace":
863893
// One label added to the results.
894+
// label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)
864895
s.Returns = promParser.ValueTypeVector
865896
s.guaranteeLabel(
866897
fmt.Sprintf("This label will be added to the result by %s() call.", n.Func.Name),
867898
n.PosRange,
868899
n.Args[1].(*promParser.StringLiteral).Val,
869900
)
901+
s.UsedLabels = appendToSlice(s.UsedLabels, n.Args[3].(*promParser.StringLiteral).Val)
870902

871903
case "pi":
872904
s.Returns = promParser.ValueTypeScalar
@@ -1040,6 +1072,7 @@ func parseBinOps(expr string, n *promParser.BinaryExpr) (src []Source) {
10401072
rhs := walkNode(expr, n.RHS)
10411073
for _, ls := range walkNode(expr, n.LHS) {
10421074
if n.VectorMatching.On {
1075+
ls.UsedLabels = appendToSlice(ls.UsedLabels, n.VectorMatching.MatchingLabels...)
10431076
ls.checkIncludedLabels(expr, pos, n.VectorMatching.MatchingLabels)
10441077
funcPos := FindFuncPosition(expr, pos, promParser.ItemTypeStr[promParser.ON], []posrange.PositionRange{
10451078
n.LHS.PositionRange(), n.RHS.PositionRange(),
@@ -1055,6 +1088,7 @@ func parseBinOps(expr string, n *promParser.BinaryExpr) (src []Source) {
10551088
n.VectorMatching.MatchingLabels,
10561089
)
10571090
} else {
1091+
ls.useLabelsNotExcluded(n.VectorMatching.MatchingLabels)
10581092
for _, name := range n.VectorMatching.MatchingLabels {
10591093
ls.excludeLabel(
10601094
fmt.Sprintf(
@@ -1114,6 +1148,7 @@ func parseBinOps(expr string, n *promParser.BinaryExpr) (src []Source) {
11141148
// foo * on(instance) group_left(a,b) bar{x="y"}
11151149
// then only group_left() labels will be included.
11161150
if n.VectorMatching.On {
1151+
rs.UsedLabels = appendToSlice(rs.UsedLabels, n.VectorMatching.MatchingLabels...)
11171152
rs.checkIncludedLabels(expr, pos, n.VectorMatching.MatchingLabels)
11181153
for _, name := range n.VectorMatching.MatchingLabels {
11191154
rs.includeLabel(
@@ -1132,6 +1167,8 @@ func parseBinOps(expr string, n *promParser.BinaryExpr) (src []Source) {
11321167
name,
11331168
)
11341169
}
1170+
} else {
1171+
rs.useLabelsNotExcluded(n.VectorMatching.MatchingLabels)
11351172
}
11361173
for _, ls := range lhs {
11371174
ls.checkIncludedLabels(
@@ -1172,6 +1209,7 @@ func parseBinOps(expr string, n *promParser.BinaryExpr) (src []Source) {
11721209
n.LHS.PositionRange(), n.RHS.PositionRange(),
11731210
})
11741211
if n.VectorMatching.On {
1212+
ls.UsedLabels = appendToSlice(ls.UsedLabels, n.VectorMatching.MatchingLabels...)
11751213
ls.checkIncludedLabels(expr, pos, n.VectorMatching.MatchingLabels)
11761214
for _, name := range n.VectorMatching.MatchingLabels {
11771215
ls.includeLabel(
@@ -1190,6 +1228,8 @@ func parseBinOps(expr string, n *promParser.BinaryExpr) (src []Source) {
11901228
name,
11911229
)
11921230
}
1231+
} else {
1232+
ls.useLabelsNotExcluded(n.VectorMatching.MatchingLabels)
11931233
}
11941234
for _, rs := range rhs {
11951235
rs.checkIncludedLabels(
@@ -1225,12 +1265,16 @@ func parseBinOps(expr string, n *promParser.BinaryExpr) (src []Source) {
12251265
// foo{} and ignoring(...) bar{}
12261266
// foo{} and bar{}
12271267
// foo{} unless bar{}
1268+
// foo{} or bar{}
1269+
// foo{} or on(...) bar{}
1270+
// foo{} or ignoring(...) bar{}
12281271
case n.VectorMatching.Card == promParser.CardManyToMany:
12291272
var lhsCanBeEmpty bool // true if any of the LHS query can produce empty results.
12301273
rhs := walkNode(expr, n.RHS)
12311274
for _, ls := range walkNode(expr, n.LHS) {
12321275
var rhsConditional bool
12331276
if n.VectorMatching.On {
1277+
ls.UsedLabels = appendToSlice(ls.UsedLabels, n.VectorMatching.MatchingLabels...)
12341278
ls.checkIncludedLabels(expr, pos, n.VectorMatching.MatchingLabels)
12351279
for _, name := range n.VectorMatching.MatchingLabels {
12361280
ls.includeLabel(
@@ -1249,6 +1293,10 @@ func parseBinOps(expr string, n *promParser.BinaryExpr) (src []Source) {
12491293
name,
12501294
)
12511295
}
1296+
} else if n.Op != promParser.LOR {
1297+
// Mark labels not set inside ignoring(...) as used, but:
1298+
// - skip 'foo OR bar' - OR doesn't do label matching, it works on any results
1299+
ls.useLabelsNotExcluded(n.VectorMatching.MatchingLabels)
12521300
}
12531301
if !ls.ReturnInfo.AlwaysReturns || ls.IsConditional {
12541302
lhsCanBeEmpty = true

internal/parser/source/source_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,25 @@ up{node_status="v", job="node_exporter"}
353353
* on(instance) group_left(node_status) sliver_metadata
354354
`, // group_left on a label already guaranteed on the left
355355
`services_enabled{job=""}`,
356+
`
357+
group by (cluster, namespace, workload, workload_type, pod) (
358+
label_join(
359+
label_join(
360+
group by (cluster, namespace, job_name, pod) (
361+
label_join(
362+
kube_pod_owner{job="kube-state-metrics", owner_kind="Job"}
363+
, "job_name", "", "owner_name")
364+
)
365+
* on (cluster, namespace, job_name) group_left(owner_kind, owner_name)
366+
group by (cluster, namespace, job_name, owner_kind, owner_name) (
367+
kube_job_owner{job="kube-state-metrics", owner_kind!="Pod", owner_kind!=""}
368+
)
369+
, "workload", "", "owner_name")
370+
, "workload_type", "", "owner_kind")
371+
)
372+
`,
373+
`foo{job="xxx"} + on(job) group_right(instance) bar{}`,
374+
`foo{job="xxx"} + ignoring(job) group_right(instance) bar{job="zzz"}`,
356375
}
357376

358377
func TestLabelsSource(t *testing.T) {
@@ -373,6 +392,7 @@ func TestLabelsSource(t *testing.T) {
373392
}
374393
done[expr] = struct{}{}
375394

395+
t.Log(expr)
376396
n, err := parser.DecodeExpr(expr)
377397
if err != nil {
378398
t.Error(err)

0 commit comments

Comments
 (0)