mirror of
https://github.com/caddyserver/caddy.git
synced 2026-03-08 01:56:49 -05:00
* perf: collect metrics once per route instead of per handler (#4644) Move Prometheus metrics instrumentation from the per-handler level to the per-route level. Previously, every middleware handler in a route was individually wrapped with metricsInstrumentedHandler, causing metrics to be collected N times per request (once per handler in the chain). Since all handlers in a route see the same request, these per-handler metrics were redundant and added significant CPU overhead (73% of request handling time per the original profiling). The fix introduces metricsInstrumentedRoute which wraps the entire compiled handler chain once in wrapRoute, collecting metrics only when the route actually matches. The handler label uses the first handler's module name, which is the most meaningful identifier for the route. Benchmark results (5 handlers per route): Old (per-handler): ~4650 ns/op, 4400 B/op, 45 allocs/op New (per-route): ~940 ns/op, 816 B/op, 8 allocs/op Improvement: ~5x faster, ~5.4x less memory, ~5.6x fewer allocs Signed-off-by: Varun Chawla <varun_6april@hotmail.com> * Remove unused metricsInstrumentedHandler code Delete the metricsInstrumentedHandler type, its constructor, and ServeHTTP method since they are no longer used after switching to route-level metrics collection via metricsInstrumentedRoute. Also remove the unused metrics parameter from wrapMiddleware and the middlewareHandlerFunc test helper, and convert existing tests to use the new route-level API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Address review feedback: restore comments, move function to bottom - Move computeApproximateRequestSize back to bottom of file to minimize diff - Restore all useful comments that were accidentally dropped - Old metricsInstrumentedHandler already removed in previous commit --------- Signed-off-by: Varun Chawla <varun_6april@hotmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
582 lines
32 KiB
Go
582 lines
32 KiB
Go
package caddyhttp
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/prometheus/client_golang/prometheus/testutil"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
)
|
|
|
|
func TestServerNameFromContext(t *testing.T) {
|
|
ctx := context.Background()
|
|
expected := "UNKNOWN"
|
|
if actual := serverNameFromContext(ctx); actual != expected {
|
|
t.Errorf("Not equal: expected %q, but got %q", expected, actual)
|
|
}
|
|
|
|
in := "foo"
|
|
ctx = context.WithValue(ctx, ServerCtxKey, &Server{name: in})
|
|
if actual := serverNameFromContext(ctx); actual != in {
|
|
t.Errorf("Not equal: expected %q, but got %q", in, actual)
|
|
}
|
|
}
|
|
|
|
func TestMetricsInstrumentedHandler(t *testing.T) {
|
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
metrics := &Metrics{
|
|
init: sync.Once{},
|
|
httpMetrics: &httpMetrics{},
|
|
}
|
|
handlerErr := errors.New("oh noes")
|
|
response := []byte("hello world!")
|
|
h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 1.0 {
|
|
t.Errorf("Not same: expected %#v, but got %#v", 1.0, actual)
|
|
}
|
|
if handlerErr == nil {
|
|
w.Write(response)
|
|
}
|
|
return handlerErr
|
|
})
|
|
|
|
ih := newMetricsInstrumentedRoute(ctx, "bar", h, metrics)
|
|
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
if actual := ih.ServeHTTP(w, r); actual != handlerErr {
|
|
t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual)
|
|
}
|
|
if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 0.0 {
|
|
t.Errorf("Not same: expected %#v, but got %#v", 0.0, actual)
|
|
}
|
|
|
|
handlerErr = nil
|
|
if err := ih.ServeHTTP(w, r); err != nil {
|
|
t.Errorf("Received unexpected error: %v", err)
|
|
}
|
|
|
|
// an empty handler - no errors, no header written
|
|
emptyHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
return nil
|
|
})
|
|
ih = newMetricsInstrumentedRoute(ctx, "empty", emptyHandler, metrics)
|
|
r = httptest.NewRequest("GET", "/", nil)
|
|
w = httptest.NewRecorder()
|
|
|
|
if err := ih.ServeHTTP(w, r); err != nil {
|
|
t.Errorf("Received unexpected error: %v", err)
|
|
}
|
|
if actual := w.Result().StatusCode; actual != 200 {
|
|
t.Errorf("Not same: expected status code %#v, but got %#v", 200, actual)
|
|
}
|
|
if actual := w.Result().Header; len(actual) != 0 {
|
|
t.Errorf("Not empty: expected headers to be empty, but got %#v", actual)
|
|
}
|
|
|
|
// handler returning an error with an HTTP status
|
|
errHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
return Error(http.StatusTooManyRequests, nil)
|
|
})
|
|
|
|
ih = newMetricsInstrumentedRoute(ctx, "foo", errHandler, metrics)
|
|
|
|
r = httptest.NewRequest("GET", "/", nil)
|
|
w = httptest.NewRecorder()
|
|
|
|
if err := ih.ServeHTTP(w, r); err == nil {
|
|
t.Errorf("expected error to be propagated")
|
|
}
|
|
|
|
expected := `
|
|
# HELP caddy_http_request_duration_seconds Histogram of round-trip request durations.
|
|
# TYPE caddy_http_request_duration_seconds histogram
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.005"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.01"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.025"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.05"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.1"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.25"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="0.5"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="1"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="2.5"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="5"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="10"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_request_duration_seconds_count{code="429",handler="foo",method="GET",server="UNKNOWN"} 1
|
|
# 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{code="200",handler="bar",method="GET",server="UNKNOWN",le="256"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="1024"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="4096"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="16384"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="65536"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="262144"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="1.048576e+06"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="4.194304e+06"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_request_size_bytes_sum{code="200",handler="bar",method="GET",server="UNKNOWN"} 23
|
|
caddy_http_request_size_bytes_count{code="200",handler="bar",method="GET",server="UNKNOWN"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="256"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="1024"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="4096"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="16384"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="65536"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="262144"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="1.048576e+06"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="4.194304e+06"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_request_size_bytes_sum{code="200",handler="empty",method="GET",server="UNKNOWN"} 23
|
|
caddy_http_request_size_bytes_count{code="200",handler="empty",method="GET",server="UNKNOWN"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="256"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="1024"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="4096"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="16384"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="65536"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="262144"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="1.048576e+06"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="4.194304e+06"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_request_size_bytes_sum{code="429",handler="foo",method="GET",server="UNKNOWN"} 23
|
|
caddy_http_request_size_bytes_count{code="429",handler="foo",method="GET",server="UNKNOWN"} 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{code="200",handler="bar",method="GET",server="UNKNOWN",le="256"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="1024"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="4096"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="16384"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="65536"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="262144"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="1.048576e+06"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="4.194304e+06"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_response_size_bytes_sum{code="200",handler="bar",method="GET",server="UNKNOWN"} 12
|
|
caddy_http_response_size_bytes_count{code="200",handler="bar",method="GET",server="UNKNOWN"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="256"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="1024"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="4096"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="16384"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="65536"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="262144"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="1.048576e+06"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="4.194304e+06"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_response_size_bytes_sum{code="200",handler="empty",method="GET",server="UNKNOWN"} 0
|
|
caddy_http_response_size_bytes_count{code="200",handler="empty",method="GET",server="UNKNOWN"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="256"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="1024"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="4096"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="16384"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="65536"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="262144"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="1.048576e+06"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="4.194304e+06"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_response_size_bytes_sum{code="429",handler="foo",method="GET",server="UNKNOWN"} 0
|
|
caddy_http_response_size_bytes_count{code="429",handler="foo",method="GET",server="UNKNOWN"} 1
|
|
# HELP caddy_http_request_errors_total Number of requests resulting in middleware errors.
|
|
# TYPE caddy_http_request_errors_total counter
|
|
caddy_http_request_errors_total{handler="bar",server="UNKNOWN"} 1
|
|
caddy_http_request_errors_total{handler="foo",server="UNKNOWN"} 1
|
|
`
|
|
if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected),
|
|
"caddy_http_request_size_bytes",
|
|
"caddy_http_response_size_bytes",
|
|
// caddy_http_request_duration_seconds_sum will vary based on how long the test took to run,
|
|
// so we check just the _bucket and _count metrics
|
|
"caddy_http_request_duration_seconds_bucket",
|
|
"caddy_http_request_duration_seconds_count",
|
|
"caddy_http_request_errors_total",
|
|
); err != nil {
|
|
t.Errorf("received unexpected error: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestMetricsInstrumentedHandlerPerHost(t *testing.T) {
|
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
metrics := &Metrics{
|
|
PerHost: true,
|
|
ObserveCatchallHosts: true, // Allow all hosts for testing
|
|
init: sync.Once{},
|
|
httpMetrics: &httpMetrics{},
|
|
allowedHosts: make(map[string]struct{}),
|
|
}
|
|
handlerErr := errors.New("oh noes")
|
|
response := []byte("hello world!")
|
|
h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 1.0 {
|
|
t.Errorf("Not same: expected %#v, but got %#v", 1.0, actual)
|
|
}
|
|
if handlerErr == nil {
|
|
w.Write(response)
|
|
}
|
|
return handlerErr
|
|
})
|
|
|
|
ih := newMetricsInstrumentedRoute(ctx, "bar", h, metrics)
|
|
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
if actual := ih.ServeHTTP(w, r); actual != handlerErr {
|
|
t.Errorf("Not same: expected %#v, but got %#v", handlerErr, actual)
|
|
}
|
|
if actual := testutil.ToFloat64(metrics.httpMetrics.requestInFlight); actual != 0.0 {
|
|
t.Errorf("Not same: expected %#v, but got %#v", 0.0, actual)
|
|
}
|
|
|
|
handlerErr = nil
|
|
if err := ih.ServeHTTP(w, r); err != nil {
|
|
t.Errorf("Received unexpected error: %v", err)
|
|
}
|
|
|
|
// an empty handler - no errors, no header written
|
|
emptyHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
return nil
|
|
})
|
|
ih = newMetricsInstrumentedRoute(ctx, "empty", emptyHandler, metrics)
|
|
r = httptest.NewRequest("GET", "/", nil)
|
|
w = httptest.NewRecorder()
|
|
|
|
if err := ih.ServeHTTP(w, r); err != nil {
|
|
t.Errorf("Received unexpected error: %v", err)
|
|
}
|
|
if actual := w.Result().StatusCode; actual != 200 {
|
|
t.Errorf("Not same: expected status code %#v, but got %#v", 200, actual)
|
|
}
|
|
if actual := w.Result().Header; len(actual) != 0 {
|
|
t.Errorf("Not empty: expected headers to be empty, but got %#v", actual)
|
|
}
|
|
|
|
// handler returning an error with an HTTP status
|
|
errHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
return Error(http.StatusTooManyRequests, nil)
|
|
})
|
|
|
|
ih = newMetricsInstrumentedRoute(ctx, "foo", errHandler, metrics)
|
|
|
|
r = httptest.NewRequest("GET", "/", nil)
|
|
w = httptest.NewRecorder()
|
|
|
|
if err := ih.ServeHTTP(w, r); err == nil {
|
|
t.Errorf("expected error to be propagated")
|
|
}
|
|
|
|
expected := `
|
|
# HELP caddy_http_request_duration_seconds Histogram of round-trip request durations.
|
|
# TYPE caddy_http_request_duration_seconds histogram
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.005"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.01"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.025"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.05"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.1"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.25"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="0.5"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="1"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="2.5"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="5"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="10"} 1
|
|
caddy_http_request_duration_seconds_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_request_duration_seconds_count{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN"} 1
|
|
# 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{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="256"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="1024"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="4096"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="16384"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="65536"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="262144"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="1.048576e+06"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="4.194304e+06"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_request_size_bytes_sum{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN"} 23
|
|
caddy_http_request_size_bytes_count{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="256"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="1024"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="4096"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="16384"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="65536"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="262144"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="1.048576e+06"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="4.194304e+06"} 1
|
|
caddy_http_request_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_request_size_bytes_sum{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN"} 23
|
|
caddy_http_request_size_bytes_count{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="256"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="1024"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="4096"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="16384"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="65536"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="262144"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="1.048576e+06"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="4.194304e+06"} 1
|
|
caddy_http_request_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_request_size_bytes_sum{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN"} 23
|
|
caddy_http_request_size_bytes_count{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN"} 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{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="256"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="1024"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="4096"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="16384"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="65536"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="262144"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="1.048576e+06"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="4.194304e+06"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_response_size_bytes_sum{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN"} 12
|
|
caddy_http_response_size_bytes_count{code="200",handler="bar",host="example.com",method="GET",server="UNKNOWN"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="256"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="1024"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="4096"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="16384"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="65536"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="262144"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="1.048576e+06"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="4.194304e+06"} 1
|
|
caddy_http_response_size_bytes_bucket{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_response_size_bytes_sum{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN"} 0
|
|
caddy_http_response_size_bytes_count{code="200",handler="empty",host="example.com",method="GET",server="UNKNOWN"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="256"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="1024"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="4096"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="16384"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="65536"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="262144"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="1.048576e+06"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="4.194304e+06"} 1
|
|
caddy_http_response_size_bytes_bucket{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN",le="+Inf"} 1
|
|
caddy_http_response_size_bytes_sum{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN"} 0
|
|
caddy_http_response_size_bytes_count{code="429",handler="foo",host="example.com",method="GET",server="UNKNOWN"} 1
|
|
# HELP caddy_http_request_errors_total Number of requests resulting in middleware errors.
|
|
# TYPE caddy_http_request_errors_total counter
|
|
caddy_http_request_errors_total{handler="bar",host="example.com",server="UNKNOWN"} 1
|
|
caddy_http_request_errors_total{handler="foo",host="example.com",server="UNKNOWN"} 1
|
|
`
|
|
if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected),
|
|
"caddy_http_request_size_bytes",
|
|
"caddy_http_response_size_bytes",
|
|
// caddy_http_request_duration_seconds_sum will vary based on how long the test took to run,
|
|
// so we check just the _bucket and _count metrics
|
|
"caddy_http_request_duration_seconds_bucket",
|
|
"caddy_http_request_duration_seconds_count",
|
|
"caddy_http_request_errors_total",
|
|
); err != nil {
|
|
t.Errorf("received unexpected error: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestMetricsCardinalityProtection(t *testing.T) {
|
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
|
|
// Test 1: Without AllowCatchAllHosts, arbitrary hosts should be mapped to "_other"
|
|
metrics := &Metrics{
|
|
PerHost: true,
|
|
ObserveCatchallHosts: false, // Default - should map unknown hosts to "_other"
|
|
init: sync.Once{},
|
|
httpMetrics: &httpMetrics{},
|
|
allowedHosts: make(map[string]struct{}),
|
|
}
|
|
|
|
// Add one allowed host
|
|
metrics.allowedHosts["allowed.com"] = struct{}{}
|
|
|
|
h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
w.Write([]byte("hello"))
|
|
return nil
|
|
})
|
|
|
|
ih := newMetricsInstrumentedRoute(ctx, "test", h, metrics)
|
|
|
|
// Test request to allowed host
|
|
r1 := httptest.NewRequest("GET", "http://allowed.com/", nil)
|
|
r1.Host = "allowed.com"
|
|
w1 := httptest.NewRecorder()
|
|
ih.ServeHTTP(w1, r1)
|
|
|
|
// Test request to unknown host (should be mapped to "_other")
|
|
r2 := httptest.NewRequest("GET", "http://attacker.com/", nil)
|
|
r2.Host = "attacker.com"
|
|
w2 := httptest.NewRecorder()
|
|
ih.ServeHTTP(w2, r2)
|
|
|
|
// Test request to another unknown host (should also be mapped to "_other")
|
|
r3 := httptest.NewRequest("GET", "http://evil.com/", nil)
|
|
r3.Host = "evil.com"
|
|
w3 := httptest.NewRecorder()
|
|
ih.ServeHTTP(w3, r3)
|
|
|
|
// Check that metrics contain:
|
|
// - One entry for "allowed.com"
|
|
// - One entry for "_other" (aggregating attacker.com and evil.com)
|
|
expected := `
|
|
# HELP caddy_http_requests_total Counter of HTTP(S) requests made.
|
|
# TYPE caddy_http_requests_total counter
|
|
caddy_http_requests_total{handler="test",host="_other",server="UNKNOWN"} 2
|
|
caddy_http_requests_total{handler="test",host="allowed.com",server="UNKNOWN"} 1
|
|
`
|
|
|
|
if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected),
|
|
"caddy_http_requests_total",
|
|
); err != nil {
|
|
t.Errorf("Cardinality protection test failed: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestMetricsHTTPSCatchAll(t *testing.T) {
|
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
|
|
// Test that HTTPS requests allow catch-all even when AllowCatchAllHosts is false
|
|
metrics := &Metrics{
|
|
PerHost: true,
|
|
ObserveCatchallHosts: false,
|
|
hasHTTPSServer: true, // Simulate having HTTPS servers
|
|
init: sync.Once{},
|
|
httpMetrics: &httpMetrics{},
|
|
allowedHosts: make(map[string]struct{}), // Empty - no explicitly allowed hosts
|
|
}
|
|
|
|
h := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
w.Write([]byte("hello"))
|
|
return nil
|
|
})
|
|
|
|
ih := newMetricsInstrumentedRoute(ctx, "test", h, metrics)
|
|
|
|
// Test HTTPS request (should be allowed even though not in allowedHosts)
|
|
r1 := httptest.NewRequest("GET", "https://unknown.com/", nil)
|
|
r1.Host = "unknown.com"
|
|
r1.TLS = &tls.ConnectionState{} // Mark as TLS/HTTPS
|
|
w1 := httptest.NewRecorder()
|
|
ih.ServeHTTP(w1, r1)
|
|
|
|
// Test HTTP request (should be mapped to "_other")
|
|
r2 := httptest.NewRequest("GET", "http://unknown.com/", nil)
|
|
r2.Host = "unknown.com"
|
|
// No TLS field = HTTP request
|
|
w2 := httptest.NewRecorder()
|
|
ih.ServeHTTP(w2, r2)
|
|
|
|
// Check that HTTPS request gets real host, HTTP gets "_other"
|
|
expected := `
|
|
# HELP caddy_http_requests_total Counter of HTTP(S) requests made.
|
|
# TYPE caddy_http_requests_total counter
|
|
caddy_http_requests_total{handler="test",host="_other",server="UNKNOWN"} 1
|
|
caddy_http_requests_total{handler="test",host="unknown.com",server="UNKNOWN"} 1
|
|
`
|
|
|
|
if err := testutil.GatherAndCompare(ctx.GetMetricsRegistry(), strings.NewReader(expected),
|
|
"caddy_http_requests_total",
|
|
); err != nil {
|
|
t.Errorf("HTTPS catch-all test failed: %s", err)
|
|
}
|
|
}
|
|
|
|
func TestMetricsInstrumentedRoute(t *testing.T) {
|
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
m := &Metrics{
|
|
init: sync.Once{},
|
|
httpMetrics: &httpMetrics{},
|
|
}
|
|
|
|
handlerErr := errors.New("oh noes")
|
|
response := []byte("hello world!")
|
|
innerHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
if actual := testutil.ToFloat64(m.httpMetrics.requestInFlight); actual != 1.0 {
|
|
t.Errorf("Expected requestInFlight to be 1.0, got %v", actual)
|
|
}
|
|
if handlerErr == nil {
|
|
w.Write(response)
|
|
}
|
|
return handlerErr
|
|
})
|
|
|
|
ih := newMetricsInstrumentedRoute(ctx, "test_handler", innerHandler, m)
|
|
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
// Test with error
|
|
if actual := ih.ServeHTTP(w, r); actual != handlerErr {
|
|
t.Errorf("Expected error %v, got %v", handlerErr, actual)
|
|
}
|
|
if actual := testutil.ToFloat64(m.httpMetrics.requestInFlight); actual != 0.0 {
|
|
t.Errorf("Expected requestInFlight to be 0.0 after request, got %v", actual)
|
|
}
|
|
if actual := testutil.ToFloat64(m.httpMetrics.requestErrors); actual != 1.0 {
|
|
t.Errorf("Expected requestErrors to be 1.0, got %v", actual)
|
|
}
|
|
|
|
// Test without error
|
|
handlerErr = nil
|
|
w = httptest.NewRecorder()
|
|
if err := ih.ServeHTTP(w, r); err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func BenchmarkMetricsInstrumentedRoute(b *testing.B) {
|
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
m := &Metrics{
|
|
init: sync.Once{},
|
|
httpMetrics: &httpMetrics{},
|
|
}
|
|
|
|
noopHandler := HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
w.Write([]byte("ok"))
|
|
return nil
|
|
})
|
|
|
|
ih := newMetricsInstrumentedRoute(ctx, "bench_handler", noopHandler, m)
|
|
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
ih.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
// BenchmarkSingleRouteMetrics simulates the new behavior where metrics
|
|
// are collected once for the entire route.
|
|
func BenchmarkSingleRouteMetrics(b *testing.B) {
|
|
ctx, _ := caddy.NewContext(caddy.Context{Context: context.Background()})
|
|
m := &Metrics{
|
|
init: sync.Once{},
|
|
httpMetrics: &httpMetrics{},
|
|
}
|
|
|
|
// Build a chain of 5 plain middleware handlers (no per-handler metrics)
|
|
var next Handler = HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
return nil
|
|
})
|
|
for i := 0; i < 5; i++ {
|
|
capturedNext := next
|
|
next = HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
|
|
return capturedNext.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// Wrap the entire chain with a single route-level metrics handler
|
|
ih := newMetricsInstrumentedRoute(ctx, "handler", next, m)
|
|
|
|
r := httptest.NewRequest("GET", "/", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
ih.ServeHTTP(w, r)
|
|
}
|
|
}
|