Day 84 — Structured Logging with slog
Learning Objectives
- Migrate all
fmt.Printftolog/slogwith JSON output - Propagate correlation IDs through context
- Configure log levels for production (suppress DEBUG)
Exercise: Complete slog Integration
package logging
import (
"context"
"log/slog"
"os"
)
type ctxKey string
const loggerKey ctxKey = "logger"
// NewProductionLogger creates a JSON logger for production
func NewProductionLogger(level slog.Level) *slog.Logger {
opts := &slog.HandlerOptions{
Level: level,
// Add source location to all log records
AddSource: level <= slog.LevelDebug,
}
handler := slog.NewJSONHandler(os.Stdout, opts)
return slog.New(handler)
}
// WithRequestContext injects request-specific fields into the logger
func WithRequestContext(ctx context.Context, logger *slog.Logger, requestID, clientIP, method, path string) (context.Context, *slog.Logger) {
enriched := logger.With(
"request_id", requestID,
"client_ip", clientIP,
"method", method,
"path", path,
)
ctx = context.WithValue(ctx, loggerKey, enriched)
return ctx, enriched
}
// FromContext retrieves the request-scoped logger
func FromContext(ctx context.Context) *slog.Logger {
if l, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
return l
}
return slog.Default()
}
// LoggingMiddleware injects a request-scoped logger
func LoggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := GetRequestID(r.Context())
ctx, reqLogger := WithRequestContext(r.Context(), logger,
requestID, realClientIP(r), r.Method, r.URL.Path)
start := time.Now()
capture := &statusCapture{ResponseWriter: w, statusCode: 200}
next.ServeHTTP(capture, r.WithContext(ctx))
reqLogger.Info("request_complete",
"status", capture.statusCode,
"latency_ms", time.Since(start).Milliseconds(),
"bytes", capture.bytesWritten,
)
})
}
}
Usage in handlers:
func myHandler(w http.ResponseWriter, r *http.Request) {
logger := logging.FromContext(r.Context())
// logger already has request_id, client_ip, method, path
logger.Info("processing request", "user_id", getUserID(r))
logger.Debug("cache lookup", "cache_key", "user:42", "hit", true)
logger.Error("upstream error", "upstream", "db", "error", err)
}
Sample JSON output:
{"time":"2025-01-10T10:00:00Z","level":"INFO","msg":"request_complete","request_id":"a3f8c2d1","client_ip":"10.0.0.1","method":"POST","path":"/api/auth/login","status":200,"latency_ms":47,"bytes":256}
Real-World Context
Splunk, Datadog, and Elastic all parse structured JSON logs automatically. The correlation request_id field ties together WAF logs, auth logs, and application logs for a single request — critical for forensics after a security incident. slog is stdlib since Go 1.21, replacing the zerolog/zap ecosystem for new services.