Skip to content

Commit 19dbe12

Browse files
authored
Merge pull request #1028 from cloudflare/comments
Improve comment warnings
2 parents f4eaf91 + da020e3 commit 19dbe12

File tree

10 files changed

+411
-109
lines changed

10 files changed

+411
-109
lines changed

cmd/pint/tests/0160_ci_comment_edit.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ level=INFO msg="Problems found" Bug=2 Warning=1
5050
rules.yml:8 Bug: `prom` Prometheus server at http://127.0.0.1:7160 didn't have any series for `up` metric in the last 1w. (promql/series)
5151
8 | expr: up == 0
5252

53-
rules.yml:8 Warning: pint disable comment `promql/series(xxx)` check doesn't match any selector in this query (promql/series)
53+
rules.yml:8 Warning: pint disable comment `promql/series(xxx)` doesn't match any selector in this query (promql/series)
5454
8 | expr: up == 0
5555

5656
rules.yml:16 Bug: `prom` Prometheus server at http://127.0.0.1:7160 didn't have any series for `up` metric in the last 1w. (promql/series)

cmd/pint/tests/0176_comments.txt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
http response prometheus /api/v1/status/flags 200 {"status":"success","data":{"storage.tsdb.retention.time": "1d"}}
2+
http response prometheus /api/v1/metadata 200 {"status":"success","data":{}}
3+
http response prometheus /api/v1/status/config 200 {"status":"success","data":{"yaml":"global:\n scrape_interval: 1m\n"}}
4+
http response prometheus /api/v1/query_range 200 {"status":"success","data":{"resultType":"matrix","result":[]}}
5+
http response prometheus /api/v1/query 200 {"status":"success","data":{"resultType":"vector","result":[]}}
6+
http start prometheus 127.0.0.1:7176
7+
8+
pint.error --no-color lint rules
9+
! stdout .
10+
cmp stderr stderr.txt
11+
12+
-- stderr.txt --
13+
level=INFO msg="Loading configuration file" path=.pint.hcl
14+
level=INFO msg="Finding all rules to check" paths=["rules"]
15+
level=INFO msg="Configured new Prometheus server" name=prom uris=1 uptime=up tags=[] include=[] exclude=[]
16+
level=WARN msg="No results for Prometheus uptime metric, you might have set uptime config option to a missing metric, please check your config" name=prom metric=up
17+
level=WARN msg="Using dummy Prometheus uptime metric results with no gaps" name=prom metric=up
18+
rules/1.yml:15 Bug: `prom` Prometheus server at http://127.0.0.1:7176 didn't have any series for `up` metric in the last 1w. (promql/series)
19+
15 | expr: up == 0
20+
21+
level=INFO msg="Problems found" Bug=1
22+
level=ERROR msg="Fatal error" err="found 1 problem(s) with severity Bug or higher"
23+
-- rules/1.yml --
24+
---
25+
groups:
26+
- name: "{{ source }}"
27+
rules:
28+
# pint ignore/begin
29+
- alert: Ignored
30+
# pint rule/set promql/series(colo_metadata) ignore/label-value brand
31+
# pint rule/set promql/series ignore/label-value colo_status
32+
expr: count(colo_metadata{colo_status="v", brand="b1"}) > 0
33+
# pint ignore/end
34+
35+
# dummy comment 1
36+
- alert: Active
37+
# dummy comment 2
38+
expr: up == 0
39+
40+
-- .pint.hcl --
41+
prometheus "prom" {
42+
uri = "http://127.0.0.1:7176"
43+
failover = []
44+
timeout = "5s"
45+
}

docs/changelog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
- [promql/series](checks/promql/series.md) check will now generate warnings if there are `# pint disable`
88
or `# pint rule/set` comments that are not matching any valid query selector or Prometheus server.
99

10+
### Fixed
11+
12+
- [promql/series](checks/promql/series.md) will now parse `rule/set` comments that target specific
13+
time series selectors in PromQL the same way as `# pint disable` comments do.
14+
1015
## v0.61.2
1116

1217
### Fixed

docs/checks/promql/series.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ Duration must follow syntax documented [here](https://prometheus.io/docs/prometh
193193
To set `min-age` for specific metric:
194194

195195
```yaml
196-
# pint rule/set promql/series($metric_name) min-age $duration
196+
# pint rule/set promql/series($selector)) min-age $duration
197197
```
198198

199199
Example:

internal/checks/promql_regexp.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/cloudflare/pint/internal/discovery"
1313
"github.com/cloudflare/pint/internal/parser"
14+
"github.com/cloudflare/pint/internal/parser/utils"
1415
)
1516

1617
const (
@@ -52,7 +53,7 @@ func (c RegexpCheck) Check(_ context.Context, _ discovery.Path, rule parser.Rule
5253
}
5354

5455
done := map[string]struct{}{}
55-
for _, selector := range getSelectors(expr.Query) {
56+
for _, selector := range utils.HasVectorSelector(expr.Query) {
5657
if _, ok := done[selector.String()]; ok {
5758
continue
5859
}

internal/checks/promql_series.go

Lines changed: 102 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/cloudflare/pint/internal/discovery"
1515
"github.com/cloudflare/pint/internal/output"
1616
"github.com/cloudflare/pint/internal/parser"
17+
"github.com/cloudflare/pint/internal/parser/utils"
1718
"github.com/cloudflare/pint/internal/promapi"
1819

1920
"github.com/prometheus/common/model"
@@ -120,10 +121,10 @@ func (c SeriesCheck) Check(ctx context.Context, _ discovery.Path, rule parser.Ru
120121

121122
params := promapi.NewRelativeRange(settings.lookbackRangeDuration, settings.lookbackStepDuration)
122123

123-
selectors := getSelectors(expr.Query)
124+
selectors := utils.HasVectorSelector(expr.Query)
124125

125126
done := map[string]bool{}
126-
for _, selector := range selectors {
127+
for _, selector := range getNonFallbackSelectors(expr.Query) {
127128
if _, ok := done[selector.String()]; ok {
128129
continue
129130
}
@@ -539,12 +540,12 @@ func (c SeriesCheck) Check(ctx context.Context, _ discovery.Path, rule parser.Ru
539540
problems = append(problems, Problem{
540541
Lines: expr.Value.Lines,
541542
Reporter: c.Reporter(),
542-
Text: fmt.Sprintf("pint %s comment `%s` check doesn't match any selector in this query", comments.DisableComment, disable.Match),
543+
Text: fmt.Sprintf("pint %s comment `%s` doesn't match any selector in this query", comments.DisableComment, disable.Match),
543544
Details: SeriesCheckUnusedDisableComment,
544545
Severity: Warning,
545546
})
546547
}
547-
for _, ruleSet := range orphanedRuleSetComments(c.Reporter(), rule, selectors) {
548+
for _, ruleSet := range orphanedRuleSetComments(rule, selectors) {
548549
problems = append(problems, Problem{
549550
Lines: expr.Value.Lines,
550551
Reporter: c.Reporter(),
@@ -635,39 +636,49 @@ func (c SeriesCheck) instantSeriesCount(ctx context.Context, query string) (int,
635636

636637
func (c SeriesCheck) getMinAge(rule parser.Rule, selector promParser.VectorSelector) (minAge time.Duration, problems []Problem) {
637638
minAge = time.Hour * 2
638-
prefixes := ruleSetMinAgePrefixes(c.Reporter(), selector)
639639
for _, ruleSet := range comments.Only[comments.RuleSet](rule.Comments, comments.RuleSetType) {
640-
for _, prefix := range prefixes {
641-
if !strings.HasPrefix(ruleSet.Value, prefix) {
640+
matcher, key, value := parseRuleSet(ruleSet.Value)
641+
if key != "min-age" {
642+
continue
643+
}
644+
if matcher != "" {
645+
isMatch, _ := matchSelectorToMetric(selector, matcher)
646+
if !isMatch {
642647
continue
643648
}
644-
val := strings.TrimPrefix(ruleSet.Value, prefix)
645-
dur, err := model.ParseDuration(val)
646-
if err != nil {
647-
problems = append(problems, Problem{
648-
Lines: rule.Lines,
649-
Reporter: c.Reporter(),
650-
Text: fmt.Sprintf("Failed to parse pint comment as duration: %s", err),
651-
Details: SeriesCheckMinAgeDetails,
652-
Severity: Warning,
653-
})
654-
} else {
655-
minAge = time.Duration(dur)
656-
}
649+
}
650+
dur, err := model.ParseDuration(value)
651+
if err != nil {
652+
problems = append(problems, Problem{
653+
Lines: rule.Lines,
654+
Reporter: c.Reporter(),
655+
Text: fmt.Sprintf("Failed to parse pint comment as duration: %s", err),
656+
Details: SeriesCheckMinAgeDetails,
657+
Severity: Warning,
658+
})
659+
} else {
660+
minAge = time.Duration(dur)
657661
}
658662
}
659663

660664
return minAge, problems
661665
}
662666

663667
func (c SeriesCheck) isLabelValueIgnored(rule parser.Rule, selector promParser.VectorSelector, labelName string) bool {
664-
values := ruleSetIgnoreLabelValValues(c.Reporter(), labelName, selector)
665668
for _, ruleSet := range comments.Only[comments.RuleSet](rule.Comments, comments.RuleSetType) {
666-
for _, val := range values {
667-
if ruleSet.Value == val {
668-
return true
669+
matcher, key, value := parseRuleSet(ruleSet.Value)
670+
if key != "ignore/label-value" {
671+
continue
672+
}
673+
if matcher != "" {
674+
isMatch, _ := matchSelectorToMetric(selector, matcher)
675+
if !isMatch {
676+
continue
669677
}
670678
}
679+
if labelName == value {
680+
return true
681+
}
671682
}
672683
return false
673684
}
@@ -686,7 +697,7 @@ func (c SeriesCheck) textAndSeverity(settings *PromqlSeriesSettings, name, text
686697
return text, s
687698
}
688699

689-
func getSelectors(n *parser.PromQLNode) (selectors []promParser.VectorSelector) {
700+
func getNonFallbackSelectors(n *parser.PromQLNode) (selectors []promParser.VectorSelector) {
690701
LOOP:
691702
for _, vs := range parser.WalkDownExpr[*promParser.VectorSelector](n) {
692703
for _, bin := range parser.WalkUpExpr[*promParser.BinaryExpr](vs.Parent) {
@@ -735,26 +746,12 @@ func isDisabled(rule parser.Rule, selector promParser.VectorSelector) bool {
735746
for _, disable := range comments.Only[comments.Disable](rule.Comments, comments.DisableType) {
736747
if strings.HasPrefix(disable.Match, SeriesCheckName+"(") && strings.HasSuffix(disable.Match, ")") {
737748
cs := strings.TrimSuffix(strings.TrimPrefix(disable.Match, SeriesCheckName+"("), ")")
738-
// try full string or name match first
739-
if cs == selector.String() || cs == selector.Name {
740-
return true
741-
}
742-
// then try matchers
743-
m, err := promParser.ParseMetricSelector(cs)
744-
if err != nil {
749+
isMatch, ok := matchSelectorToMetric(selector, cs)
750+
if !ok {
745751
continue
746752
}
747-
for _, l := range m {
748-
var isMatch bool
749-
for _, s := range selector.LabelMatchers {
750-
if s.Type == l.Type && s.Name == l.Name && s.Value == l.Value {
751-
isMatch = true
752-
break
753-
}
754-
}
755-
if !isMatch {
756-
goto NEXT
757-
}
753+
if !isMatch {
754+
goto NEXT
758755
}
759756
return true
760757
}
@@ -763,6 +760,48 @@ func isDisabled(rule parser.Rule, selector promParser.VectorSelector) bool {
763760
return false
764761
}
765762

763+
func matchSelectorToMetric(selector promParser.VectorSelector, metric string) (bool, bool) {
764+
// Try full string or name match first.
765+
if metric == selector.String() || metric == selector.Name {
766+
return true, true
767+
}
768+
// Then try matchers.
769+
m, err := promParser.ParseMetricSelector(metric)
770+
if err != nil {
771+
// Ignore errors
772+
return false, false
773+
}
774+
for _, l := range m {
775+
var isMatch bool
776+
for _, s := range selector.LabelMatchers {
777+
if s.Type == l.Type && s.Name == l.Name && s.Value == l.Value {
778+
return true, true
779+
}
780+
}
781+
if !isMatch {
782+
return false, true
783+
}
784+
}
785+
return false, true
786+
}
787+
788+
func parseRuleSet(s string) (matcher, key, value string) {
789+
if strings.HasPrefix(s, SeriesCheckName+"(") {
790+
matcher = strings.TrimPrefix(s[:strings.LastIndex(s, ")")], SeriesCheckName+"(")
791+
s = s[strings.LastIndex(s, ")")+1:]
792+
} else {
793+
s = strings.TrimPrefix(s, SeriesCheckName)
794+
}
795+
parts := strings.Fields(s)
796+
if len(parts) > 0 {
797+
key = parts[0]
798+
}
799+
if len(parts) > 1 {
800+
value = strings.Join(parts[1:], " ")
801+
}
802+
return matcher, key, value
803+
}
804+
766805
func orphanedDisableComments(ctx context.Context, rule parser.Rule, selectors []promParser.VectorSelector) (orhpaned []comments.Disable) {
767806
var promNames, promTags []string
768807
if val := ctx.Value(promapi.AllPrometheusServers); val != nil {
@@ -787,32 +826,13 @@ func orphanedDisableComments(ctx context.Context, rule parser.Rule, selectors []
787826
continue
788827
}
789828
for _, selector := range selectors {
790-
// try full string or name match first
791-
if match == selector.String() || match == selector.Name {
792-
wasUsed = true
793-
goto NEXT
794-
}
795-
// then try matchers
796-
m, err := promParser.ParseMetricSelector(match)
797-
if err != nil {
829+
isMatch, ok := matchSelectorToMetric(selector, match)
830+
if !ok {
798831
continue
799832
}
800-
var isMismatch bool
801-
for _, l := range m {
802-
var isMatch bool
803-
for _, s := range selector.LabelMatchers {
804-
if s.Type == l.Type && s.Name == l.Name && s.Value == l.Value {
805-
isMatch = true
806-
break
807-
}
808-
}
809-
if !isMatch {
810-
isMismatch = true
811-
break
812-
}
813-
}
814-
if !isMismatch {
833+
if isMatch {
815834
wasUsed = true
835+
goto NEXT
816836
}
817837
}
818838
NEXT:
@@ -823,45 +843,31 @@ func orphanedDisableComments(ctx context.Context, rule parser.Rule, selectors []
823843
return orhpaned
824844
}
825845

826-
func ruleSetIgnoreLabelValValues(reporter, labelName string, selector promParser.VectorSelector) []string {
827-
bareSelector := stripLabels(selector)
828-
return []string{
829-
fmt.Sprintf("%s ignore/label-value %s", reporter, labelName),
830-
fmt.Sprintf("%s(%s) ignore/label-value %s", reporter, bareSelector.String(), labelName),
831-
fmt.Sprintf("%s(%s) ignore/label-value %s", reporter, selector.String(), labelName),
832-
}
833-
}
834-
835-
func ruleSetMinAgePrefixes(reporter string, selector promParser.VectorSelector) []string {
836-
bareSelector := stripLabels(selector)
837-
return []string{
838-
reporter + " min-age ",
839-
fmt.Sprintf("%s(%s) min-age ", reporter, bareSelector.String()),
840-
fmt.Sprintf("%s(%s) min-age ", reporter, selector.String()),
841-
}
842-
}
843-
844-
func orphanedRuleSetComments(reporter string, rule parser.Rule, selectors []promParser.VectorSelector) (orhpaned []comments.RuleSet) {
846+
func orphanedRuleSetComments(rule parser.Rule, selectors []promParser.VectorSelector) (orhpaned []comments.RuleSet) {
845847
for _, ruleSet := range comments.Only[comments.RuleSet](rule.Comments, comments.RuleSetType) {
846-
var used bool
848+
var wasUsed bool
849+
matcher, key, value := parseRuleSet(ruleSet.Value)
847850
for _, selector := range selectors {
848-
for _, lm := range selector.LabelMatchers {
849-
for _, val := range ruleSetIgnoreLabelValValues(reporter, lm.Name, selector) {
850-
if ruleSet.Value == val {
851-
used = true
852-
goto NEXT
853-
}
851+
if matcher != "" {
852+
isMatch, _ := matchSelectorToMetric(selector, matcher)
853+
if !isMatch {
854+
continue
854855
}
855-
for _, val := range ruleSetMinAgePrefixes(reporter, selector) {
856-
if strings.HasPrefix(ruleSet.Value, val) {
857-
used = true
856+
}
857+
switch key {
858+
case "min-age":
859+
wasUsed = true
860+
case "ignore/label-value":
861+
for _, lm := range selector.LabelMatchers {
862+
if lm.Name == value {
863+
wasUsed = true
858864
goto NEXT
859865
}
860866
}
861867
}
862868
}
863869
NEXT:
864-
if !used {
870+
if !wasUsed {
865871
orhpaned = append(orhpaned, ruleSet)
866872
}
867873
}

0 commit comments

Comments
 (0)