// Package http provides HTTP middleware and utilities, including request logging using slog. // // The LoggingHandler middleware logs HTTP request details, response status, bytes written, // and handles panics gracefully with stack trace logging. package http import ( "bufio" "log/slog" "net" "net/http" "time" "go.sudomsg.com/kit/logging" ) type tracingWriter struct { http.ResponseWriter StatusCode int BytesWritten int Hijacked bool } var ( _ http.ResponseWriter = &tracingWriter{} _ http.Flusher = &tracingWriter{} _ http.Hijacker = &tracingWriter{} _ interface{ Unwrap() http.ResponseWriter } = &tracingWriter{} ) func (tw *tracingWriter) WriteHeader(code int) { tw.ResponseWriter.WriteHeader(code) tw.StatusCode = code } func (tw *tracingWriter) Write(b []byte) (int, error) { if tw.StatusCode == 0 { tw.WriteHeader(http.StatusOK) } n, err := tw.ResponseWriter.Write(b) tw.BytesWritten += n return n, err } func (tw *tracingWriter) Flush() { rc := http.NewResponseController(tw.ResponseWriter) _ = rc.Flush() } func (tw *tracingWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { rc := http.NewResponseController(tw.ResponseWriter) conn, rw, err := rc.Hijack() if err == nil { tw.Hijacked = true } return conn, rw, err } func (tw *tracingWriter) Unwrap() http.ResponseWriter { return tw.ResponseWriter } // LoggingHandler is an HTTP middleware that logs requests and responses using slog. // // It recovers panics during request processing, logs them with stack traces, // and returns a 500 Internal Server Error if the connection is not hijacked. type LoggingHandler struct { next http.Handler logger *slog.Logger } var _ http.Handler = &LoggingHandler{} // New creates a new LoggingHandler wrapping the next http.Handler, using the provided slog.Logger. // // If logger is nil, it attempts to obtain one from the request context. func New(next http.Handler, logger *slog.Logger) *LoggingHandler { return &LoggingHandler{ next: next, logger: logger, } } // ServeHTTP implements the http.Handler interface. // // It logs the HTTP method, path, client IP, protocol, host, user-agent, referer, // request latency, response status, and bytes written. // It also recovers panics, logs them, and returns HTTP 500 if appropriate. func (h *LoggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() logger := h.logger if logger == nil { logger = logging.FromContext(ctx) } logger = logger.With( slog.String("method", r.Method), slog.String("path", r.URL.RequestURI()), slog.String("ip", r.RemoteAddr), ) ctx = logging.WithLogger(ctx, logger) r = r.WithContext(ctx) tw := &tracingWriter{ResponseWriter: w} defer func() { if err := recover(); err != nil { logging.RecoverAndLog(ctx, "http panic", err) if !tw.Hijacked && tw.StatusCode == 0 { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } } }() start := time.Now() h.next.ServeHTTP(tw, r) if !tw.Hijacked { latency := time.Since(start) logger.LogAttrs(r.Context(), slog.LevelInfo, "HTTP request Handled", slog.String("proto", r.Proto), slog.String("host", r.Host), slog.String("user-agent", r.UserAgent()), slog.String("referer", r.Referer()), slog.Duration("latency", latency), slog.Int("status", tw.StatusCode), slog.Int("bytes", tw.BytesWritten), ) } }