Skip to content

Commit 572ca3a

Browse files
authored
Allow /metrics handler output filtering via name[] query param (#1925)
* Add metric filtering support to promhttp.HandlerForTransactional This commit adds support for filtering metrics by name using the name[] query parameter in HandlerForTransactional. Multiple metric names can be specified by providing the parameter multiple times. Example usage: /metrics?name[]=http_requests_total&name[]=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 name[] parameters are provided, all metrics are returned (backward compatible) - When name[] 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]> * Repeat comment on all methods Signed-off-by: Oleg Zaytsev <[email protected]> * Fail if can't create the request in the test Signed-off-by: Oleg Zaytsev <[email protected]> * Merge tests Signed-off-by: Oleg Zaytsev <[email protected]> * Update CHANGELOG.md Signed-off-by: Oleg Zaytsev <[email protected]> --------- Signed-off-by: Oleg Zaytsev <[email protected]>
1 parent b3886a6 commit 572ca3a

File tree

3 files changed

+135
-0
lines changed

3 files changed

+135
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
## Unreleased
22

3+
* [FEATURE] HTTP handlers created by `promhttp` package now support metrics filtering by providing one or more `name[]` query parameters. The default behavior when none are provided remains the same, returning all metrics. #1925
4+
35
## 1.23.2 / 2025-09-05
46

57
This release is made to upgrade to prometheus/common v0.66.1, which drops the dependencies github.com/grafana/regexp and go.uber.org/atomic and replaces gopkg.in/yaml.v2 with go.yaml.in/yaml/v2 (a drop-in replacement).

prometheus/promhttp/http.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ var gzipPool = sync.Pool{
8989
// metrics used for instrumentation will be shared between them, providing
9090
// global scrape counts.
9191
//
92+
// The handler supports filtering metrics by name using the `name[]` query parameter.
93+
// Multiple metric names can be specified by providing the parameter multiple times.
94+
// When no name[] parameters are provided, all metrics are returned.
95+
//
9296
// This function is meant to cover the bulk of basic use cases. If you are doing
9397
// anything that requires more customization (including using a non-default
9498
// Gatherer, different instrumentation, and non-default HandlerOpts), use the
@@ -105,13 +109,21 @@ func Handler() http.Handler {
105109
// Gatherers, with non-default HandlerOpts, and/or with custom (or no)
106110
// instrumentation. Use the InstrumentMetricHandler function to apply the same
107111
// kind of instrumentation as it is used by the Handler function.
112+
//
113+
// The handler supports filtering metrics by name using the `name[]` query parameter.
114+
// Multiple metric names can be specified by providing the parameter multiple times.
115+
// When no name[] parameters are provided, all metrics are returned.
108116
func HandlerFor(reg prometheus.Gatherer, opts HandlerOpts) http.Handler {
109117
return HandlerForTransactional(prometheus.ToTransactionalGatherer(reg), opts)
110118
}
111119

112120
// HandlerForTransactional is like HandlerFor, but it uses transactional gather, which
113121
// can safely change in-place returned *dto.MetricFamily before call to `Gather` and after
114122
// call to `done` of that `Gather`.
123+
//
124+
// The handler supports filtering metrics by name using the `name[]` query parameter.
125+
// Multiple metric names can be specified by providing the parameter multiple times.
126+
// When no name[] parameters are provided, all metrics are returned.
115127
func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerOpts) http.Handler {
116128
var (
117129
inFlightSem chan struct{}
@@ -245,7 +257,21 @@ func HandlerForTransactional(reg prometheus.TransactionalGatherer, opts HandlerO
245257
return false
246258
}
247259

260+
// Build metric name filter set from query params (if any)
261+
var metricFilter map[string]struct{}
262+
if metricNames := req.URL.Query()["name[]"]; len(metricNames) > 0 {
263+
metricFilter = make(map[string]struct{}, len(metricNames))
264+
for _, name := range metricNames {
265+
metricFilter[name] = struct{}{}
266+
}
267+
}
268+
248269
for _, mf := range mfs {
270+
if metricFilter != nil {
271+
if _, ok := metricFilter[mf.GetName()]; !ok {
272+
continue
273+
}
274+
}
249275
if handleError(enc.Encode(mf)) {
250276
return
251277
}

prometheus/promhttp/http_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,3 +640,110 @@ 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+
testCases := []struct {
666+
name string
667+
url string
668+
shouldContain []string
669+
shouldNotContain []string
670+
}{
671+
{
672+
name: "single metric filter",
673+
url: "/?name[]=test_counter",
674+
shouldContain: []string{"test_counter"},
675+
shouldNotContain: []string{"test_gauge", "test_histogram"},
676+
},
677+
{
678+
name: "multiple metric filters",
679+
url: "/?name[]=test_counter&name[]=test_gauge",
680+
shouldContain: []string{"test_counter", "test_gauge"},
681+
shouldNotContain: []string{"test_histogram"},
682+
},
683+
{
684+
name: "no filter returns all metrics",
685+
url: "/",
686+
shouldContain: []string{"test_counter", "test_gauge", "test_histogram"},
687+
shouldNotContain: []string{},
688+
},
689+
{
690+
name: "non-matching filter returns empty",
691+
url: "/?name[]=nonexistent_metric",
692+
shouldContain: []string{},
693+
shouldNotContain: []string{"test_counter", "test_gauge", "test_histogram"},
694+
},
695+
{
696+
name: "empty name[] value",
697+
url: "/?name[]=",
698+
shouldContain: []string{},
699+
shouldNotContain: []string{"test_counter", "test_gauge", "test_histogram"},
700+
},
701+
{
702+
name: "duplicate name[] values",
703+
url: "/?name[]=test_counter&name[]=test_counter",
704+
shouldContain: []string{"test_counter"},
705+
shouldNotContain: []string{"test_gauge", "test_histogram"},
706+
},
707+
}
708+
709+
for _, tc := range testCases {
710+
t.Run(tc.name, func(t *testing.T) {
711+
mReg := &mockTransactionGatherer{g: reg}
712+
713+
writer := httptest.NewRecorder()
714+
request, err := http.NewRequest(http.MethodGet, tc.url, nil)
715+
if err != nil {
716+
t.Fatal(err)
717+
}
718+
719+
request.Header.Add(acceptHeader, acceptTextPlain)
720+
721+
handler := HandlerForTransactional(mReg, HandlerOpts{})
722+
handler.ServeHTTP(writer, request)
723+
724+
if got, want := writer.Code, http.StatusOK; got != want {
725+
t.Errorf("got HTTP status code %d, want %d", got, want)
726+
}
727+
728+
body := writer.Body.String()
729+
for _, expected := range tc.shouldContain {
730+
if !strings.Contains(body, expected) {
731+
t.Errorf("expected body to contain %q, got: %s", expected, body)
732+
}
733+
}
734+
for _, notExpected := range tc.shouldNotContain {
735+
if strings.Contains(body, notExpected) {
736+
t.Errorf("expected body to NOT contain %q, got: %s", notExpected, body)
737+
}
738+
}
739+
740+
// Verify that Gather and done are called even with filtering.
741+
if got := mReg.gatherInvoked; got != 1 {
742+
t.Errorf("unexpected number of gather invokes, want 1, got %d", got)
743+
}
744+
if got := mReg.doneInvoked; got != 1 {
745+
t.Errorf("unexpected number of done invokes, want 1, got %d", got)
746+
}
747+
})
748+
}
749+
}

0 commit comments

Comments
 (0)