Logging in a microservices architecture isn't just about sprinkling console.log statements across your codebase. For experienced developers, it's a critical component of observability that can make or break your ability to debug, monitor, and optimize distributed systems. The naive approach of dumping everything into a centralized log aggregator leads to noise, cost overhead, and slow diagnosis. Instead, you need structured, contextual, and level-headed logging that aligns with your system's complexity.
First, ditch unstructured logs. Using JSON or equivalent key-value pairs in each log entry allows tools like Elasticsearch, Loki, or Datadog to parse and query efficiently. But structure alone isn't enough—every log must carry context: trace IDs, service names, request IDs, and latency data. In Go, this is straightforward with middleware. In Python, libraries like structlog enforce structure. The goal is to make every log entry an event with enough metadata to reconstruct the request flow without grepping timestamp ranges.
Second, embrace log levels with purpose. INFO is for business-relevant events, not every HTTP hit. DEBUG should be togglable at runtime via environment variables or feature flags, not baked into code. ERROR must indicate a symptom that needs human attention, not just exception stack traces for expected failures. Implement a standard for what each level means across your services; inconsistency breeds confusion.
Third, consider sampling. In high-throughput systems, logging every request is unsustainable. Implement dynamic sampling—log the first few events of a pattern, then back off. Or use head-based sampling with trace ID decisions. This reduces storage costs while preserving key data for anomalies. For essential events like payment failures or auth misconfigurations, always log fully.
Your logging framework should also support correlation. Use OpenTelemetry to propagate context across service boundaries. Here's a concise Go example that demonstrates structured, contextual logging with a trace-aware middleware:
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) {
traceID := r.Header.Get("X-Trace-ID")
if traceID == "" {
traceID = uuid.New().String()
}
l := logger.With( slog.String("trace_id", traceID), slog.String("method", r.Method), slog.String("path", r.URL.Path))
l.Info("request started")
start := time.Now()
next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "logger", l)))
l.Info("request completed", slog.Duration("duration", time.Since(start)))
})
}
}
This injects a trace ID and persists it via context, so downstream handlers log consistently without manual propagation. Libraries like slog in Go 1.21 integrate well with structured backends and are intentionally minimal. Use that.
Finally, avoid side effects in log production. Never block on disk writes or sinks. Use async, buffer, and batch. In critical path code, do not format log messages before checking the log level—lazy evaluation matters in hot paths.
Logging is not an afterthought. It's a first-class concern in microservices design. Done right, it accelerates debugging, reduces MTTR, and keeps your ops team sane. Stop treating it as a firehose—make it a precision instrument.

Top comments (0)