package server import ( "log/slog" "net/http" "strings" "time" "chat-agent-system/internal/httpx" ) // accessLogMiddleware emits one structured line per HTTP request when enabled. // It always stamps X-Request-ID so later error writers can reuse it. // It never logs bodies, Authorization, Cookie, or raw query strings. func (s *Server) accessLogMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestID := httpx.RequestID(r) w.Header().Set(httpx.HeaderRequestID, requestID) if s == nil || s.cfg == nil || !s.cfg.HTTPAccessLogEnabled { next.ServeHTTP(w, r) return } started := time.Now() rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, r) slog.Info("http_access", "method", r.Method, "path", accessLogPath(r.URL.Path), "status", rec.status, "duration_ms", time.Since(started).Milliseconds(), "request_id", requestID, "err_code", rec.Header().Get(httpx.HeaderErrorCode), ) }) } type statusRecorder struct { http.ResponseWriter status int wroteHeader bool } func (r *statusRecorder) WriteHeader(code int) { if r.wroteHeader { return } r.wroteHeader = true r.status = code r.ResponseWriter.WriteHeader(code) } func (r *statusRecorder) Write(b []byte) (int, error) { if !r.wroteHeader { r.WriteHeader(http.StatusOK) } return r.ResponseWriter.Write(b) } // accessLogPath keeps only the URL path. Query strings are dropped so tokens // and secrets in ?access_token=… never reach the log line. func accessLogPath(path string) string { path = strings.TrimSpace(path) if path == "" { return "/" } if i := strings.IndexByte(path, '?'); i >= 0 { path = path[:i] } if i := strings.IndexByte(path, '#'); i >= 0 { path = path[:i] } return path } // accessLogSafeMessage is used by tests to assert the middleware contract. func accessLogSafeMessage(method, path string, status int, durationMs int64, requestID, errCode string) map[string]any { msg := map[string]any{ "method": method, "path": accessLogPath(path), "status": status, "duration_ms": durationMs, "request_id": requestID, } if errCode != "" { msg["err_code"] = errCode } return msg }