package httpmw import ( "net/http" "time" "code.nochebuena.dev/go/logz" ) // Logger is the minimal interface httpmw needs — satisfied by logz.Logger via duck typing. // httpmw does NOT duck-type the Logger for context purposes (it imports logz for // logz.WithRequestID / logz.GetRequestID context helpers). type Logger interface { Info(msg string, args ...any) Error(msg string, err error, args ...any) With(args ...any) Logger } // StatusRecorder wraps http.ResponseWriter to expose the written status code. type StatusRecorder struct { http.ResponseWriter Status int } func (r *StatusRecorder) WriteHeader(code int) { r.Status = code r.ResponseWriter.WriteHeader(code) } // logzLogger is the subset we need from logz.Logger for RequestLogger. // We accept the Logger interface (duck-typed) but need to also use logz.GetRequestID. // Those two concerns are separate: Logger injection = duck-typed; context helpers = logz import. // RequestLogger logs each request with method, path, status code, latency, and request ID. func RequestLogger(logger Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rec := &StatusRecorder{ResponseWriter: w, Status: http.StatusOK} start := time.Now() next.ServeHTTP(rec, r) latency := time.Since(start) reqID := logz.GetRequestID(r.Context()) if rec.Status >= 500 { logger.Error("http: request", nil, "method", r.Method, "path", r.URL.Path, "status", rec.Status, "latency", latency.String(), "request_id", reqID, ) } else { logger.Info("http: request", "method", r.Method, "path", r.URL.Path, "status", rec.Status, "latency", latency.String(), "request_id", reqID, ) } }) } }