mirror of
https://github.com/caddyserver/caddy.git
synced 2025-10-19 15:53:17 +00:00
feat: add per_proto option to HTTP metrics for protocol-based labeling
This commit is contained in:
parent
551f793700
commit
67a57b77ab
2 changed files with 120 additions and 0 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue