middleware/http/accesslog.go

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
}