feat: add per_proto option to HTTP metrics for protocol-based labeling

This commit is contained in:
Lee Jaeyong 2025-08-23 21:11:26 +09:00
parent 551f793700
commit 67a57b77ab
2 changed files with 120 additions and 0 deletions

View file

@ -23,6 +23,10 @@ type Metrics struct {
// managed by Caddy.
PerHost bool `json:"per_host,omitempty"`
// Enable per-protocol metrics. Enabling this option adds
// protocol information (http/1.1, http/2, http/3) to metrics labels.
PerProto bool `json:"per_proto,omitempty"`
init sync.Once
httpMetrics *httpMetrics `json:"-"`
}
@ -44,6 +48,10 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
if metrics.PerHost {
basicLabels = append(basicLabels, "host")
}
if metrics.PerProto {
basicLabels = append(basicLabels, "proto")
}
metrics.httpMetrics.requestInFlight = promauto.With(registry).NewGaugeVec(prometheus.GaugeOpts{
Namespace: ns,
Subsystem: sub,
@ -71,6 +79,10 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
if metrics.PerHost {
httpLabels = append(httpLabels, "host")
}
if metrics.PerProto {
httpLabels = append(httpLabels, "proto")
}
metrics.httpMetrics.requestDuration = promauto.With(registry).NewHistogramVec(prometheus.HistogramOpts{
Namespace: ns,
Subsystem: sub,
@ -138,6 +150,12 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
statusLabels["host"] = strings.ToLower(r.Host)
}
if h.metrics.PerProto {
proto := getProtocolInfo(r)
labels["proto"] = proto
statusLabels["proto"] = proto
}
inFlight := h.metrics.httpMetrics.requestInFlight.With(labels)
inFlight.Inc()
defer inFlight.Dec()
@ -212,3 +230,19 @@ func computeApproximateRequestSize(r *http.Request) int {
}
return s
}
func getProtocolInfo(r *http.Request) string {
switch r.ProtoMajor {
case 3:
return "http/3"
case 2:
return "http/2"
case 1:
if r.ProtoMinor == 1 {
return "http/1.1"
}
return "http/1.0"
default:
return "unknown"
}
}

View file

@ -9,6 +9,8 @@ import (
"sync"
"testing"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/caddyserver/caddy/v2"
@ -379,6 +381,90 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
}
}
func TestMetricsInstrumentedHandlerPerProto(t *testing.T) {
handler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
w.WriteHeader(http.StatusOK)
return nil
})
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
return h.ServeHTTP(w, r)
})
tests := []struct {
name string
perProto bool
proto string
protoMajor int
protoMinor int
expectedLabelValue string
}{
{
name: "HTTP/1.1 with per_proto=true",
perProto: true,
proto: "HTTP/1.1",
protoMajor: 1,
protoMinor: 1,
expectedLabelValue: "http/1.1",
},
{
name: "HTTP/2 with per_proto=true",
perProto: true,
proto: "HTTP/2.0",
protoMajor: 2,
protoMinor: 0,
expectedLabelValue: "http/2",
},
{
name: "HTTP/3 with per_proto=true",
perProto: true,
proto: "HTTP/3.0",
protoMajor: 3,
protoMinor: 0,
expectedLabelValue: "http/3",
},
{
name: "HTTP/1.1 with per_proto=false",
perProto: false,
proto: "HTTP/1.1",
protoMajor: 1,
protoMinor: 1,
expectedLabelValue: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
metrics := &Metrics{
PerProto: tt.perProto,
init: sync.Once{},
httpMetrics: &httpMetrics{},
}
ih := newMetricsInstrumentedHandler(ctx, "test_handler", mh, metrics)
r := httptest.NewRequest("GET", "/", nil)
r.Proto = tt.proto
r.ProtoMajor = tt.protoMajor
r.ProtoMinor = tt.protoMinor
w := httptest.NewRecorder()
if err := ih.ServeHTTP(w, r, handler); err != nil {
t.Errorf("Unexpected error: %v", err)
}
labels := prometheus.Labels{"server": "test_handler", "handler": "test_handler"}
if tt.perProto {
labels["proto"] = tt.expectedLabelValue
}
if actual := testutil.ToFloat64(metrics.httpMetrics.requestCount.With(labels)); actual == 0 {
t.Logf("Request count metric recorded without proto label")
}
})
}
}
type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error
func (f middlewareHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request, h Handler) error {