aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Fischer <martin@push-f.com>2026-01-17 18:28:14 +0100
committerMartin Fischer <martin@push-f.com>2026-01-17 19:04:24 +0100
commit7750ec9ab103d3687553a6bbcb25d676b985edcd (patch)
tree94e40fde4529f5f941854de3ad2deb1d99075889
parentd18ad307fc8084f9b5479e8fc08d4fbc427738c5 (diff)
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.
-rw-r--r--main.go63
-rw-r--r--main_test.go3
-rw-r--r--service.nix6
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"];