package chimw
import (
"io"
"log/slog"
"net/http"
"testing"
"github.com/go-chi/chi/v5"
chimiddleware "github.com/go-chi/chi/v5/middleware"
)
// nullWriter is a ResponseWriter that keeps nothing; see the same type in
// middleware for why a recorder is the wrong instrument here.
type nullWriter struct{ header http.Header }
func (w *nullWriter) Header() http.Header { return w.header }
func (w *nullWriter) Write(b []byte) (int, error) { return len(b), nil }
func (w *nullWriter) WriteHeader(int) {}
// discardLogger is a real handler doing real encoding work, writing nowhere.
// The formatter's cost is the attributes it builds and the record the handler
// encodes; sending that to a buffer would grow one by b.N records and measure
// the allocator instead.
func discardLogger() *slog.Logger {
return slog.New(slog.NewJSONHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelDebug}))
}
// benchRouter mounts one handler at /page behind the middlewares the case
// wants, which is how a service installs them.
func benchRouter(f SlogFormatter, withRequestID bool) http.Handler {
r := chi.NewRouter()
if withRequestID {
r.Use(chimiddleware.RequestID)
}
r.Use(RequestLogger(f))
r.Get("/page", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ok"))
})
return r
}
// benchRequest is reused across iterations: chi routes on a copy carrying its
// own route context and leaves this value alone.
func benchRequest(b *testing.B, path string) *http.Request {
b.Helper()
r, err := http.NewRequest(http.MethodGet, "http://bench.example.org"+path, nil)
if err != nil {
b.Fatalf("building the request: %v", err)
}
return r
}
// BenchmarkRequestLogger is the outermost middleware of every service on the
// instance: one record per request, built and encoded on the request's own
// goroutine before it returns.
//
// The three cases are the three shapes that exist in production. "logged" is
// the ordinary request. "with_request_id" adds chi's RequestID above, which is
// how these routers are actually wired and which costs a context lookup plus a
// sixth attribute. "skipped" is a probe hitting /healthz every second — it must
// stay far cheaper than the other two, because that is the entire reason Skip
// exists.
func BenchmarkRequestLogger(b *testing.B) {
logger := discardLogger()
b.Run("logged", func(b *testing.B) {
h := benchRouter(SlogFormatter{Logger: logger}, false)
r := benchRequest(b, "/page")
b.ReportAllocs()
for b.Loop() {
h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r)
}
})
b.Run("with_request_id", func(b *testing.B) {
h := benchRouter(SlogFormatter{Logger: logger}, true)
r := benchRequest(b, "/page")
b.ReportAllocs()
for b.Loop() {
h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r)
}
})
b.Run("skipped", func(b *testing.B) {
h := benchRouter(SlogFormatter{
Logger: logger,
Skip: SkipPaths("/page"),
}, false)
r := benchRequest(b, "/page")
b.ReportAllocs()
for b.Loop() {
h.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, r)
}
})
}
// BenchmarkGetHead measures the pair a page route registers: the GET a viewer
// makes and the HEAD a monitor or a proxy makes through the same handler. Both
// go through the routing tree this package writes into, so this is the cost of
// the convenience rather than of chi.
func BenchmarkGetHead(b *testing.B) {
r := chi.NewRouter()
GetHead(r, "/page", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ok"))
})
for _, method := range []string{http.MethodGet, http.MethodHead} {
b.Run(method, func(b *testing.B) {
req, err := http.NewRequest(method, "http://bench.example.org/page", nil)
if err != nil {
b.Fatalf("building the request: %v", err)
}
b.ReportAllocs()
for b.Loop() {
r.ServeHTTP(&nullWriter{header: make(http.Header, 4)}, req)
}
})
}
}