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>
146 lines
3.6 KiB
Go
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...)
|
|
}
|
|
})
|
|
}
|
|
}
|