diff --git a/cmd/pint/tests/0012_issue_20.txt b/cmd/pint/tests/0012_issue_20.txt new file mode 100644 index 00000000..69952beb --- /dev/null +++ b/cmd/pint/tests/0012_issue_20.txt @@ -0,0 +1,64 @@ +pint.ok lint rules +! stdout . +cmp stderr stderr.txt + +-- stderr.txt -- +level=info msg="Loading configuration file" path=.pint.hcl +level=info msg="File parsed" path=rules/1.yaml rules=1 +level=warn msg="Tried to read more lines than present in the source file, this is likely due to '\n' usage in some rules, see https://github.com/cloudflare/pint/issues/20 for details" path=rules/1.yaml +rules/1.yaml:9-13: runbook_url annotation is required (alerts/annotation) + annotations: + summary: "HAProxy server healthcheck failure (instance {{ $labels.instance }})" + description: "Some server healthcheck are failing on {{ $labels.server }}\n VALUE = {{ $value }}\n LABELS: {{ $labels }}" + +-- rules/1.yaml -- +groups: + - name: "haproxy.api_server.rules" + rules: + - alert: HaproxyServerHealthcheckFailure + expr: increase(haproxy_server_check_failures_total[15m]) > 100 + for: 5m + labels: + severity: 24x7 + annotations: + summary: "HAProxy server healthcheck failure (instance {{ $labels.instance }})" + description: "Some server healthcheck are failing on {{ $labels.server }}\n VALUE = {{ $value }}\n LABELS: {{ $labels }}" +-- .pint.hcl -- +rule { + match { + kind = "alerting" + } + # Each alert must have a 'severity' annotation that's either '24x7','10x5' or 'debug'. + label "severity" { + severity = "bug" + value = "(24x7|10x5|debug)" + required = true + } + annotation "runbook_url" { + severity = "warning" + required = true + } +} + +rule { + # Disallow spaces in label/annotation keys, they're only allowed in values. + reject ".* +.*" { + label_keys = true + annotation_keys = true + } + + # Disallow URLs in labels, they should go to annotations. + reject "https?://.+" { + label_keys = true + label_values = true + } + # Check how many times each alert would fire in the last 1d. + alerts { + range = "1d" + step = "1m" + resolve = "5m" + } + # Check if '{{ $value }}'/'{{ .Value }}' is used in labels + # https://www.robustperception.io/dont-put-the-value-in-alert-labels + value {} +} diff --git a/internal/parser/models.go b/internal/parser/models.go index d2127ff9..d4dc82ec 100644 --- a/internal/parser/models.go +++ b/internal/parser/models.go @@ -51,7 +51,7 @@ type FilePosition struct { Lines []int } -func (fp FilePosition) FistLine() (line int) { +func (fp FilePosition) FirstLine() (line int) { for _, l := range fp.Lines { if line == 0 || l < line { line = l diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 143aadfa..72092fb6 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -123,9 +123,9 @@ func parseRule(content []byte, node *yaml.Node) (rule Rule, isEmpty bool, err er } } - if exprPart != nil && exprPart.Key.Position.FistLine() != exprPart.Value.Position.FistLine() { + if exprPart != nil && exprPart.Key.Position.FirstLine() != exprPart.Value.Position.FirstLine() { for { - start := exprPart.Value.Position.FistLine() - 1 + start := exprPart.Value.Position.FirstLine() - 1 end := exprPart.Value.Position.LastLine() input := strings.Join(strings.Split(string(content), "\n")[start:end], "") input = strings.ReplaceAll(input, " ", "") diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 0597280a..9298a9f9 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -613,6 +613,123 @@ data: `), output: nil, }, + /* + FIXME https://github.com/cloudflare/pint/issues/20 + { + content: []byte(`groups: + - name: "haproxy.api_server.rules" + rules: + - alert: HaproxyServerHealthcheckFailure + expr: increase(haproxy_server_check_failures_total[15m]) > 100 + for: 5m + labels: + severity: 24x7 + annotations: + summary: "HAProxy server healthcheck failure (instance {{ $labels.instance }})" + description: "Some server healthcheck are failing on {{ $labels.server }}\n VALUE = {{ $value }}\n LABELS: {{ $labels }}" + `), + output: []parser.Rule{ + { + AlertingRule: &parser.AlertingRule{ + Alert: parser.YamlKeyValue{ + Key: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{4}}, + Value: "alert", + }, + Value: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{4}}, + Value: "HaproxyServerHealthcheckFailure", + }, + }, + Expr: parser.PromQLExpr{ + Key: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{5}}, + Value: "expr", + }, + Value: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{5}}, + Value: "increase(haproxy_server_check_failures_total[15m]) > 100", + }, + Query: &parser.PromQLNode{ + Expr: "increase(haproxy_server_check_failures_total[15m]) > 100", + Children: []*parser.PromQLNode{ + { + Expr: "increase(haproxy_server_check_failures_total[15m])", + Children: []*parser.PromQLNode{ + { + Expr: "haproxy_server_check_failures_total[15m]", + Children: []*parser.PromQLNode{ + { + Expr: "haproxy_server_check_failures_total", + }, + }, + }, + }, + }, + {Expr: "100"}, + }, + }, + }, + For: &parser.YamlKeyValue{ + Key: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{6}}, + Value: "for", + }, + Value: &parser.YamlNode{Position: parser.FilePosition{Lines: []int{6}}, + Value: "5m", + }, + }, + Labels: &parser.YamlMap{ + Key: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{7}}, + Value: "labels", + }, + Items: []*parser.YamlKeyValue{ + { + Key: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{8}}, + Value: "severity", + }, + Value: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{8}}, + Value: "24x7", + }, + }, + }, + }, + Annotations: &parser.YamlMap{ + Key: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{9}}, + Value: "annotations", + }, + Items: []*parser.YamlKeyValue{ + { + Key: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{10}}, + Value: "summary", + }, + Value: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{10}}, + Value: "HAProxy server healthcheck failure (instance {{ $labels.instance }})", + }, + }, + { + Key: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{11}}, + Value: "description", + }, + Value: &parser.YamlNode{ + Position: parser.FilePosition{Lines: []int{11}}, + Value: `Some server healthcheck are failing on {{ $labels.server }}\n VALUE = {{ $value }}\n LABELS: {{ $labels }}`, + }, + }, + }, + }, + }, + }, + }, + }, + */ } alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true }) diff --git a/internal/reporter/console.go b/internal/reporter/console.go index dbd67b3b..c8b16402 100644 --- a/internal/reporter/console.go +++ b/internal/reporter/console.go @@ -10,6 +10,7 @@ import ( "github.com/cloudflare/pint/internal/checks" "github.com/cloudflare/pint/internal/output" + "github.com/rs/zerolog/log" ) func NewConsoleReporter(output io.Writer) ConsoleReporter { @@ -68,7 +69,13 @@ func (cr ConsoleReporter) Submit(summary Summary) error { msg = append(msg, output.MakeGray(report.Problem.Text)) } msg = append(msg, output.MakeMagneta(" (%s)\n", report.Problem.Reporter)) - for _, c := range strings.Split(content, "\n")[firstLine-1 : lastLine] { + + lines := strings.Split(content, "\n") + if lastLine > len(lines)-1 { + lastLine = len(lines) - 1 + log.Warn().Str("path", report.Path).Msgf("Tried to read more lines than present in the source file, this is likely due to '\n' usage in some rules, see https://github.com/cloudflare/pint/issues/20 for details") + } + for _, c := range lines[firstLine-1 : lastLine] { msg = append(msg, output.MakeWhite("%s\n", c)) } perFile[report.Path] = append(perFile[report.Path], strings.Join(msg, ""))