rdev/internal/logging/middleware.go
jordan d69da6d627 feat: add structured logging infrastructure and SDLC extensions
Major changes:
- Add internal/logging package with field constants, context propagation,
  sensitive data auto-redaction, and per-component log levels
- Add worker timeout constants (TimeoutQuickOp, TimeoutHealthCheck, etc.)
- Extend SDLC with callback handlers, generate endpoints, and executor
- Add new cookbook trees for aeries and slackpath progression
- Add skeleton templates for queue, realtime, and microservices
- Add worker component template with async job processing
- Refactor services and handlers to use new logging infrastructure
- Split component.go into component_infra.go and component_listing.go

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 22:56:04 -07:00

146 lines
3.6 KiB
Go

package logging
import (
"net/http"
"time"
"github.com/google/uuid"
)
// responseWriter wraps http.ResponseWriter to capture status code.
type responseWriter struct {
http.ResponseWriter
status int
wroteHeader bool
bytesWritten int
}
func (rw *responseWriter) WriteHeader(code int) {
if rw.wroteHeader {
return
}
rw.status = code
rw.wroteHeader = true
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
if !rw.wroteHeader {
rw.WriteHeader(http.StatusOK)
}
n, err := rw.ResponseWriter.Write(b)
rw.bytesWritten += n
return n, err
}
// Unwrap returns the original http.ResponseWriter, required for http.ResponseController.
func (rw *responseWriter) Unwrap() http.ResponseWriter {
return rw.ResponseWriter
}
// RequestIDHeader is the header name for request ID propagation.
const RequestIDHeader = "X-Request-ID"
// MiddlewareConfig configures the logging middleware.
type MiddlewareConfig struct {
// Logger is the logger to use. If nil, uses Default().
Logger *Logger
// SkipPaths are paths that should not be logged.
SkipPaths map[string]bool
// GenerateRequestID controls whether to generate a request ID if not present.
GenerateRequestID bool
}
// DefaultMiddlewareConfig returns sensible defaults.
func DefaultMiddlewareConfig() MiddlewareConfig {
return MiddlewareConfig{
SkipPaths: map[string]bool{
"/health": true,
"/ready": true,
},
GenerateRequestID: true,
}
}
// Middleware returns an HTTP middleware that logs requests and enriches context.
// It:
// - Generates or propagates request IDs
// - Logs request start (debug) and completion (info/warn/error based on status)
// - Stores an enriched logger in context for handlers to use
func Middleware(cfg MiddlewareConfig) func(http.Handler) http.Handler {
logger := cfg.Logger
if logger == nil {
logger = Default()
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip logging for configured paths
if cfg.SkipPaths[r.URL.Path] {
next.ServeHTTP(w, r)
return
}
start := time.Now()
// Get or generate request ID
requestID := r.Header.Get(RequestIDHeader)
if requestID == "" && cfg.GenerateRequestID {
requestID = uuid.New().String()
}
// Set request ID in response header
if requestID != "" {
w.Header().Set(RequestIDHeader, requestID)
}
// Wrap response writer to capture status code and bytes
wrapped := &responseWriter{
ResponseWriter: w,
status: http.StatusOK,
}
// Create request-scoped logger
reqLogger := logger.With(
FieldRequestID, requestID,
FieldHTTPMethod, r.Method,
FieldHTTPPath, r.URL.Path,
FieldHTTPRemoteAddr, r.RemoteAddr,
)
// Store logger in context for handlers to use
ctx := WithContext(r.Context(), reqLogger)
// Log request start at debug level
reqLogger.Debug("request started",
FieldHTTPUserAgent, r.UserAgent(),
FieldHTTPHost, r.Host,
)
// Call next handler with enriched context
next.ServeHTTP(wrapped, r.WithContext(ctx))
// Calculate duration
duration := time.Since(start)
// Log completion with appropriate level based on status
attrs := []any{
FieldHTTPStatus, wrapped.status,
FieldDuration, duration.Milliseconds(),
"bytes", wrapped.bytesWritten,
}
switch {
case wrapped.status >= 500:
reqLogger.Error("request completed", attrs...)
case wrapped.status >= 400:
reqLogger.Warn("request completed", attrs...)
default:
reqLogger.Info("request completed", attrs...)
}
})
}
}