From 7750ec9ab103d3687553a6bbcb25d676b985edcd Mon Sep 17 00:00:00 2001 From: Martin Fischer Date: Sat, 17 Jan 2026 18:28:14 +0100 Subject: break: collect metrics on demand As per https://prometheus.io/docs/instrumenting/writing_exporters/#scheduling: > Metrics should only be pulled from the application when Prometheus scrapes them, > exporters should not perform scrapes based on their own timers. > That is, all scrapes should be synchronous. --- main.go | 63 ++++++++++++++++++++++++++++-------------------------------- main_test.go | 3 +-- service.nix | 6 ------ 3 files changed, 30 insertions(+), 42 deletions(-) diff --git a/main.go b/main.go index 919fe24..2e0e2cc 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,6 @@ import ( "regexp" "slices" "strings" - "time" "github.com/alecthomas/kong" "github.com/prometheus/client_golang/prometheus" @@ -21,10 +20,9 @@ import ( ) type Args struct { - Port int `help:"Port to listen on" short:"p" default:"8888" env:"PORT"` - Repos StringMap `placeholder:"{\"name\": \"path\", ...}" required:"" help:"Repositories to be grepped" env:"REPOS"` - Patterns StringMap `placeholder:"{\"name\": \"regex\", ...}" required:"" help:"Patterns to be grepped for" env:"PATTERNS"` - Interval time.Duration `help:"How often the repos should be grepped (units: s,m,h for seconds, minutes, hours)" default:"10m" env:"INTERVAL"` + Port int `help:"Port to listen on" short:"p" default:"8888" env:"PORT"` + Repos StringMap `placeholder:"{\"name\": \"path\", ...}" required:"" help:"Repositories to be grepped" env:"REPOS"` + Patterns StringMap `placeholder:"{\"name\": \"regex\", ...}" required:"" help:"Patterns to be grepped for" env:"PATTERNS"` } type StringMap map[string]string @@ -46,31 +44,20 @@ func main() { "Only the HEAD of the repositories will be grepped.", )) - http.Handle("/metrics", promhttp.Handler()) - te, err := newGitGrepExporter(args.Repos, args.Patterns, prometheus.DefaultRegisterer) ctx.FatalIfErrorf(err) - go func() { - ticker := time.NewTicker(args.Interval) - defer ticker.Stop() - - // execute immediately on start - te.grepRepos() - - for range ticker.C { - te.grepRepos() - } - }() + prometheus.MustRegister(te) + http.Handle("/metrics", promhttp.Handler()) - slog.Info("starting", "repos", args.Repos, "patterns", args.Patterns, "interval", args.Interval) + slog.Info("starting", "repos", args.Repos, "patterns", args.Patterns) slog.Info(fmt.Sprintf("serving metrics at http://localhost:%d/metrics", args.Port)) http.ListenAndServe(fmt.Sprintf(":%d", args.Port), nil) } type GitGrepExporter struct { repos map[string]string - gauges map[string]*prometheus.GaugeVec + descs map[string]*prometheus.Desc regexes map[string]*regexp.Regexp combinedPattern string } @@ -83,30 +70,38 @@ func newGitGrepExporter(repos map[string]string, patterns map[string]string, reg return nil, fmt.Errorf("no patterns specified") } - gauges := map[string]*prometheus.GaugeVec{} + descs := map[string]*prometheus.Desc{} regexes := map[string]*regexp.Regexp{} for name, pattern := range patterns { - gauges[name] = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Name: fmt.Sprintf("%s_lines", name), - }, []string{"repo"}) - reg.MustRegister(gauges[name]) + descs[name] = prometheus.NewDesc( + fmt.Sprintf("%s_lines", name), + "", + []string{"repo"}, + nil, + ) regexes[name] = regexp.MustCompile(pattern) } - return &GitGrepExporter{repos, gauges, regexes, strings.Join(slices.Collect(maps.Values(patterns)), "|")}, nil + return &GitGrepExporter{repos, descs, regexes, strings.Join(slices.Collect(maps.Values(patterns)), "|")}, nil +} + +func (e *GitGrepExporter) Describe(ch chan<- *prometheus.Desc) { + for _, desc := range e.descs { + ch <- desc + } } -func (g *GitGrepExporter) grepRepos() { +func (g *GitGrepExporter) Collect(ch chan<- prometheus.Metric) { for name, path := range g.repos { slog.Info("starting grep", "path", path) - g.grepRepo(name) + g.collectCountsForRepo(name, ch) slog.Info("finished grep", "path", path) } } -func (g *GitGrepExporter) grepRepo(name string) { - path := g.repos[name] +func (g *GitGrepExporter) collectCountsForRepo(repo string, ch chan<- prometheus.Metric) { + path := g.repos[repo] output, err := run("git", "-C", path, "grep", "-hE", g.combinedPattern, "HEAD") if err != nil { @@ -114,8 +109,8 @@ func (g *GitGrepExporter) grepRepo(name string) { if exitErr.ExitCode() == 1 { // no matches found - for _, gauge := range g.gauges { - gauge.With(map[string]string{"repo": name}).Set(0) + for _, desc := range g.descs { + ch <- prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, 0, repo) } return } @@ -138,8 +133,8 @@ func (g *GitGrepExporter) grepRepo(name string) { } } } - for word, gauge := range g.gauges { - gauge.With(map[string]string{"repo": name}).Set(counts[word]) + for word, desc := range g.descs { + ch <- prometheus.MustNewConstMetric(desc, prometheus.GaugeValue, counts[word], repo) } } diff --git a/main_test.go b/main_test.go index bd5130a..9fdecca 100644 --- a/main_test.go +++ b/main_test.go @@ -18,6 +18,7 @@ func TestGrepRepo(t *testing.T) { if err != nil { t.Fatal(err) } + reg.MustRegister(te) run = func(name string, args ...string) ([]byte, error) { return []byte( @@ -29,8 +30,6 @@ func TestGrepRepo(t *testing.T) { ), nil } - te.grepRepo("project1") - want := "# HELP match_lines \n" + "# TYPE match_lines gauge\n" + "match_lines{repo=\"project1\"} 4\n" + diff --git a/service.nix b/service.nix index 0b5aa91..bc0bbdc 100644 --- a/service.nix +++ b/service.nix @@ -24,11 +24,6 @@ in type = lib.types.listOf lib.types.str; default = []; }; - - interval = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = "10m"; - }; }; config = lib.mkIf cfg.enable { @@ -42,7 +37,6 @@ in PORT = toString cfg.port; REPOS = builtins.toJSON cfg.repos; PATTERNS = builtins.toJSON cfg.patterns; - INTERVAL = cfg.interval; }; wantedBy = ["multi-user.target"]; -- cgit v1.3.1