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) } }) } }