Skip to content

Commit 6a1b08c

Browse files
fix: only export scrape_errors_total for related job filters (#915)
1 parent 69f1076 commit 6a1b08c

4 files changed

Lines changed: 95 additions & 18 deletions

File tree

cmd/sql_exporter/promhttp.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ func ExporterHandlerFor(exporter sql_exporter.Exporter) http.Handler {
3636

3737
// Parse the query params and set the job filters if any
3838
jobFilters := req.URL.Query()["jobs[]"]
39-
exporter.SetJobFilters(jobFilters)
39+
if err := exporter.SetJobFilters(jobFilters); err != nil {
40+
slog.Warn("Error setting job filters, ignoring", "error", err)
41+
http.Error(w, "Error setting job filters: "+err.Error(), http.StatusBadRequest)
42+
return
43+
}
4044

4145
// Go through prometheus.Gatherers to sanitize and sort metrics.
4246
gatherer := prometheus.Gatherers{exporter.WithContext(ctx), sql_exporter.SvcRegistry}
@@ -61,6 +65,9 @@ func ExporterHandlerFor(exporter sql_exporter.Exporter) http.Handler {
6165
}
6266
}
6367

68+
// Filter the scrape_errors_total metric family to only include metrics for the jobs in the jobFilters list.
69+
mfs = exporter.FilterScrapeErrorsTotal(mfs)
70+
6471
contentType := expfmt.Negotiate(req.Header)
6572
buf := getBuf()
6673
defer giveBuf(buf)
@@ -115,7 +122,8 @@ func contextFor(req *http.Request, exporter sql_exporter.Exporter) (context.Cont
115122
// Subtract the timeout offset, unless the result would be negative or zero.
116123
timeoutOffset := time.Duration(exporter.Config().Globals.TimeoutOffset)
117124
if timeoutOffset > timeout {
118-
slog.Error("global.scrape_timeout_offset is greater than Prometheus' scraping timeout, ignoring", "timeout", timeout, "timeoutOffset", timeoutOffset)
125+
slog.Error("global.scrape_timeout_offset is greater than Prometheus' scraping timeout, ignoring",
126+
"timeout", timeout, "timeoutOffset", timeoutOffset)
119127
} else {
120128
timeout -= timeoutOffset
121129
}

collector.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ func (cc *cachingCollector) Collect(ctx context.Context, conn *sql.DB, ch chan<-
121121
// Have the lock.
122122
if age := collTime.Sub(cacheTime); age > cc.minInterval || len(cc.cache) == 0 {
123123
// Cache contents are older than minInterval, collect fresh metrics, cache them and pipe them through.
124-
slog.Debug("Collecting fresh metrics", "logContext", cc.rawColl.logContext, "min_interval", cc.minInterval.Seconds(), "cache_age", age.Seconds())
124+
slog.Debug("Collecting fresh metrics", "logContext", cc.rawColl.logContext, "min_interval",
125+
cc.minInterval.Seconds(), "cache_age", age.Seconds())
125126
cacheChan := make(chan Metric, capMetricChan)
126127
cc.cache = make([]Metric, 0, len(cc.cache))
127128
go func() {
@@ -131,7 +132,8 @@ func (cc *cachingCollector) Collect(ctx context.Context, conn *sql.DB, ch chan<-
131132
for metric := range cacheChan {
132133
// catch invalid metrics and return them immediately, don't cache them
133134
if ctx.Err() != nil {
134-
slog.Debug("Context closed, returning invalid metric", "logContext", cc.rawColl.logContext)
135+
slog.Debug("Context closed, returning invalid metric", "logContext",
136+
cc.rawColl.logContext)
135137
ch <- NewInvalidMetric(errors.Wrap(cc.rawColl.logContext, ctx.Err()))
136138
continue
137139
}
@@ -141,7 +143,8 @@ func (cc *cachingCollector) Collect(ctx context.Context, conn *sql.DB, ch chan<-
141143
}
142144
cacheTime = collTime
143145
} else {
144-
slog.Debug("Returning cached metrics", "logContext", cc.rawColl.logContext, "min_interval", cc.minInterval.Seconds(), "cache_age", age.Seconds())
146+
slog.Debug("Returning cached metrics", "logContext", cc.rawColl.logContext, "min_interval",
147+
cc.minInterval.Seconds(), "cache_age", age.Seconds())
145148
for _, metric := range cc.cache {
146149
ch <- metric
147150
}

exporter.go

Lines changed: 77 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@ type Exporter interface {
3737
// UpdateTarget updates the targets field
3838
UpdateTarget([]Target)
3939
// SetJobFilters sets the jobFilters field
40-
SetJobFilters([]string)
40+
SetJobFilters([]string) error
4141
// DropErrorMetrics resets the scrape_errors_total metric
4242
DropErrorMetrics()
43+
// FilterScrapeErrorsTotal filters the scrape_errors_total metric family to only include metrics for the jobs in
44+
// the jobFilters list.
45+
FilterScrapeErrorsTotal([]*dto.MetricFamily) []*dto.MetricFamily
4346
}
4447

4548
type exporter struct {
@@ -67,7 +70,8 @@ func NewExporter(configFile string) (Exporter, error) {
6770

6871
var targets []Target
6972
if c.Target != nil {
70-
target, err := NewTarget("", c.Target.Name, "", string(c.Target.DSN), c.Target.Collectors(), nil, c.Globals, c.Target.EnablePing)
73+
target, err := NewTarget("", c.Target.Name, "", string(c.Target.DSN),
74+
c.Target.Collectors(), nil, c.Globals, c.Target.EnablePing)
7175
if err != nil {
7276
return nil, err
7377
}
@@ -91,7 +95,7 @@ func NewExporter(configFile string) (Exporter, error) {
9195
return &exporter{
9296
config: c,
9397
targets: targets,
94-
jobFilters: []string{},
98+
jobFilters: nil,
9599
ctx: context.Background(),
96100
}, nil
97101
}
@@ -181,6 +185,7 @@ func (e *exporter) Gather() ([]*dto.MetricFamily, error) {
181185
for _, mf := range dtoMetricFamilies {
182186
result = append(result, mf)
183187
}
188+
184189
return result, errs
185190
}
186191

@@ -192,13 +197,49 @@ func (e *exporter) filterTargets(jf []string) {
192197
filteredTargets = append(filteredTargets, target)
193198
}
194199
}
195-
if len(filteredTargets) == 0 {
196-
slog.Error("No targets found for job filters. Nothing to scrape.")
197-
}
198200
e.targets = filteredTargets
199201
}
200202
}
201203

204+
// FilterScrapeErrorsTotal filters the scrape_errors_total metric family to only include metrics for the jobs in the
205+
// jobFilters list. If jobFilters is empty it returns the original metric families unmodified.
206+
func (e *exporter) FilterScrapeErrorsTotal(mfs []*dto.MetricFamily) []*dto.MetricFamily {
207+
if len(e.jobFilters) == 0 {
208+
return mfs
209+
}
210+
211+
result := make([]*dto.MetricFamily, 0, len(mfs))
212+
for _, mf := range mfs {
213+
if mf.GetName() != "scrape_errors_total" {
214+
result = append(result, mf)
215+
continue
216+
}
217+
218+
// Filter metrics in this family to only include those with a job label in the jobFilters list.
219+
filtered := make([]*dto.Metric, 0, len(mf.Metric))
220+
for _, metric := range mf.Metric {
221+
for _, label := range metric.Label {
222+
if label.GetName() == "job" {
223+
if slices.Contains(e.jobFilters, label.GetValue()) {
224+
filtered = append(filtered, metric)
225+
}
226+
break
227+
}
228+
}
229+
}
230+
231+
if len(filtered) > 0 {
232+
result = append(result, &dto.MetricFamily{
233+
Name: mf.Name,
234+
Help: mf.Help,
235+
Type: mf.Type,
236+
Metric: filtered,
237+
})
238+
}
239+
}
240+
return result
241+
}
242+
202243
// Config implements Exporter.
203244
func (e *exporter) Config() *config.Config {
204245
return e.config
@@ -210,8 +251,31 @@ func (e *exporter) UpdateTarget(target []Target) {
210251
}
211252

212253
// SetJobFilters implements Exporter.
213-
func (e *exporter) SetJobFilters(filters []string) {
254+
func (e *exporter) SetJobFilters(filters []string) error {
255+
// If the filters list contains a single empty string, treat it as no filters.
256+
if len(filters) == 0 || (len(filters) == 1 && filters[0] == "") {
257+
slog.Debug("Received empty job filter, treating as no filters")
258+
e.jobFilters = nil
259+
return nil
260+
}
261+
262+
// Single target mode has no jobs - filters are not applicable
263+
if len(e.config.Jobs) == 0 {
264+
slog.Warn("Job filters are not applicable in single target mode, ignoring", "filters", filters)
265+
e.jobFilters = nil
266+
return nil
267+
}
268+
269+
for _, name := range filters {
270+
if !slices.ContainsFunc(e.config.Jobs, func(j *config.JobConfig) bool {
271+
return j.Name == name
272+
}) {
273+
return fmt.Errorf("invalid job name: %s", name)
274+
}
275+
}
276+
214277
e.jobFilters = filters
278+
return nil
215279
}
216280

217281
// DropErrorMetrics implements Exporter.
@@ -233,8 +297,12 @@ func registerScrapeErrorMetric() *prometheus.CounterVec {
233297
// split comma separated list of key=value pairs and return a map of key value pairs
234298
func parseContextLog(list string) map[string]string {
235299
m := make(map[string]string)
236-
for _, item := range strings.Split(list, ",") {
300+
for item := range strings.SplitSeq(list, ",") {
237301
parts := strings.SplitN(item, "=", 2)
302+
if len(parts) != 2 {
303+
slog.Warn("Invalid context log item, ignoring", "item", item)
304+
continue
305+
}
238306
m[parts[0]] = parts[1]
239307
}
240308
return m
@@ -243,8 +311,5 @@ func parseContextLog(list string) map[string]string {
243311
// TrimMissingCtx trims the leading comma and space from the log context string.
244312
// Leading comma appears when previous parameter is undefined, which is a side-effect of running in single target mode.
245313
func TrimMissingCtx(logContext string) string {
246-
if strings.HasPrefix(logContext, ",") {
247-
logContext = strings.TrimLeft(logContext, ", ")
248-
}
249-
return logContext
314+
return strings.TrimPrefix(logContext, ", ")
250315
}

job.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ func NewJob(jc *config.JobConfig, gc *config.GlobalConfig) (Job, errors.WithCont
4545
}
4646
constLabels[name] = value
4747
}
48-
t, err := NewTarget(j.logContext, tname, jc.Name, string(dsn), jc.Collectors(), constLabels, gc, jc.EnablePing)
48+
t, err := NewTarget(j.logContext, tname, jc.Name, string(dsn), jc.Collectors(),
49+
constLabels, gc, jc.EnablePing)
4950
if err != nil {
5051
return nil, err
5152
}

0 commit comments

Comments
 (0)