83 lines
2.0 KiB
Go
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,
|
|
}
|
|
}
|