Files
Agent-System/internal/server/access_log_middleware.go
T

83 lines
2.0 KiB
Go

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 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) {
if s == nil || s.cfg == nil || !s.cfg.HTTPAccessLogEnabled {
next.ServeHTTP(w, r)
return
}
started := time.Now()
requestID := httpx.RequestID(r)
w.Header().Set(httpx.HeaderRequestID, requestID)
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,
)
})
}
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 string) map[string]any {
return map[string]any{
"method": method,
"path": accessLogPath(path),
"status": status,
"duration_ms": durationMs,
"request_id": requestID,
}
}