Skip to content

Commit d48d43b

Browse files
author
Lee Jaeyong
committed
Add custom labels support for metrics
1 parent 551f793 commit d48d43b

File tree

5 files changed

+249
-2
lines changed

5 files changed

+249
-2
lines changed

caddyconfig/httpcaddyfile/options.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,8 +472,20 @@ func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) {
472472
switch d.Val() {
473473
case "per_host":
474474
metrics.PerHost = true
475+
case "labels":
476+
if metrics.Labels == nil {
477+
metrics.Labels = make(map[string]string)
478+
}
479+
for nesting := d.Nesting(); d.NextBlock(nesting); {
480+
key := d.Val()
481+
if !d.NextArg() {
482+
return nil, d.ArgErr()
483+
}
484+
value := d.Val()
485+
metrics.Labels[key] = value
486+
}
475487
default:
476-
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
488+
return nil, d.Errf("unrecognized metrics option '%s'", d.Val())
477489
}
478490
}
479491
return metrics, nil

caddyconfig/httpcaddyfile/options_test.go

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package httpcaddyfile
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -58,7 +59,65 @@ func TestGlobalLogOptionSyntax(t *testing.T) {
5859
}
5960

6061
if string(out) != tc.output {
61-
t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, out)
62+
t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, string(out))
63+
}
64+
}
65+
}
66+
67+
func TestGlobalMetricsOptionSyntax(t *testing.T) {
68+
for i, tc := range []struct {
69+
input string
70+
expectError bool
71+
}{
72+
{
73+
input: `{
74+
metrics {
75+
per_host
76+
}
77+
}`,
78+
expectError: false,
79+
},
80+
{
81+
input: `{
82+
metrics {
83+
labels {
84+
proto "{http.request.proto}"
85+
method "{http.request.method}"
86+
}
87+
}
88+
}`,
89+
expectError: false,
90+
},
91+
{
92+
input: `{
93+
metrics {
94+
per_host
95+
labels {
96+
proto "{http.request.proto}"
97+
host "{http.request.host}"
98+
}
99+
}
100+
}`,
101+
expectError: false,
102+
},
103+
{
104+
input: `{
105+
metrics {
106+
unknown_option
107+
}
108+
}`,
109+
expectError: true,
110+
},
111+
} {
112+
adapter := caddyfile.Adapter{
113+
ServerType: ServerType{},
114+
}
115+
116+
out, _, err := adapter.Adapt([]byte(tc.input), nil)
117+
118+
if err != nil != tc.expectError {
119+
t.Errorf("Test %d error expectation failed Expected: %v, got %v", i, tc.expectError, err)
120+
continue
62121
}
63122
}
64123
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
metrics {
3+
labels {
4+
proto "{http.request.proto}"
5+
method "{http.request.method}"
6+
client_ip "{http.request.remote}"
7+
host "{http.request.host}"
8+
}
9+
}
10+
}
11+
12+
:8080 {
13+
respond "Hello World" 200
14+
}
15+
----------
16+
{
17+
"apps": {
18+
"http": {
19+
"servers": {
20+
"srv0": {
21+
"listen": [
22+
":8080"
23+
],
24+
"routes": [
25+
{
26+
"handle": [
27+
{
28+
"body": "Hello World",
29+
"handler": "static_response",
30+
"status_code": 200
31+
}
32+
]
33+
}
34+
]
35+
}
36+
},
37+
"metrics": {
38+
"labels": {
39+
"client_ip": "{http.request.remote}",
40+
"host": "{http.request.host}",
41+
"method": "{http.request.method}",
42+
"proto": "{http.request.proto}"
43+
}
44+
}
45+
}
46+
}
47+
}

modules/caddyhttp/metrics.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ type Metrics struct {
2323
// managed by Caddy.
2424
PerHost bool `json:"per_host,omitempty"`
2525

26+
// Labels allows users to define custom labels for metrics.
27+
// The value can use placeholders like {http.request.scheme}, {http.request.proto}, {http.request.remote}, etc.
28+
// These labels will be added to all HTTP metrics.
29+
Labels map[string]string `json:"labels,omitempty"`
30+
2631
init sync.Once
2732
httpMetrics *httpMetrics `json:"-"`
2833
}
@@ -44,6 +49,12 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
4449
if metrics.PerHost {
4550
basicLabels = append(basicLabels, "host")
4651
}
52+
if metrics.Labels != nil {
53+
for key := range metrics.Labels {
54+
basicLabels = append(basicLabels, key)
55+
}
56+
}
57+
4758
metrics.httpMetrics.requestInFlight = promauto.With(registry).NewGaugeVec(prometheus.GaugeOpts{
4859
Namespace: ns,
4960
Subsystem: sub,
@@ -71,6 +82,12 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
7182
if metrics.PerHost {
7283
httpLabels = append(httpLabels, "host")
7384
}
85+
if metrics.Labels != nil {
86+
for key := range metrics.Labels {
87+
httpLabels = append(httpLabels, key)
88+
}
89+
}
90+
7491
metrics.httpMetrics.requestDuration = promauto.With(registry).NewHistogramVec(prometheus.HistogramOpts{
7592
Namespace: ns,
7693
Subsystem: sub,
@@ -111,6 +128,36 @@ func serverNameFromContext(ctx context.Context) string {
111128
return srv.name
112129
}
113130

131+
// processCustomLabels processes custom labels by replacing placeholders with actual values.
132+
func (h *metricsInstrumentedHandler) processCustomLabels(r *http.Request) prometheus.Labels {
133+
labels := make(prometheus.Labels)
134+
135+
if h.metrics.Labels == nil {
136+
return labels
137+
}
138+
139+
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
140+
if repl == nil {
141+
repl = caddy.NewReplacer()
142+
}
143+
144+
for key, value := range h.metrics.Labels {
145+
if strings.Contains(value, "{") && strings.Contains(value, "}") {
146+
replaced := repl.ReplaceAll(value, "")
147+
148+
if replaced == "" || replaced == value {
149+
replaced = "unknown"
150+
}
151+
152+
labels[key] = replaced
153+
} else {
154+
labels[key] = value
155+
}
156+
}
157+
158+
return labels
159+
}
160+
114161
type metricsInstrumentedHandler struct {
115162
handler string
116163
mh MiddlewareHandler
@@ -138,6 +185,12 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
138185
statusLabels["host"] = strings.ToLower(r.Host)
139186
}
140187

188+
customLabels := h.processCustomLabels(r)
189+
for key, value := range customLabels {
190+
labels[key] = value
191+
statusLabels[key] = value
192+
}
193+
141194
inFlight := h.metrics.httpMetrics.requestInFlight.With(labels)
142195
inFlight.Inc()
143196
defer inFlight.Dec()

modules/caddyhttp/metrics_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,82 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
379379
}
380380
}
381381

382+
func TestMetricsInstrumentedHandlerCustomLabels(t *testing.T) {
383+
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
384+
metrics := &Metrics{
385+
Labels: map[string]string{
386+
"proto": "{http.request.proto}",
387+
"client_ip": "IP: {http.request.remote}",
388+
"host": "Host is {http.request.host}",
389+
"version": "v1.0.0",
390+
},
391+
init: sync.Once{},
392+
httpMetrics: &httpMetrics{},
393+
}
394+
395+
h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
396+
w.Write([]byte("hello world!"))
397+
return nil
398+
})
399+
400+
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
401+
return h.ServeHTTP(w, r)
402+
})
403+
404+
ih := newMetricsInstrumentedHandler(ctx, "custom_labels", mh, metrics)
405+
406+
r := httptest.NewRequest("GET", "/", nil)
407+
r.Host = "example.com"
408+
r.RemoteAddr = "192.168.1.1:12345"
409+
410+
repl := caddy.NewReplacer()
411+
reqCtx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
412+
r = r.WithContext(reqCtx)
413+
414+
w := httptest.NewRecorder()
415+
416+
addHTTPVarsToReplacer(repl, r, w)
417+
418+
if err := ih.ServeHTTP(w, r, h); err != nil {
419+
t.Errorf("Received unexpected error: %v", err)
420+
}
421+
422+
expected := `
423+
# HELP caddy_http_request_size_bytes Total size of the request. Includes body
424+
# TYPE caddy_http_request_size_bytes histogram
425+
caddy_http_request_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="256"} 1
426+
caddy_http_request_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="1024"} 1
427+
caddy_http_request_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="4096"} 1
428+
caddy_http_request_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="16384"} 1
429+
caddy_http_request_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="65536"} 1
430+
caddy_http_request_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="262144"} 1
431+
caddy_http_request_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="1.048576e+06"} 1
432+
caddy_http_request_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="4.194304e+06"} 1
433+
caddy_http_request_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="+Inf"} 1
434+
caddy_http_request_size_bytes_sum{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0"} 23
435+
caddy_http_request_size_bytes_count{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0"} 1
436+
# HELP caddy_http_response_size_bytes Size of the returned response.
437+
# TYPE caddy_http_response_size_bytes histogram
438+
caddy_http_response_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="256"} 1
439+
caddy_http_response_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="1024"} 1
440+
caddy_http_response_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="4096"} 1
441+
caddy_http_response_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="16384"} 1
442+
caddy_http_response_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="65536"} 1
443+
caddy_http_response_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="262144"} 1
444+
caddy_http_response_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="1.048576e+06"} 1
445+
caddy_http_response_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="4.194304e+06"} 1
446+
caddy_http_response_size_bytes_bucket{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0",le="+Inf"} 1
447+
caddy_http_response_size_bytes_sum{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0"} 12
448+
caddy_http_response_size_bytes_count{client_ip="IP: 192.168.1.1:12345",code="200",handler="custom_labels",host="Host is example.com",method="GET",proto="HTTP/1.1",server="UNKNOWN",version="v1.0.0"} 1
449+
`
450+
if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected),
451+
"caddy_http_request_size_bytes",
452+
"caddy_http_response_size_bytes",
453+
); err != nil {
454+
t.Errorf("received unexpected error: %s", err)
455+
}
456+
}
457+
382458
type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error
383459

384460
func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error {

0 commit comments

Comments
 (0)