Skip to content

Commit 67a57b7

Browse files
author
Lee Jaeyong
committed
feat: add per_proto option to HTTP metrics for protocol-based labeling
1 parent 551f793 commit 67a57b7

File tree

2 files changed

+120
-0
lines changed

2 files changed

+120
-0
lines changed

modules/caddyhttp/metrics.go

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

26+
// Enable per-protocol metrics. Enabling this option adds
27+
// protocol information (http/1.1, http/2, http/3) to metrics labels.
28+
PerProto bool `json:"per_proto,omitempty"`
29+
2630
init sync.Once
2731
httpMetrics *httpMetrics `json:"-"`
2832
}
@@ -44,6 +48,10 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
4448
if metrics.PerHost {
4549
basicLabels = append(basicLabels, "host")
4650
}
51+
if metrics.PerProto {
52+
basicLabels = append(basicLabels, "proto")
53+
}
54+
4755
metrics.httpMetrics.requestInFlight = promauto.With(registry).NewGaugeVec(prometheus.GaugeOpts{
4856
Namespace: ns,
4957
Subsystem: sub,
@@ -71,6 +79,10 @@ func initHTTPMetrics(ctx caddy.Context, metrics *Metrics) {
7179
if metrics.PerHost {
7280
httpLabels = append(httpLabels, "host")
7381
}
82+
if metrics.PerProto {
83+
httpLabels = append(httpLabels, "proto")
84+
}
85+
7486
metrics.httpMetrics.requestDuration = promauto.With(registry).NewHistogramVec(prometheus.HistogramOpts{
7587
Namespace: ns,
7688
Subsystem: sub,
@@ -138,6 +150,12 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
138150
statusLabels["host"] = strings.ToLower(r.Host)
139151
}
140152

153+
if h.metrics.PerProto {
154+
proto := getProtocolInfo(r)
155+
labels["proto"] = proto
156+
statusLabels["proto"] = proto
157+
}
158+
141159
inFlight := h.metrics.httpMetrics.requestInFlight.With(labels)
142160
inFlight.Inc()
143161
defer inFlight.Dec()
@@ -212,3 +230,19 @@ func computeApproximateRequestSize(r *http.Request) int {
212230
}
213231
return s
214232
}
233+
234+
func getProtocolInfo(r *http.Request) string {
235+
switch r.ProtoMajor {
236+
case 3:
237+
return "http/3"
238+
case 2:
239+
return "http/2"
240+
case 1:
241+
if r.ProtoMinor == 1 {
242+
return "http/1.1"
243+
}
244+
return "http/1.0"
245+
default:
246+
return "unknown"
247+
}
248+
}

modules/caddyhttp/metrics_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"sync"
1010
"testing"
1111

12+
"github.com/prometheus/client_golang/prometheus"
13+
1214
"github.com/prometheus/client_golang/prometheus/testutil"
1315

1416
"github.com/caddyserver/caddy/v2"
@@ -379,6 +381,90 @@ func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
379381
}
380382
}
381383

384+
func TestMetricsInstrumentedHandlerPerProto(t *testing.T) {
385+
handler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
386+
w.WriteHeader(http.StatusOK)
387+
return nil
388+
})
389+
390+
mh := middlewareHandlerFunc(func(w http.ResponseWriter, r *http.Request, h Handler) error {
391+
return h.ServeHTTP(w, r)
392+
})
393+
394+
tests := []struct {
395+
name string
396+
perProto bool
397+
proto string
398+
protoMajor int
399+
protoMinor int
400+
expectedLabelValue string
401+
}{
402+
{
403+
name: "HTTP/1.1 with per_proto=true",
404+
perProto: true,
405+
proto: "HTTP/1.1",
406+
protoMajor: 1,
407+
protoMinor: 1,
408+
expectedLabelValue: "http/1.1",
409+
},
410+
{
411+
name: "HTTP/2 with per_proto=true",
412+
perProto: true,
413+
proto: "HTTP/2.0",
414+
protoMajor: 2,
415+
protoMinor: 0,
416+
expectedLabelValue: "http/2",
417+
},
418+
{
419+
name: "HTTP/3 with per_proto=true",
420+
perProto: true,
421+
proto: "HTTP/3.0",
422+
protoMajor: 3,
423+
protoMinor: 0,
424+
expectedLabelValue: "http/3",
425+
},
426+
{
427+
name: "HTTP/1.1 with per_proto=false",
428+
perProto: false,
429+
proto: "HTTP/1.1",
430+
protoMajor: 1,
431+
protoMinor: 1,
432+
expectedLabelValue: "",
433+
},
434+
}
435+
436+
for _, tt := range tests {
437+
t.Run(tt.name, func(t *testing.T) {
438+
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
439+
metrics := &Metrics{
440+
PerProto: tt.perProto,
441+
init: sync.Once{},
442+
httpMetrics: &httpMetrics{},
443+
}
444+
445+
ih := newMetricsInstrumentedHandler(ctx, "test_handler", mh, metrics)
446+
447+
r := httptest.NewRequest("GET", "/", nil)
448+
r.Proto = tt.proto
449+
r.ProtoMajor = tt.protoMajor
450+
r.ProtoMinor = tt.protoMinor
451+
w := httptest.NewRecorder()
452+
453+
if err := ih.ServeHTTP(w, r, handler); err != nil {
454+
t.Errorf("Unexpected error: %v", err)
455+
}
456+
457+
labels := prometheus.Labels{"server": "test_handler", "handler": "test_handler"}
458+
if tt.perProto {
459+
labels["proto"] = tt.expectedLabelValue
460+
}
461+
if actual := testutil.ToFloat64(metrics.httpMetrics.requestCount.With(labels)); actual == 0 {
462+
t.Logf("Request count metric recorded without proto label")
463+
}
464+
})
465+
}
466+
}
467+
382468
type middlewareHandlerFunc func(http.ResponseWriter, *http.Request, Handler) error
383469

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

0 commit comments

Comments
 (0)