package http
import (
"net/http"
"time"
)
// AccessLog returns a middleware that emits one "http.access" record per
// request with method, path, status, bytes written, and elapsed time.
func AccessLog(fallback Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ww := &responseWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(ww, r)
dur := time.Since(start)
l := LoggerFrom(r.Context(), fallback)
fs := []Field{
{Key: "method", Value: r.Method},
{Key: "path", Value: r.URL.Path},
{Key: "status", Value: ww.status},
{Key: "bytes", Value: ww.bytes},
{Key: "duration_ms", Value: dur.Milliseconds()},
}
if ua := r.UserAgent(); ua != "" {
fs = append(fs, Field{Key: "ua", Value: ua})
}
if ww.status >= 500 {
l.Error("http.access", fs...)
} else {
l.Info("http.access", fs...)
}
})
}
}
// responseWriter captures the status code and number of bytes written so
// the access log can report them.
type responseWriter struct {
http.ResponseWriter
status int
bytes int
wrote bool
}
// WriteHeader captures the status.
func (w *responseWriter) WriteHeader(code int) {
if !w.wrote {
w.status = code
w.wrote = true
w.ResponseWriter.WriteHeader(code)
}
}
// Write records the number of bytes written.
func (w *responseWriter) Write(p []byte) (int, error) {
if !w.wrote {
w.wrote = true
}
n, err := w.ResponseWriter.Write(p)
w.bytes += n
return n, err
}
// Flush forwards to the underlying writer if supported.
func (w *responseWriter) Flush() {
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
// Hijack forwards to the underlying writer if supported. This is required
// for websocket-style handlers that upgrade the connection.
func (w *responseWriter) Hijack() (conn httpHijacker, err error) {
if h, ok := w.ResponseWriter.(http.Hijacker); ok {
c, rw, err := h.Hijack()
if err != nil {
return httpHijacker{}, err
}
return httpHijacker{Conn: c, ReadWriter: rw}, nil
}
return httpHijacker{}, http.ErrNotSupported
}
// httpHijacker bundles the values returned by http.Hijacker.Hijack so the
// wrapper can expose a single return value to callers that just need a
// yes/no check.
type httpHijacker struct {
Conn any
ReadWriter any
}