Skip to content

Commit 4d96d8f

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 7994213 commit 4d96d8f

File tree

6 files changed

+163
-30
lines changed

6 files changed

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

+49-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package injectproxy
1616
import (
1717
"context"
1818
"encoding/json"
19+
"errors"
1920
"fmt"
2021
"io"
2122
"net/http"
@@ -377,6 +378,9 @@ func NewRoutes(upstream *url.URL, label string, extractLabeler ExtractLabeler, o
377378
"/api/v1/rules": modifyAPIResponse(r.filterRules),
378379
"/api/v1/alerts": modifyAPIResponse(r.filterAlerts),
379380
}
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.
380384
proxy.ModifyResponse = r.ModifyResponse
381385
return r, nil
382386
}
@@ -578,18 +582,59 @@ func enforceQueryValues(e *Enforcer, v url.Values) (values string, noQuery bool,
578582
return v.Encode(), true, nil
579583
}
580584

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

591635
q := req.URL.Query()
592636
if err := injectMatcher(q, matcher); err != nil {
637+
prometheusAPIError(w, err.Error(), http.StatusBadRequest)
593638
return
594639
}
595640

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)