What is structured logging, and how does log/slog support it?

5 minintermediategologgingslogobservability

Quick Answer

Structured logging means emitting log entries as machine-parseable key-value data (commonly JSON), rather than free-form text strings, so log aggregation tools can filter, search, and alert on specific fields reliably instead of parsing unstructured text with fragile regexes. Go's standard library added log/slog in Go 1.21 specifically for this: slog.Info("user logged in", "user_id", 42, "method", "oauth") produces a structured record with user_id and method as queryable fields, not just baked into a message string. slog supports pluggable handlers (JSON output, human-readable text output, or a custom handler shipping to an external system), and structured, leveled logging (slog.Debug, Info, Warn, Error) that integrates cleanly with most log aggregation platforms out of the box.

Detailed Answer

Before slog (added in Go 1.21), structured logging in Go meant reaching for a third-party library like zap or logrus. Now it's built directly into the standard library.

Unstructured vs. structured logging

// Unstructured: hard to query reliably at scale
log.Printf("user %d logged in via %s", userID, method)
// "user 42 logged in via oauth" — good luck filtering by user_id in a log aggregator
// Structured: user_id and method are real, queryable fields
slog.Info("user logged in", "user_id", 42, "method", "oauth")

JSON output for production log aggregation

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)

slog.Info("user logged in", "user_id", 42, "method", "oauth")
{"time":"2024-01-15T10:30:00Z","level":"INFO","msg":"user logged in","user_id":42,"method":"oauth"}

Most log aggregation platforms (Datadog, Elasticsearch, CloudWatch) can index this directly, letting you filter on user_id=42 reliably, something regex-parsing a free-form message string can never do robustly.

Leveled logging

Four severity levels cover most needs:

slog.Debug("cache miss", "key", cacheKey)
slog.Info("request handled", "path", r.URL.Path, "status", 200)
slog.Warn("retrying request", "attempt", 3)
slog.Error("database connection failed", "error", err)

Attaching persistent context to a logger

slog.With avoids repeating the same fields on every call:

requestLogger := slog.With("request_id", reqID, "user_id", userID)
requestLogger.Info("processing request")
requestLogger.Info("request completed", "duration_ms", 42)

slog.With returns a new logger carrying those fields on every subsequent call, so you don't have to repeat request_id/user_id manually on every log line within a single request's handling.

Why this matters in production

Structured logs are what make real observability tooling (dashboards, alerts, log-based metrics) actually work reliably. A log aggregator can build a dashboard of "errors by user_id" trivially from structured JSON fields. It essentially can't do that robustly from free-text log messages with inconsistent formatting across a codebase.

Related Resources