cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.57.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

cmd/pint/scan.go

300lines · modecode

1package main
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "log/slog"
8 "regexp"
9 "strconv"
10 "sync"
11 "time"
12
13 "github.com/prometheus/prometheus/model/rulefmt"
14 "go.uber.org/atomic"
15
16 "github.com/cloudflare/pint/internal/checks"
17 "github.com/cloudflare/pint/internal/comments"
18 "github.com/cloudflare/pint/internal/config"
19 "github.com/cloudflare/pint/internal/discovery"
20 "github.com/cloudflare/pint/internal/parser"
21 "github.com/cloudflare/pint/internal/promapi"
22 "github.com/cloudflare/pint/internal/reporter"
23)
24
25var (
26 yamlErrRe = regexp.MustCompile("^yaml: line (.+): (.+)")
27 yamlUnmarshalErrRe = regexp.MustCompile("^yaml: unmarshal errors:\n line (.+): (.+)")
28 rulefmtGroupRe = regexp.MustCompile("^([0-9]+):[0-9]+: group \".+\", rule [0-9]+, (.+)")
29 rulefmtGroupnameRe = regexp.MustCompile("^([0-9]+):[0-9]+: (groupname: .+)")
30)
31
32const (
33 yamlParseReporter = "yaml/parse"
34 ignoreFileReporter = "ignore/file"
35 pintCommentReporter = "pint/comment"
36)
37
38func tryDecodingYamlError(err error) (l int, s string) {
39 s = err.Error()
40
41 werr := &rulefmt.WrappedError{}
42 if errors.As(err, &werr) {
43 if uerr := werr.Unwrap(); uerr != nil {
44 s = uerr.Error()
45 }
46 }
47
48 for _, re := range []*regexp.Regexp{yamlErrRe, yamlUnmarshalErrRe, rulefmtGroupRe, rulefmtGroupnameRe} {
49 parts := re.FindStringSubmatch(err.Error())
50 if len(parts) > 2 {
51 line, err2 := strconv.Atoi(parts[1])
52 if err2 != nil || line <= 0 {
53 return 1, s
54 }
55 return line, parts[2]
56 }
57 }
58 return 1, s
59}
60
61func checkRules(ctx context.Context, workers int, isOffline bool, gen *config.PrometheusGenerator, cfg config.Config, entries []discovery.Entry) (summary reporter.Summary, err error) {
62 if isOffline {
63 slog.Info("Offline mode, skipping Prometheus discovery")
64 } else {
65 if len(entries) > 0 {
66 if err = gen.GenerateDynamic(ctx); err != nil {
67 return summary, err
68 }
69 slog.Debug("Generated all Prometheus servers", slog.Int("count", gen.Count()))
70 } else {
71 slog.Info("No rules found, skipping Prometheus discovery")
72 }
73 }
74
75 checkIterationChecks.Set(0)
76 checkIterationChecksDone.Set(0)
77
78 start := time.Now()
79 defer func() {
80 lastRunDuration.Set(time.Since(start).Seconds())
81 }()
82
83 jobs := make(chan scanJob, workers*5)
84 results := make(chan reporter.Report, workers*5)
85 wg := sync.WaitGroup{}
86
87 ctx = context.WithValue(ctx, promapi.AllPrometheusServers, gen.Servers())
88 for _, s := range cfg.Check {
89 settings, _ := s.Decode()
90 key := checks.SettingsKey(s.Name)
91 ctx = context.WithValue(ctx, key, settings)
92 }
93
94 for w := 1; w <= workers; w++ {
95 wg.Add(1)
96 go func() {
97 defer wg.Done()
98 scanWorker(ctx, jobs, results)
99 }()
100 }
101
102 go func() {
103 defer close(results)
104 wg.Wait()
105 }()
106
107 var onlineChecksCount, offlineChecksCount, checkedEntriesCount atomic.Int64
108 go func() {
109 for _, entry := range entries {
110 switch {
111 case entry.State == discovery.Excluded:
112 continue
113 case entry.PathError != nil && entry.State == discovery.Removed:
114 continue
115 case entry.Rule.Error.Err != nil && entry.State == discovery.Removed:
116 continue
117 case entry.PathError == nil && entry.Rule.Error.Err == nil:
118 if entry.Rule.RecordingRule != nil {
119 rulesParsedTotal.WithLabelValues(config.RecordingRuleType).Inc()
120 slog.Debug("Found recording rule",
121 slog.String("path", entry.SourcePath),
122 slog.String("record", entry.Rule.RecordingRule.Record.Value),
123 slog.String("lines", entry.Rule.Lines.String()),
124 )
125 }
126 if entry.Rule.AlertingRule != nil {
127 rulesParsedTotal.WithLabelValues(config.AlertingRuleType).Inc()
128 slog.Debug("Found alerting rule",
129 slog.String("path", entry.SourcePath),
130 slog.String("alert", entry.Rule.AlertingRule.Alert.Value),
131 slog.String("lines", entry.Rule.Lines.String()),
132 )
133 }
134
135 checkedEntriesCount.Inc()
136 checkList := cfg.GetChecksForRule(ctx, gen, entry, entry.DisabledChecks)
137 for _, check := range checkList {
138 checkIterationChecks.Inc()
139 if check.Meta().IsOnline {
140 onlineChecksCount.Inc()
141 } else {
142 offlineChecksCount.Inc()
143 }
144 jobs <- scanJob{entry: entry, allEntries: entries, check: check}
145 }
146 default:
147 if entry.Rule.Error.Err != nil {
148 slog.Debug("Found invalid rule",
149 slog.String("path", entry.SourcePath),
150 slog.String("lines", entry.Rule.Lines.String()),
151 )
152 rulesParsedTotal.WithLabelValues(config.InvalidRuleType).Inc()
153 }
154 jobs <- scanJob{entry: entry, allEntries: entries, check: nil}
155 }
156 }
157 defer close(jobs)
158 }()
159
160 for result := range results {
161 summary.Report(result)
162 }
163 summary.SortReports()
164 summary.Duration = time.Since(start)
165 summary.TotalEntries = len(entries)
166 summary.CheckedEntries = checkedEntriesCount.Load()
167 summary.OnlineChecks = onlineChecksCount.Load()
168 summary.OfflineChecks = offlineChecksCount.Load()
169
170 lastRunTime.SetToCurrentTime()
171
172 return summary, nil
173}
174
175type scanJob struct {
176 check checks.RuleChecker
177 allEntries []discovery.Entry
178 entry discovery.Entry
179}
180
181func scanWorker(ctx context.Context, jobs <-chan scanJob, results chan<- reporter.Report) {
182 for job := range jobs {
183 select {
184 case <-ctx.Done():
185 return
186 default:
187 var commentErr comments.CommentError
188 var ignoreErr discovery.FileIgnoreError
189 switch {
190 case errors.As(job.entry.PathError, &ignoreErr):
191 results <- reporter.Report{
192 ReportedPath: job.entry.ReportedPath,
193 SourcePath: job.entry.SourcePath,
194 ModifiedLines: job.entry.ModifiedLines,
195 Problem: checks.Problem{
196 Lines: parser.LineRange{
197 First: ignoreErr.Line,
198 Last: ignoreErr.Line,
199 },
200 Reporter: ignoreFileReporter,
201 Text: ignoreErr.Error(),
202 Severity: checks.Information,
203 },
204 Owner: job.entry.Owner,
205 }
206 case errors.As(job.entry.PathError, &commentErr):
207 results <- reporter.Report{
208 ReportedPath: job.entry.ReportedPath,
209 SourcePath: job.entry.SourcePath,
210 ModifiedLines: job.entry.ModifiedLines,
211 Problem: checks.Problem{
212 Lines: parser.LineRange{
213 First: commentErr.Line,
214 Last: commentErr.Line,
215 },
216 Reporter: pintCommentReporter,
217 Text: fmt.Sprintf("This comment is not a valid pint control comment: %s", commentErr.Error()),
218 Severity: checks.Warning,
219 },
220 Owner: job.entry.Owner,
221 }
222 case job.entry.PathError != nil:
223 line, e := tryDecodingYamlError(job.entry.PathError)
224 results <- reporter.Report{
225 ReportedPath: job.entry.ReportedPath,
226 SourcePath: job.entry.SourcePath,
227 ModifiedLines: job.entry.ModifiedLines,
228 Problem: checks.Problem{
229 Lines: parser.LineRange{
230 First: line,
231 Last: line,
232 },
233 Reporter: yamlParseReporter,
234 Text: fmt.Sprintf("YAML parser returned an error when reading this file: `%s`.", e),
235 Details: `pint cannot read this file because YAML parser returned an error.
236This usually means that you have an indention error or the file doesn't have the YAML structure required by Prometheus for [recording](https://prometheus.io/docs/prometheus/latest/configuration/recording_rules/) and [alerting](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/) rules.
237If this file is a template that will be rendered into valid YAML then you can instruct pint to ignore some lines using comments, see [pint docs](https://cloudflare.github.io/pint/ignoring.html).
238`,
239 Severity: checks.Fatal,
240 },
241 Owner: job.entry.Owner,
242 }
243 case job.entry.Rule.Error.Err != nil:
244 results <- reporter.Report{
245 ReportedPath: job.entry.ReportedPath,
246 SourcePath: job.entry.SourcePath,
247 ModifiedLines: job.entry.ModifiedLines,
248 Rule: job.entry.Rule,
249 Problem: checks.Problem{
250 Lines: parser.LineRange{
251 First: job.entry.Rule.Error.Line,
252 Last: job.entry.Rule.Error.Line,
253 },
254 Reporter: yamlParseReporter,
255 Text: fmt.Sprintf("This rule is not a valid Prometheus rule: `%s`.", job.entry.Rule.Error.Err.Error()),
256 Details: `This Prometheus rule is not valid.
257This usually means that it's missing some required fields.`,
258 Severity: checks.Fatal,
259 },
260 Owner: job.entry.Owner,
261 }
262 default:
263 if job.entry.State == discovery.Unknown {
264 slog.Warn(
265 "Bug: unknown rule state",
266 slog.String("path", job.entry.ReportedPath),
267 slog.Int("line", job.entry.Rule.Lines.First),
268 slog.String("name", job.entry.Rule.Name()),
269 )
270 }
271
272 start := time.Now()
273 problems := job.check.Check(ctx, job.entry.ReportedPath, job.entry.Rule, job.allEntries)
274 checkDuration.WithLabelValues(job.check.Reporter()).Observe(time.Since(start).Seconds())
275 for _, problem := range problems {
276 results <- reporter.Report{
277 ReportedPath: job.entry.ReportedPath,
278 SourcePath: job.entry.SourcePath,
279 ModifiedLines: job.entry.ModifiedLines,
280 Rule: job.entry.Rule,
281 Problem: problem,
282 Owner: job.entry.Owner,
283 }
284 }
285 }
286 }
287
288 checkIterationChecksDone.Inc()
289 }
290}
291
292func submitReports(reps []reporter.Reporter, summary reporter.Summary) (err error) {
293 for _, rep := range reps {
294 err = rep.Submit(summary)
295 if err != nil {
296 return err
297 }
298 }
299 return nil
300}