Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a0f3326

Browse files
committedJun 7, 2024··
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 a0f3326

File tree

6 files changed

+162
-30
lines changed

6 files changed

+162
-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

+52-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,33 @@ 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+
},
339368
} {
340369
for _, u := range []string{
341370
"http://prometheus.example.com/federate",
@@ -351,7 +380,12 @@ func TestMatch(t *testing.T) {
351380
)
352381
defer m.Close()
353382

354-
r, err := NewRoutes(m.url, proxyLabel, HTTPFormEnforcer{ParameterName: proxyLabel}, WithEnabledLabelsAPI())
383+
r, err := NewRoutes(
384+
m.url,
385+
proxyLabel,
386+
HTTPFormEnforcer{ParameterName: proxyLabel},
387+
append([]Option{WithEnabledLabelsAPI()}, tc.opts...)...,
388+
)
355389
if err != nil {
356390
t.Fatalf("unexpected error: %v", err)
357391
}
@@ -382,6 +416,7 @@ func TestMatch(t *testing.T) {
382416
t.Logf("%s", string(body))
383417
t.FailNow()
384418
}
419+
385420
if resp.StatusCode != http.StatusOK {
386421
return
387422
}
@@ -411,15 +446,15 @@ func TestMatchWithPost(t *testing.T) {
411446
// No "match" parameter.
412447
labelv: []string{"default"},
413448
expCode: http.StatusOK,
414-
expMatch: []string{`{namespace=~"default"}`},
449+
expMatch: []string{`{namespace="default"}`},
415450
expBody: okResponse,
416451
},
417452
{
418453
// Single "match" parameters.
419454
labelv: []string{"default"},
420455
matches: []string{`{job="prometheus",__name__=~"job:.*"}`},
421456
expCode: http.StatusOK,
422-
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace=~"default"}`},
457+
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`},
423458
expBody: okResponse,
424459
},
425460
{
@@ -443,15 +478,15 @@ func TestMatchWithPost(t *testing.T) {
443478
labelv: []string{"default"},
444479
matches: []string{`{job="prometheus",__name__=~"job:.*",namespace="default"}`},
445480
expCode: http.StatusOK,
446-
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default",namespace=~"default"}`},
481+
expMatch: []string{`{job="prometheus",__name__=~"job:.*",namespace="default",namespace="default"}`},
447482
expBody: okResponse,
448483
},
449484
{
450485
// Many "match" parameters.
451486
labelv: []string{"default"},
452487
matches: []string{`{job="prometheus"}`, `{__name__=~"job:.*"}`},
453488
expCode: http.StatusOK,
454-
expMatch: []string{`{job="prometheus",namespace=~"default"}`, `{__name__=~"job:.*",namespace=~"default"}`},
489+
expMatch: []string{`{job="prometheus",namespace="default"}`, `{__name__=~"job:.*",namespace="default"}`},
455490
expBody: okResponse,
456491
},
457492
{
@@ -546,14 +581,14 @@ func TestSeries(t *testing.T) {
546581
{
547582
name: `No "match[]" parameter returns 200 with empty body`,
548583
labelv: []string{"default"},
549-
expMatch: []string{`{namespace=~"default"}`},
584+
expMatch: []string{`{namespace="default"}`},
550585
expResponse: okResponse,
551586
expCode: http.StatusOK,
552587
},
553588
{
554589
name: `No "match[]" parameter returns 200 with empty body for POSTs`,
555590
labelv: []string{"default"},
556-
expMatch: []string{`{namespace=~"default"}`},
591+
expMatch: []string{`{namespace="default"}`},
557592
expResponse: okResponse,
558593
expCode: http.StatusOK,
559594
},
@@ -562,7 +597,7 @@ func TestSeries(t *testing.T) {
562597
labelv: []string{"default"},
563598
promQuery: "up",
564599
expCode: http.StatusOK,
565-
expMatch: []string{`{__name__="up",namespace=~"default"}`},
600+
expMatch: []string{`{__name__="up",namespace="default"}`},
566601
expResponse: okResponse,
567602
},
568603
{
@@ -586,7 +621,7 @@ func TestSeries(t *testing.T) {
586621
labelv: []string{"default"},
587622
promQuery: `up{instance="localhost:9090"}`,
588623
expCode: http.StatusOK,
589-
expMatch: []string{`{instance="localhost:9090",__name__="up",namespace=~"default"}`},
624+
expMatch: []string{`{instance="localhost:9090",__name__="up",namespace="default"}`},
590625
expResponse: okResponse,
591626
},
592627
{
@@ -679,15 +714,15 @@ func TestSeriesWithPost(t *testing.T) {
679714
name: `No "match[]" parameter returns 200 with empty body`,
680715
labelv: []string{"default"},
681716
method: http.MethodPost,
682-
expMatch: []string{`{namespace=~"default"}`},
717+
expMatch: []string{`{namespace="default"}`},
683718
expResponse: okResponse,
684719
expCode: http.StatusOK,
685720
},
686721
{
687722
name: `No "match[]" parameter returns 200 with empty body for POSTs`,
688723
method: http.MethodPost,
689724
labelv: []string{"default"},
690-
expMatch: []string{`{namespace=~"default"}`},
725+
expMatch: []string{`{namespace="default"}`},
691726
expResponse: okResponse,
692727
expCode: http.StatusOK,
693728
},
@@ -697,7 +732,7 @@ func TestSeriesWithPost(t *testing.T) {
697732
promQueryBody: "up",
698733
method: http.MethodPost,
699734
expCode: http.StatusOK,
700-
expMatch: []string{`{__name__="up",namespace=~"default"}`},
735+
expMatch: []string{`{__name__="up",namespace="default"}`},
701736
expResponse: okResponse,
702737
},
703738
{
@@ -724,7 +759,7 @@ func TestSeriesWithPost(t *testing.T) {
724759
promQueryBody: `up{instance="localhost:9090"}`,
725760
method: http.MethodPost,
726761
expCode: http.StatusOK,
727-
expMatch: []string{`{instance="localhost:9090",__name__="up",namespace=~"default"}`},
762+
expMatch: []string{`{instance="localhost:9090",__name__="up",namespace="default"}`},
728763
expResponse: okResponse,
729764
},
730765
{

‎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)
Please sign in to comment.