Skip to content

Commit 371b506

Browse files
committed
fix: support regex for non-query endpoints
PR #171 implemented support for regex label values but only for the query endpoints. This change adds support for all other Prometheus API endpoints. Signed-off-by: Simon Pasquier <[email protected]>
1 parent 435301e commit 371b506

File tree

6 files changed

+171
-30
lines changed

6 files changed

+171
-30
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,11 @@ NOTE: When the `/api/v1/labels` and `/api/v1/label/<name>/values` endpoints were
204204
205205
### Rules endpoint
206206
207-
The proxy requests the `/api/v1/rules` Prometheus endpoint, discards the rules that don't contain an exact match of the label and returns the modified response to the client.
207+
The proxy requests the `/api/v1/rules` Prometheus endpoint, discards the rules that don't contain an exact match of the label(s) and returns the modified response to the client.
208208
209209
### Alerts endpoint
210210
211-
The proxy requests the `/api/v1/alerts` Prometheus endpoint, discards the rules that don't contain an exact match of the label and returns the modified response to the client.
211+
The proxy requests the `/api/v1/alerts` Prometheus endpoint, discards the rules that don't contain an exact match of the label(s) and returns the modified response to the client.
212212
213213
### Silences endpoint
214214

go.mod

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ require (
1111
github.com/prometheus/alertmanager v0.27.0
1212
github.com/prometheus/client_golang v1.19.1
1313
github.com/prometheus/prometheus v0.52.1
14-
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
1514
gotest.tools/v3 v3.5.1
1615
)
1716

injectproxy/routes.go

+48-4
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,9 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, o
378378
"/api/v1/rules": modifyAPIResponse(r.filterRules),
379379
"/api/v1/alerts": modifyAPIResponse(r.filterAlerts),
380380
}
381+
//FIXME: when ModifyResponse returns an error, the default ErrorHandler is
382+
//called which returns 502 Bad Gateway. It'd be more appropriate to treat
383+
//the error and return 400 in case of bad input for instance.
381384
proxy.ModifyResponse = r.ModifyResponse
382385
return r, nil
383386
}
@@ -577,21 +580,62 @@ func enforceQueryValues(e *PromQLEnforcer, v url.Values) (values string, noQuery
577580
return v.Encode(), true, nil
578581
}
579582

583+
func (r *routes) newLabelMatcher(vals ...string) (*labels.Matcher, error) {
584+
if r.regexMatch {
585+
if len(vals) != 1 {
586+
return nil, errors.New("only one label value allowed with regex match")
587+
}
588+
589+
re := vals[0]
590+
compiledRegex, err := regexp.Compile(re)
591+
if err != nil {
592+
return nil, fmt.Errorf("invalid regex: %w", err)
593+
}
594+
595+
if compiledRegex.MatchString("") {
596+
return nil, errors.New("regex should not match empty string")
597+
}
598+
599+
m, err := labels.NewMatcher(labels.MatchRegexp, r.label, re)
600+
if err != nil {
601+
return nil, err
602+
}
603+
604+
return m, nil
605+
}
606+
607+
if len(vals) == 1 {
608+
return &labels.Matcher{
609+
Name: r.label,
610+
Type: labels.MatchEqual,
611+
Value: vals[0],
612+
}, nil
613+
}
614+
615+
m, err := labels.NewMatcher(labels.MatchRegexp, r.label, labelValuesToRegexpString(vals))
616+
if err != nil {
617+
return nil, err
618+
}
619+
620+
return m, nil
621+
}
622+
580623
// matcher modifies all the match[] HTTP parameters to match on the tenant label.
581624
// If none was provided, a tenant label matcher matcher is injected.
582625
// This works for non-query Prometheus API endpoints like /api/v1/series,
583626
// /api/v1/label/<name>/values, /api/v1/labels and /federate which support
584627
// multiple matchers.
585628
// See e.g https://prometheus.io/docs/prometheus/latest/querying/api/#querying-metadata
586629
func (r *routes) matcher(w http.ResponseWriter, req *http.Request) {
587-
matcher := &labels.Matcher{
588-
Name: r.label,
589-
Type: labels.MatchRegexp,
590-
Value: labelValuesToRegexpString(MustLabelValues(req.Context())),
630+
matcher, err := r.newLabelMatcher(MustLabelValues(req.Context())...)
631+
if err != nil {
632+
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
633+
return
591634
}
592635

593636
q := req.URL.Query()
594637
if err := injectMatcher(q, matcher); err != nil {
638+
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
595639
return
596640
}
597641

injectproxy/routes_test.go

+61-17
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func checkParameterAbsent(param string, next http.Handler) http.Handler {
3434
prometheusAPIError(w, fmt.Sprintf("unexpected error: %v", err), http.StatusInternalServerError)
3535
return
3636
}
37+
3738
if len(kvs[param]) != 0 {
3839
prometheusAPIError(w, fmt.Sprintf("unexpected parameter %q", param), http.StatusInternalServerError)
3940
return
@@ -264,6 +265,7 @@ func TestMatch(t *testing.T) {
264265
for _, tc := range []struct {
265266
labelv []string
266267
matches []string
268+
opts []Option
267269

268270
expCode int
269271
expMatch []string
@@ -277,15 +279,15 @@ func TestMatch(t *testing.T) {
277279
// No "match" parameter.
278280
labelv: []string{"default"},
279281
expCode: http.StatusOK,
280-
expMatch: []string{`{namespace=~"default"}`},
282+
expMatch: []string{`{namespace="default"}`},
281283
expBody: okResponse,
282284
},
283285
{
284286
// Single "match" parameters.
285287
labelv: []string{"default"},
286288
matches: []string{`{job="prometheus",__name__=~"job:.*"}`},
287289
expCode: http.StatusOK,
288-
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace=~"default"}`},
290+
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`},
289291
expBody: okResponse,
290292
},
291293
{
@@ -309,15 +311,15 @@ func TestMatch(t *testing.T) {
309311
labelv: []string{"default"},
310312
matches: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`},
311313
expCode: http.StatusOK,
312-
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default",namespace=~"default"}`},
314+
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default",namespace="default"}`},
313315
expBody: okResponse,
314316
},
315317
{
316318
// Many "match" parameters.
317319
labelv: []string{"default"},
318320
matches: []string{`{job="prometheus"}`, `{__name__=~"job:.*"}`},
319321
expCode: http.StatusOK,
320-
expMatch: []string{`{job="prometheus",namespace=~"default"}`, `{__name__=~"job:.*",namespace=~"default"}`},
322+
expMatch: []string{`{job="prometheus",namespace="default"}`, `{__name__=~"job:.*",namespace="default"}`},
321323
expBody: okResponse,
322324
},
323325
{
@@ -336,6 +338,42 @@ func TestMatch(t *testing.T) {
336338
},
337339
expBody: okResponse,
338340
},
341+
{
342+
// Many "match" parameters with a single regex value.
343+
labelv: []string{".+-monitoring"},
344+
matches: []string{
345+
`{job="prometheus"}`,
346+
`{__name__=~"job:.*"}`,
347+
`{namespace="something"}`,
348+
},
349+
opts: []Option{WithRegexMatch()},
350+
351+
expCode: http.StatusOK,
352+
expMatch: []string{
353+
`{job="prometheus",namespace=~".+-monitoring"}`,
354+
`{__name__=~"job:.*",namespace=~".+-monitoring"}`,
355+
`{namespace="something",namespace=~".+-monitoring"}`,
356+
},
357+
expBody: okResponse,
358+
},
359+
{
360+
// A single "match" parameter with multiple regex values.
361+
labelv: []string{"default", "something"},
362+
matches: []string{
363+
`{job="prometheus"}`,
364+
},
365+
opts: []Option{WithRegexMatch()},
366+
expCode: http.StatusBadRequest,
367+
},
368+
{
369+
// A single "match" parameter with a regex value matching the empty string.
370+
labelv: []string{".*"},
371+
matches: []string{
372+
`{job="prometheus"}`,
373+
},
374+
opts: []Option{WithRegexMatch()},
375+
expCode: http.StatusBadRequest,
376+
},
339377
} {
340378
for _, u := range []string{
341379
"http://prometheus.example.com/federate",
@@ -351,7 +389,12 @@ func TestMatch(t *testing.T) {
351389
)
352390
defer m.Close()
353391

354-
r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithEnabledLabelsAPI())
392+
r, err := NewRoutes(
393+
m.url,
394+
proxyLabel,
395+
HTTPFormEnforcer{ParameterName: proxyLabel},
396+
append([]Option{WithEnabledLabelsAPI()}, tc.opts...)...,
397+
)
355398
if err != nil {
356399
t.Fatalf("unexpected error: %v", err)
357400
}
@@ -382,6 +425,7 @@ func TestMatch(t *testing.T) {
382425
t.Logf("%s", string(body))
383426
t.FailNow()
384427
}
428+
385429
if resp.StatusCode != http.StatusOK {
386430
return
387431
}
@@ -411,15 +455,15 @@ func TestMatchWithPost(t *testing.T) {
411455
// No "match" parameter.
412456
labelv: []string{"default"},
413457
expCode: http.StatusOK,
414-
expMatch: []string{`{namespace=~"default"}`},
458+
expMatch: []string{`{namespace="default"}`},
415459
expBody: okResponse,
416460
},
417461
{
418462
// Single "match" parameters.
419463
labelv: []string{"default"},
420464
matches: []string{`{job="prometheus",__name__=~"job:.*"}`},
421465
expCode: http.StatusOK,
422-
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace=~"default"}`},
466+
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`},
423467
expBody: okResponse,
424468
},
425469
{
@@ -443,15 +487,15 @@ func TestMatchWithPost(t *testing.T) {
443487
labelv: []string{"default"},
444488
matches: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`},
445489
expCode: http.StatusOK,
446-
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default",namespace=~"default"}`},
490+
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default",namespace="default"}`},
447491
expBody: okResponse,
448492
},
449493
{
450494
// Many "match" parameters.
451495
labelv: []string{"default"},
452496
matches: []string{`{job="prometheus"}`, `{__name__=~"job:.*"}`},
453497
expCode: http.StatusOK,
454-
expMatch: []string{`{job="prometheus",namespace=~"default"}`, `{__name__=~"job:.*",namespace=~"default"}`},
498+
expMatch: []string{`{job="prometheus",namespace="default"}`, `{__name__=~"job:.*",namespace="default"}`},
455499
expBody: okResponse,
456500
},
457501
{
@@ -546,14 +590,14 @@ func TestSeries(t *testing.T) {
546590
{
547591
name: `No "match[]" parameter returns 200 with empty body`,
548592
labelv: []string{"default"},
549-
expMatch: []string{`{namespace=~"default"}`},
593+
expMatch: []string{`{namespace="default"}`},
550594
expResponse: okResponse,
551595
expCode: http.StatusOK,
552596
},
553597
{
554598
name: `No "match[]" parameter returns 200 with empty body for POSTs`,
555599
labelv: []string{"default"},
556-
expMatch: []string{`{namespace=~"default"}`},
600+
expMatch: []string{`{namespace="default"}`},
557601
expResponse: okResponse,
558602
expCode: http.StatusOK,
559603
},
@@ -562,7 +606,7 @@ func TestSeries(t *testing.T) {
562606
labelv: []string{"default"},
563607
promQuery: "up",
564608
expCode: http.StatusOK,
565-
expMatch: []string{`{__name__="up",namespace=~"default"}`},
609+
expMatch: []string{`{__name__="up",namespace="default"}`},
566610
expResponse: okResponse,
567611
},
568612
{
@@ -586,7 +630,7 @@ func TestSeries(t *testing.T) {
586630
labelv: []string{"default"},
587631
promQuery: `up{instance="localhost:9090"}`,
588632
expCode: http.StatusOK,
589-
expMatch: []string{`{instance="localhost:9090",__name__="up",namespace=~"default"}`},
633+
expMatch: []string{`{instance="localhost:9090",__name__="up",namespace="default"}`},
590634
expResponse: okResponse,
591635
},
592636
{
@@ -679,15 +723,15 @@ func TestSeriesWithPost(t *testing.T) {
679723
name: `No "match[]" parameter returns 200 with empty body`,
680724
labelv: []string{"default"},
681725
method: http.MethodPost,
682-
expMatch: []string{`{namespace=~"default"}`},
726+
expMatch: []string{`{namespace="default"}`},
683727
expResponse: okResponse,
684728
expCode: http.StatusOK,
685729
},
686730
{
687731
name: `No "match[]" parameter returns 200 with empty body for POSTs`,
688732
method: http.MethodPost,
689733
labelv: []string{"default"},
690-
expMatch: []string{`{namespace=~"default"}`},
734+
expMatch: []string{`{namespace="default"}`},
691735
expResponse: okResponse,
692736
expCode: http.StatusOK,
693737
},
@@ -697,7 +741,7 @@ func TestSeriesWithPost(t *testing.T) {
697741
promQueryBody: "up",
698742
method: http.MethodPost,
699743
expCode: http.StatusOK,
700-
expMatch: []string{`{__name__="up",namespace=~"default"}`},
744+
expMatch: []string{`{__name__="up",namespace="default"}`},
701745
expResponse: okResponse,
702746
},
703747
{
@@ -724,7 +768,7 @@ func TestSeriesWithPost(t *testing.T) {
724768
promQueryBody: `up{instance="localhost:9090"}`,
725769
method: http.MethodPost,
726770
expCode: http.StatusOK,
727-
expMatch: []string{`{instance="localhost:9090",__name__="up",namespace=~"default"}`},
771+
expMatch: []string{`{instance="localhost:9090",__name__="up",namespace="default"}`},
728772
expResponse: okResponse,
729773
},
730774
{

injectproxy/rules.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"time"
2424

2525
"github.com/prometheus/prometheus/model/labels"
26-
"golang.org/x/exp/slices"
2726
)
2827

2928
type apiResponse struct {
@@ -210,14 +209,20 @@ func (r *routes) filterRules(lvalues []string, resp *apiResponse) (interface{},
210209
return nil, fmt.Errorf("can't decode rules data: %w", err)
211210
}
212211

212+
m, err := r.newLabelMatcher(lvalues...)
213+
if err != nil {
214+
return nil, err
215+
}
216+
213217
filtered := []*ruleGroup{}
214218
for _, rg := range rgs.RuleGroups {
215219
var rules []rule
216220
for _, rule := range rg.Rules {
217-
if lval := rule.Labels().Get(r.label); lval != "" && slices.Contains(lvalues, lval) {
221+
if lval := rule.Labels().Get(r.label); lval != "" && m.Matches(lval) {
218222
rules = append(rules, rule)
219223
}
220224
}
225+
221226
if len(rules) > 0 {
222227
rg.Rules = rules
223228
filtered = append(filtered, rg)
@@ -233,9 +238,14 @@ func (r *routes) filterAlerts(lvalues []string, resp *apiResponse) (interface{},
233238
return nil, fmt.Errorf("can't decode alerts data: %w", err)
234239
}
235240

241+
m, err := r.newLabelMatcher(lvalues...)
242+
if err != nil {
243+
return nil, err
244+
}
245+
236246
filtered := []*alert{}
237247
for _, alert := range data.Alerts {
238-
if lval := alert.Labels.Get(r.label); lval != "" && slices.Contains(lvalues, lval) {
248+
if lval := alert.Labels.Get(r.label); lval != "" && m.Matches(lval) {
239249
filtered = append(filtered, alert)
240250
}
241251
}

0 commit comments

Comments
 (0)