Add custom labels support for metrics

This commit is contained in:
Lee Jaeyong 2025-08-24 11:44:32 +09:00
parent 551f793700
commit 0f851108d1
5 changed files with 248 additions and 2 deletions

View file

@ -472,8 +472,20 @@ func unmarshalCaddyfileMetricsOptions(d *caddyfile.Dispenser) (any, error) {
switch d.Val() {
case "per_host":
metrics.PerHost = true
case "labels":
if metrics.Labels == nil {
metrics.Labels = make(map[string]string)
}
for nesting := d.Nesting(); d.NextBlock(nesting); {
key := d.Val()
if !d.NextArg() {
return nil, d.ArgErr()
}
value := d.Val()
metrics.Labels[key] = value
}
default:
return nil, d.Errf("unrecognized servers option '%s'", d.Val())
return nil, d.Errf("unrecognized metrics option '%s'", d.Val())
}
}
return metrics, nil

View file

@ -58,7 +58,65 @@ func TestGlobalLogOptionSyntax(t *testing.T) {
}
if string(out) != tc.output {
t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, out)
t.Errorf("Test %d error output mismatch Expected: %s, got %s", i, tc.output, string(out))
}
}
}
func TestGlobalMetricsOptionSyntax(t *testing.T) {
for i, tc := range []struct {
input string
expectError bool
}{
{
input: `{
metrics {
per_host
}
}`,
expectError: false,
},
{
input: `{
metrics {
labels {
proto "{http.request.proto}"
method "{http.request.method}"
}
}
}`,
expectError: false,
},
{
input: `{
metrics {
per_host
labels {
proto "{http.request.proto}"
host "{http.request.host}"
}
}
}`,
expectError: false,
},
{
input: `{
metrics {
unknown_option
}
}`,
expectError: true,
},
} {
adapter := caddyfile.Adapter{
ServerType: ServerType{},
}
out, _, err := adapter.Adapt([]byte(tc.input), nil)
if err != nil != tc.expectError {
t.Errorf("Test %d error expectation failed Expected: %v, got %v", i, tc.expectError, err)
continue
}
}
}

View file

@ -0,0 +1,47 @@
{
metrics {
labels {
proto "{http.request.proto}"
method "{http.request.method}"
client_ip "{http.request.remote}"
host "{http.request.host}"
}
}
}
:8080 {
respond "Hello World" 200
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":8080"
],
"routes": [
{
"handle": [
{
"body": "Hello World",
"handler": "static_response",
"status_code": 200
}
]
}
]
}
},
"metrics": {
"labels": {
"client_ip": "{http.request.remote}",
"host": "{http.request.host}",
"method": "{http.request.method}",
"proto": "{http.request.proto}"
}
}
}
}
}

View file

@ -23,6 +23,11 @@ type Metrics struct {
// managed by Caddy.
PerHost bool `json:"per_host,omitempty"`
// Labels allows users to define custom labels for metrics.
// The value can use placeholders like {http.request.scheme}, {http.request.proto}, {http.request.remote}, etc.
// These labels will be added to all HTTP metrics.
Labels map[string]string `json:"labels,omitempty"`
init sync.Once
httpMetrics *httpMetrics `json:"-"`
}
@ -44,6 +49,12 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
if metrics.PerHost {
basicLabels = append(basicLabels, "host")
}
if metrics.Labels != nil {
for key := range metrics.Labels {
basicLabels = append(basicLabels, key)
}
}
metrics.httpMetrics.requestInFlight = promauto.With(registry).NewGaugeVec(prometheus.GaugeOpts{
Namespace: ns,
Subsystem: sub,
@ -71,6 +82,12 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
if metrics.PerHost {
httpLabels = append(httpLabels, "host")
}
if metrics.Labels != nil {
for key := range metrics.Labels {
httpLabels = append(httpLabels, key)
}
}
metrics.httpMetrics.requestDuration = promauto.With(registry).NewHistogramVec(prometheus.HistogramOpts{
Namespace: ns,
Subsystem: sub,
@ -111,6 +128,36 @@ func serverNameFromContext(ctx context.Context) string {
return srv.name
}
// processCustomLabels processes custom labels by replacing placeholders with actual values.
func (h *metricsInstrumentedHandler) processCustomLabels(r *http.Request) prometheus.Labels {
labels := make(prometheus.Labels)
if h.metrics.Labels == nil {
return labels
}
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
if repl == nil {
repl = caddy.NewReplacer()
}
for key, value := range h.metrics.Labels {
if strings.Contains(value, "{") && strings.Contains(value, "}") {
replaced := repl.ReplaceAll(value, "")
if replaced == "" || replaced == value {
replaced = "unknown"
}
labels[key] = replaced
} else {
labels[key] = value
}
}
return labels
}
type metricsInstrumentedHandler struct {
handler string
mh MiddlewareHandler
@ -138,6 +185,12 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
statusLabels["host"] = strings.ToLower(r.Host)
}
customLabels := h.processCustomLabels(r)
for key, value := range customLabels {
labels[key] = value
statusLabels[key] = value
}
inFlight := h.metrics.httpMetrics.requestInFlight.With(labels)
inFlight.Inc()
defer inFlight.Dec()

View file

@ -379,6 +379,82 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
}
}
func TestMetricsInstrumentedHandlerCustomLabels(t *testing.T) {
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
metrics := &Metrics{
Labels: map[string]string{
"proto": "{http.request.proto}",
"client_ip": "IP: {http.request.remote}",
"host": "Host is {http.request.host}",
"version": "v1.0.0",
},
init: sync.Once{},
httpMetrics: &httpMetrics{},
}
h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
w.Write([]byte("hello world!"))
return nil
})
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
return h.ServeHTTP(w, r)
})
ih := newMetricsInstrumentedHandler(ctx, "custom_labels", mh, metrics)
r := httptest.NewRequest("GET", "/", nil)
r.Host = "example.com"
r.RemoteAddr = "192.168.1.1:12345"
repl := caddy.NewReplacer()
reqCtx := context.WithValue(r.Context(), caddy.ReplacerCtxKey, repl)
r = r.WithContext(reqCtx)
w := httptest.NewRecorder()
addHTTPVarsToReplacer(repl, r, w)
if err := ih.ServeHTTP(w, r, h); err != nil {
t.Errorf("Received unexpected error: %v", err)
}
expected := `
# HELP caddy_http_request_size_bytes Total size of the request. Includes body
# TYPE caddy_http_request_size_bytes histogram
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
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
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
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
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
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
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
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
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
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
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
# HELP caddy_http_response_size_bytes Size of the returned response.
# TYPE caddy_http_response_size_bytes histogram
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
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
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
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
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
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
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
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
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
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
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
`
if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected),
"caddy_http_request_size_bytes",
"caddy_http_response_size_bytes",
); err != nil {
t.Errorf("received unexpected error: %s", err)
}
}
type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error
func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error {