diff --git a/examples/custom-instrumentation/main.go b/examples/custom-instrumentation/main.go index 53a0cc35..f9e0320e 100644 --- a/examples/custom-instrumentation/main.go +++ b/examples/custom-instrumentation/main.go @@ -2,6 +2,7 @@ package main import ( + "context" "fmt" "log" "net/http" @@ -23,7 +24,7 @@ func main() { } func datadogObserver(client statsd.ClientInterface) httpbin.Observer { - return func(result httpbin.Result) { + return func(_ context.Context, result httpbin.Result) { // Log the request log.Printf("%d %s %s %s", result.Status, result.Method, result.URI, result.Duration) diff --git a/httpbin/httpbin_test.go b/httpbin/httpbin_test.go index a168cf17..2d79346e 100644 --- a/httpbin/httpbin_test.go +++ b/httpbin/httpbin_test.go @@ -1,6 +1,7 @@ package httpbin import ( + "context" "fmt" "net/http" "net/http/httptest" @@ -29,7 +30,7 @@ func TestNewOptions(t *testing.T) { t.Parallel() maxDuration := 1 * time.Second maxBodySize := int64(1024) - observer := func(_ Result) {} + observer := func(_ context.Context, _ Result) {} h := New( WithMaxBodySize(maxBodySize), @@ -61,7 +62,7 @@ func TestNewObserver(t *testing.T) { expectedStatus := http.StatusTeapot observed := false - observer := func(r Result) { + observer := func(_ context.Context, r Result) { observed = true if r.Status != expectedStatus { t.Fatalf("expected result status = %d, got %d", expectedStatus, r.Status) diff --git a/httpbin/middleware.go b/httpbin/middleware.go index b8ad5611..01fda345 100644 --- a/httpbin/middleware.go +++ b/httpbin/middleware.go @@ -117,37 +117,50 @@ func observe(o Observer, h http.Handler) http.Handler { mw := &metaResponseWriter{w: w} t := time.Now() h.ServeHTTP(mw, r) - o(Result{ - Status: mw.Status(), - Method: r.Method, - URI: r.URL.RequestURI(), - Size: mw.Size(), - Duration: time.Since(t), - UserAgent: r.Header.Get("User-Agent"), - ClientIP: getClientIP(r), + o(r.Context(), Result{ + Status: mw.Status(), + Method: r.Method, + URI: r.URL.RequestURI(), + Route: r.Pattern, + Size: mw.Size(), + Duration: time.Since(t), + UserAgent: r.Header.Get("User-Agent"), + ClientIP: getClientIP(r), + RequestSize: r.ContentLength, }) }) } -// Result is the result of handling a request, used for instrumentation +// Result records the details of an incoming request and the resulting +// response, for instrumentation via an [Observer]. type Result struct { Status int Method string URI string - Size int64 Duration time.Duration UserAgent string ClientIP string + + // Response size in bytes written. TODO: consider rename to ResponseSize + // for clarity/consistency with RequestSize. + Size int64 + + // Matched net/http handler route pattern, via [Request.Pattern]: + // https://pkg.go.dev/net/http#Request + Route string + + // Request size via, incoming Content-Length header. + RequestSize int64 } // Observer is a function that will be called with the details of a handled // request, which can be used for logging, instrumentation, etc -type Observer func(result Result) +type Observer func(ctx context.Context, result Result) // StdLogObserver creates an Observer that will log each request in structured // format using the given stdlib logger func StdLogObserver(l *slog.Logger) Observer { - return func(result Result) { + return func(ctx context.Context, result Result) { logLevel := slog.LevelInfo if result.Status >= 500 { logLevel = slog.LevelError @@ -155,16 +168,18 @@ func StdLogObserver(l *slog.Logger) Observer { logLevel = slog.LevelWarn } l.LogAttrs( - context.Background(), + ctx, logLevel, fmt.Sprintf("%d %s %s %.1fms", result.Status, result.Method, result.URI, result.Duration.Seconds()*1e3), slog.Int("status", result.Status), slog.String("method", result.Method), slog.String("uri", result.URI), + slog.String("route", result.Route), slog.Int64("size_bytes", result.Size), slog.Float64("duration_ms", result.Duration.Seconds()*1e3), slog.String("user_agent", result.UserAgent), slog.String("client_ip", result.ClientIP), + slog.Int64("request_size_bytes", result.RequestSize), ) } } diff --git a/httpbin/middleware_test.go b/httpbin/middleware_test.go index 85886ae1..781a26b3 100644 --- a/httpbin/middleware_test.go +++ b/httpbin/middleware_test.go @@ -1,6 +1,7 @@ package httpbin import ( + "context" "net/http" "net/http/httptest" "testing" @@ -17,7 +18,7 @@ func TestTestMode(t *testing.T) { // will cause a panic. This happens most often when we forget to return // early after writing an error response, and has helped identify and fix // some subtly broken error handling. - observer := func(_ Result) {} + observer := func(_ context.Context, _ Result) {} handler := observe(observer, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusOK)