Skip to content

Commit d7a8069

Browse files
refactor: use custom registry instead of defaults, fix filteredTargets mutations (#916)
* refactor: use custom registry instead of default global registry * fix: address filteredTargets mutation issue
1 parent 6a1b08c commit d7a8069

3 files changed

Lines changed: 43 additions & 25 deletions

File tree

cmd/sql_exporter/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func main() {
9797
}
9898

9999
slog.Warn("Starting SQL exporter", "versionInfo", version.Info(), "buildContext", version.BuildContext())
100-
exporter, err := sql_exporter.NewExporter(*configFile)
100+
exporter, err := sql_exporter.NewExporter(*configFile, sql_exporter.SvcRegistry)
101101
if err != nil {
102102
slog.Error("Error creating exporter", "error", err)
103103
os.Exit(1)
@@ -113,7 +113,7 @@ func main() {
113113
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { http.Error(w, "OK", http.StatusOK) })
114114
http.HandleFunc("/", HomeHandlerFunc(*metricsPath))
115115
http.HandleFunc("/config", ConfigHandlerFunc(*metricsPath, exporter))
116-
http.Handle(*metricsPath, promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer, ExporterHandlerFor(exporter)))
116+
http.Handle(*metricsPath, promhttp.InstrumentMetricHandler(prometheus.DefaultRegisterer, ExporterHandlerFor(exporter, sql_exporter.SvcRegistry)))
117117
// Expose exporter metrics separately, for debugging purposes.
118118
http.Handle("/sql_exporter_metrics", promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}))
119119
// Expose refresh handler to reload collectors and targets

cmd/sql_exporter/promhttp.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const (
2929
)
3030

3131
// ExporterHandlerFor returns an http.Handler for the provided Exporter.
32-
func ExporterHandlerFor(exporter sql_exporter.Exporter) http.Handler {
32+
func ExporterHandlerFor(exporter sql_exporter.Exporter, registry prometheus.Gatherer) http.Handler {
3333
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
3434
ctx, cancel := contextFor(req, exporter)
3535
defer cancel()
@@ -43,7 +43,7 @@ func ExporterHandlerFor(exporter sql_exporter.Exporter) http.Handler {
4343
}
4444

4545
// Go through prometheus.Gatherers to sanitize and sort metrics.
46-
gatherer := prometheus.Gatherers{exporter.WithContext(ctx), sql_exporter.SvcRegistry}
46+
gatherer := prometheus.Gatherers{exporter.WithContext(ctx), registry}
4747
mfs, err := gatherer.Gather()
4848
if err != nil {
4949
switch t := err.(type) {

exporter.go

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ var (
2626
scrapeErrorsMetric *prometheus.CounterVec
2727
)
2828

29-
// Exporter is a prometheus.Gatherer that gathers SQL metrics from targets and merges them with the default registry.
29+
// Exporter is a prometheus.Gatherer that gathers SQL metrics from targets and merges them with the custom registry.
3030
type Exporter interface {
3131
prometheus.Gatherer
3232

@@ -50,11 +50,12 @@ type exporter struct {
5050
targets []Target
5151
jobFilters []string
5252

53-
ctx context.Context
53+
ctx context.Context
54+
registry prometheus.Registerer
5455
}
5556

5657
// NewExporter returns a new Exporter with the provided config.
57-
func NewExporter(configFile string) (Exporter, error) {
58+
func NewExporter(configFile string, registry prometheus.Registerer) (Exporter, error) {
5859
c, err := config.Load(configFile)
5960
if err != nil {
6061
return nil, err
@@ -90,13 +91,17 @@ func NewExporter(configFile string) (Exporter, error) {
9091
}
9192
}
9293

93-
scrapeErrorsMetric = registerScrapeErrorMetric()
94+
scrapeErrorsMetric, err = registerScrapeErrorMetric(registry)
95+
if err != nil {
96+
return nil, err
97+
}
9498

9599
return &exporter{
96100
config: c,
97101
targets: targets,
98102
jobFilters: nil,
99103
ctx: context.Background(),
104+
registry: registry,
100105
}, nil
101106
}
102107

@@ -106,26 +111,28 @@ func (e *exporter) WithContext(ctx context.Context) Exporter {
106111
targets: e.targets,
107112
jobFilters: e.jobFilters,
108113
ctx: ctx,
114+
registry: e.registry,
109115
}
110116
}
111117

112-
// Gather implements prometheus.Gatherer.
118+
// Gather implements prometheus.Gatherer. Should be called with a context-aware Exporter returned by WithContext() to
119+
// ensure the context is respected by collectors.
113120
func (e *exporter) Gather() ([]*dto.MetricFamily, error) {
114121
var (
115122
metricChan = make(chan Metric, capMetricChan)
116123
errs prometheus.MultiError
117124
)
118125

119-
// Filter out jobs that are not in the jobFilters list
120-
e.filterTargets(e.jobFilters)
126+
// Take a local snapshot to avoid mutating e.targets while collectors are running.
127+
targets := e.filteredTargets()
121128

122-
if len(e.targets) == 0 {
129+
if len(targets) == 0 {
123130
return nil, errors.New("no targets found")
124131
}
125132

126133
var wg sync.WaitGroup
127-
wg.Add(len(e.targets))
128-
for _, t := range e.targets {
134+
wg.Add(len(targets))
135+
for _, t := range targets {
129136
go func(target Target) {
130137
defer wg.Done()
131138
target.Collect(e.ctx, metricChan)
@@ -189,16 +196,18 @@ func (e *exporter) Gather() ([]*dto.MetricFamily, error) {
189196
return result, errs
190197
}
191198

192-
func (e *exporter) filterTargets(jf []string) {
193-
if len(jf) > 0 {
194-
var filteredTargets []Target
195-
for _, target := range e.targets {
196-
if slices.Contains(jf, target.JobGroup()) {
197-
filteredTargets = append(filteredTargets, target)
198-
}
199+
func (e *exporter) filteredTargets() []Target {
200+
if len(e.jobFilters) == 0 {
201+
return e.targets
202+
}
203+
204+
var filtered []Target
205+
for _, target := range e.targets {
206+
if slices.Contains(e.jobFilters, target.JobGroup()) {
207+
filtered = append(filtered, target)
199208
}
200-
e.targets = filteredTargets
201209
}
210+
return filtered
202211
}
203212

204213
// FilterScrapeErrorsTotal filters the scrape_errors_total metric family to only include metrics for the jobs in the
@@ -285,13 +294,22 @@ func (e *exporter) DropErrorMetrics() {
285294
}
286295

287296
// registerScrapeErrorMetric registers the metrics for the exporter itself.
288-
func registerScrapeErrorMetric() *prometheus.CounterVec {
297+
func registerScrapeErrorMetric(registry prometheus.Registerer) (*prometheus.CounterVec, error) {
289298
scrapeErrors := prometheus.NewCounterVec(prometheus.CounterOpts{
290299
Name: "scrape_errors_total",
291300
Help: "Total number of scrape errors per job, target, collector and query",
292301
}, svcMetricLabels)
293-
SvcRegistry.MustRegister(scrapeErrors)
294-
return scrapeErrors
302+
303+
if err := registry.Register(scrapeErrors); err != nil {
304+
var alreadyRegisteredErr prometheus.AlreadyRegisteredError
305+
if errors.As(err, &alreadyRegisteredErr) {
306+
slog.Debug("scrape_errors_total metric already registered, using existing metric")
307+
return alreadyRegisteredErr.ExistingCollector.(*prometheus.CounterVec), nil
308+
}
309+
slog.Error("failed to register scrape_errors_total metric", "error", err)
310+
return nil, err
311+
}
312+
return scrapeErrors, nil
295313
}
296314

297315
// split comma separated list of key=value pairs and return a map of key value pairs

0 commit comments

Comments
 (0)