Frontend Labs
⋯

Day 84 — Structured Logging with slog

Learning Objectives

  • Migrate all fmt.Printf to log/slog with 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.


Generated from week12.md · 8 Oct 2026 16:36