Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions modules/caddyhttp/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
}
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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"
}
}
86 changes: 86 additions & 0 deletions modules/caddyhttp/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down
Loading