Skip to content

Commit 9391633

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

File tree

5 files changed

+248
-2
lines changed

5 files changed

+248
-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: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,65 @@ func TestGlobalLogOptionSyntax(t *testing.T) {
5858
}
5959

6060
if string(out) != tc.output {
61-
t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, out)
61+
t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, string(out))
62+
}
63+
}
64+
}
65+
66+
func TestGlobalMetricsOptionSyntax(t *testing.T) {
67+
for i, tc := range []struct {
68+
input string
69+
expectError bool
70+
}{
71+
{
72+
input: `{
73+
metrics {
74+
per_host
75+
}
76+
}`,
77+
expectError: false,
78+
},
79+
{
80+
input: `{
81+
metrics {
82+
labels {
83+
proto "{http.request.proto}"
84+
method "{http.request.method}"
85+
}
86+
}
87+
}`,
88+
expectError: false,
89+
},
90+
{
91+
input: `{
92+
metrics {
93+
per_host
94+
labels {
95+
proto "{http.request.proto}"
96+
host "{http.request.host}"
97+
}
98+
}
99+
}`,
100+
expectError: false,
101+
},
102+
{
103+
input: `{
104+
metrics {
105+
unknown_option
106+
}
107+
}`,
108+
expectError: true,
109+
},
110+
} {
111+
adapter := caddyfile.Adapter{
112+
ServerType: ServerType{},
113+
}
114+
115+
out, _, err := adapter.Adapt([]byte(tc.input), nil)
116+
117+
if err != nil != tc.expectError {
118+
t.Errorf("Test %d error expectation failed Expected: %v, got %v", i, tc.expectError, err)
119+
continue
62120
}
63121
}
64122
}
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)