Skip to content
Merged
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
3 changes: 2 additions & 1 deletion examples/custom-instrumentation/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package main

import (
"context"
"fmt"
"log"
"net/http"
Expand All @@ -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)

Expand Down
5 changes: 3 additions & 2 deletions httpbin/httpbin_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package httpbin

import (
"context"
"fmt"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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)
Expand Down
41 changes: 28 additions & 13 deletions httpbin/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,54 +117,69 @@ 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
} else if result.Status >= 400 && result.Status < 500 {
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),
)
}
}
3 changes: 2 additions & 1 deletion httpbin/middleware_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package httpbin

import (
"context"
"net/http"
"net/http/httptest"
"testing"
Expand All @@ -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)
Expand Down
Loading