Skip to content

Commit 80d1536

Browse files
committed
Add metric filtering support to promhttp.HandlerForTransactional
This commit adds support for filtering metrics by name using the metric[] query parameter in HandlerForTransactional. Multiple metric names can be specified by providing the parameter multiple times. Example usage: /metrics?metric[]=http_requests_total&metric[]=process_cpu_seconds_total Implementation details: - Query parameters are parsed and converted to a map for O(1) lookup - Filtering happens inline during the encoding loop to avoid allocating a new slice - When no metric[] parameters are provided, all metrics are returned (backward compatible) - When metric[] parameters don't match any metrics, an empty response is returned with HTTP 200 Tests include: - Single and multiple metric filtering - Backward compatibility (no filter returns all) - Non-matching filters - Empty and duplicate values - Verification that transactional gather lifecycle is maintained Signed-off-by: Oleg Zaytsev <[email protected]>
1 parent 9050000 commit 80d1536

File tree

2 files changed

+140
-0
lines changed

2 files changed

+140
-0
lines changed

prometheus/promhttp/http.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {
112112
// HandlerForTransactional is like HandlerFor, but it uses transactional gather, which
113113
// can safely change in-place returned *dto.MetricFamily before call to `Gather` and after
114114
// call to `done` of that `Gather`.
115+
//
116+
// The handler supports filtering metrics by name using the `metric[]` query parameter.
117+
// Multiple metric names can be specified by providing the parameter multiple times.
118+
// When no metric[] parameters are provided, all metrics are returned.
115119
func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerOpts) http.Handler {
116120
var (
117121
inFlightSem chan struct{}
@@ -245,7 +249,21 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO
245249
return false
246250
}
247251

252+
// Build metric name filter set from query params (if any)
253+
var metricFilter map[string]struct{}
254+
if metricNames := req.URL.Query()["metric[]"]; len(metricNames) > 0 {
255+
metricFilter = make(map[string]struct{}, len(metricNames))
256+
for _, name := range metricNames {
257+
metricFilter[name] = struct{}{}
258+
}
259+
}
260+
248261
for _, mf := range mfs {
262+
if metricFilter != nil {
263+
if _, ok := metricFilter[mf.GetName()]; !ok {
264+
continue
265+
}
266+
}
249267
if handleError(enc.Encode(mf)) {
250268
return
251269
}

prometheus/promhttp/http_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,3 +640,125 @@ func BenchmarkCompression(b *testing.B) {
640640
}
641641
}
642642
}
643+
644+
func TestHandlerWithMetricFilter(t *testing.T) {
645+
reg := prometheus.NewRegistry()
646+
647+
counter := prometheus.NewCounter(prometheus.CounterOpts{
648+
Name: "test_counter",
649+
Help: "A test counter.",
650+
})
651+
gauge := prometheus.NewGauge(prometheus.GaugeOpts{
652+
Name: "test_gauge",
653+
Help: "A test gauge.",
654+
})
655+
histogram := prometheus.NewHistogram(prometheus.HistogramOpts{
656+
Name: "test_histogram",
657+
Help: "A test histogram.",
658+
})
659+
660+
reg.MustRegister(counter, gauge, histogram)
661+
counter.Inc()
662+
gauge.Set(42)
663+
histogram.Observe(3.14)
664+
665+
mReg := &mockTransactionGatherer{g: reg}
666+
handler := HandlerForTransactional(mReg, HandlerOpts{})
667+
668+
testCases := []struct {
669+
name string
670+
url string
671+
shouldContain []string
672+
shouldNotContain []string
673+
}{
674+
{
675+
name: "single metric filter",
676+
url: "/?metric[]=test_counter",
677+
shouldContain: []string{"test_counter"},
678+
shouldNotContain: []string{"test_gauge", "test_histogram"},
679+
},
680+
{
681+
name: "multiple metric filters",
682+
url: "/?metric[]=test_counter&metric[]=test_gauge",
683+
shouldContain: []string{"test_counter", "test_gauge"},
684+
shouldNotContain: []string{"test_histogram"},
685+
},
686+
{
687+
name: "no filter returns all metrics",
688+
url: "/",
689+
shouldContain: []string{"test_counter", "test_gauge", "test_histogram"},
690+
shouldNotContain: []string{},
691+
},
692+
{
693+
name: "non-matching filter returns empty",
694+
url: "/?metric[]=nonexistent_metric",
695+
shouldContain: []string{},
696+
shouldNotContain: []string{"test_counter", "test_gauge", "test_histogram"},
697+
},
698+
{
699+
name: "empty metric[] value",
700+
url: "/?metric[]=",
701+
shouldContain: []string{},
702+
shouldNotContain: []string{"test_counter", "test_gauge", "test_histogram"},
703+
},
704+
{
705+
name: "duplicate metric[] values",
706+
url: "/?metric[]=test_counter&metric[]=test_counter",
707+
shouldContain: []string{"test_counter"},
708+
shouldNotContain: []string{"test_gauge", "test_histogram"},
709+
},
710+
}
711+
712+
for _, tc := range testCases {
713+
t.Run(tc.name, func(t *testing.T) {
714+
writer := httptest.NewRecorder()
715+
request, _ := http.NewRequest(http.MethodGet, tc.url, nil)
716+
request.Header.Add(acceptHeader, acceptTextPlain)
717+
718+
handler.ServeHTTP(writer, request)
719+
720+
if got, want := writer.Code, http.StatusOK; got != want {
721+
t.Errorf("got HTTP status code %d, want %d", got, want)
722+
}
723+
724+
body := writer.Body.String()
725+
for _, expected := range tc.shouldContain {
726+
if !strings.Contains(body, expected) {
727+
t.Errorf("expected body to contain %q, got: %s", expected, body)
728+
}
729+
}
730+
for _, notExpected := range tc.shouldNotContain {
731+
if strings.Contains(body, notExpected) {
732+
t.Errorf("expected body to NOT contain %q, got: %s", notExpected, body)
733+
}
734+
}
735+
})
736+
}
737+
}
738+
739+
func TestHandlerWithMetricFilterTransactionalCalls(t *testing.T) {
740+
reg := prometheus.NewRegistry()
741+
742+
counter := prometheus.NewCounter(prometheus.CounterOpts{
743+
Name: "test_counter",
744+
Help: "A test counter.",
745+
})
746+
reg.MustRegister(counter)
747+
748+
mReg := &mockTransactionGatherer{g: reg}
749+
handler := HandlerForTransactional(mReg, HandlerOpts{})
750+
751+
writer := httptest.NewRecorder()
752+
request, _ := http.NewRequest(http.MethodGet, "/?metric[]=test_counter", nil)
753+
request.Header.Add(acceptHeader, acceptTextPlain)
754+
755+
handler.ServeHTTP(writer, request)
756+
757+
// Verify that Gather and done are called even with filtering
758+
if got := mReg.gatherInvoked; got != 1 {
759+
t.Errorf("unexpected number of gather invokes, want 1, got %d", got)
760+
}
761+
if got := mReg.doneInvoked; got != 1 {
762+
t.Errorf("unexpected number of done invokes, want 1, got %d", got)
763+
}
764+
}

0 commit comments

Comments
 (0)