Structured Logging Done Right
Why structured logs matter and how to implement them without overcomplicating things.
Key Insights
- Structured logs (JSON) are for querying; if you ever need to search your logs at scale, structure them from the start
- Log at boundaries (HTTP handlers, queue consumers, jobs) with consistent field names and include request/user context
- Go’s standard library slog package (1.21+) provides structured logging without third-party dependencies
Plain text logs are fine for reading. Structured logs are for querying. If you ever need to search your logs (you will), structure them from the start.
What Structured Logging Looks Like
Instead of:
2025-12-22 10:30:15 ERROR Failed to process order 12345 for user john@example.com: insufficient funds
Emit:
{"time":"2025-12-22T10:30:15Z","level":"error","msg":"failed to process order","order_id":"12345","user":"john@example.com","reason":"insufficient_funds"}
Go’s slog Package
Since Go 1.21, the standard library includes structured logging:
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
logger.Info("order processed",
slog.String("order_id", order.ID),
slog.Int("items", len(order.Items)),
slog.Duration("duration", elapsed),
)
Key Principles
- Log at boundaries: HTTP handlers, queue consumers, scheduled jobs
- Include context: Request IDs, user IDs, operation names
- Use consistent field names:
user_ideverywhere, not sometimesuserId - Log the outcome, not the attempt: “order processed” not “processing order”
- Levels matter: ERROR for things that need attention, INFO for business events, DEBUG for development
What Not to Log
- Passwords, tokens, or secrets
- Full request/response bodies in production
- Every iteration of a loop
- Things you’ll never search for
Structured logging is a foundation for observability. Get it right early and debugging production issues becomes significantly easier.