mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-16 16:00:03 +00:00
* feat(logging): configure levels and structured runtime output * fix(logging): bound request-derived log fields * fix(logging): retain malformed IATA source attributes safely * test(logging): capture structured REST rate-limit records
64 lines
2.0 KiB
Go
64 lines
2.0 KiB
Go
// Copyright 2026 Beacon Contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package middleware
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
chimw "github.com/go-chi/chi/v5/middleware"
|
|
)
|
|
|
|
// RequestLogger uses chi's response wrapper to preserve streaming/hijacking support.
|
|
// Place it after TrustedProxyIP and RequestID, and before Recoverer.
|
|
func RequestLogger(next http.Handler) http.Handler {
|
|
return chimw.RequestLogger(requestLogFormatter{})(next)
|
|
}
|
|
|
|
type requestLogFormatter struct{}
|
|
type requestLogEntry struct{ request *http.Request }
|
|
|
|
func (requestLogFormatter) NewLogEntry(r *http.Request) chimw.LogEntry {
|
|
return &requestLogEntry{request: r}
|
|
}
|
|
|
|
func (e *requestLogEntry) logger() *slog.Logger {
|
|
path := "unmatched"
|
|
if ctx := chi.RouteContext(e.request.Context()); ctx != nil && ctx.RoutePattern() != "" {
|
|
path = ctx.RoutePattern()
|
|
}
|
|
return slog.Default().With("component", "http", "method", singleLineLogValue(e.request.Method), "path", path,
|
|
"client_ip", singleLineLogValue(e.request.RemoteAddr), "request_id", singleLineLogValue(chimw.GetReqID(e.request.Context())))
|
|
}
|
|
|
|
// Strip line breaks before request fields reach a handler or downstream log consumer.
|
|
func singleLineLogValue(value string) string {
|
|
value = strings.ReplaceAll(value, "\r", "")
|
|
return strings.ReplaceAll(value, "\n", "")
|
|
}
|
|
|
|
func (e *requestLogEntry) Write(status, bytes int, _ http.Header, elapsed time.Duration, _ any) {
|
|
if status == 0 {
|
|
status = http.StatusOK
|
|
}
|
|
level := slog.LevelInfo
|
|
if status >= 500 {
|
|
level = slog.LevelError
|
|
} else if status >= 400 {
|
|
level = slog.LevelWarn
|
|
}
|
|
if !slog.Default().Enabled(e.request.Context(), level) {
|
|
return
|
|
}
|
|
e.logger().Log(e.request.Context(), level, "request complete", "status", status, "bytes", bytes, "duration_ms", float64(elapsed)/float64(time.Millisecond))
|
|
}
|
|
|
|
func (e *requestLogEntry) Panic(value any, stack []byte) {
|
|
e.logger().ErrorContext(e.request.Context(), "request panic", "panic", fmt.Sprint(value), "stack", string(stack))
|
|
}
|