#golang#logging#slog#observability#clean-code

Modern Structured Logging in Go with log/slog

"Complete guide to structured logging with Go 1.21+ log/slog: JSON handlers, performance tuning, context propagation, and data redaction."

By huud
0 views
~3 min read
Modern Structured Logging in Go with log/slog

Modern Structured Logging in Go with log/slog

With the release of Go 1.21, the standard library introduced log/slog, bringing native, high-performance structured logging to the standard distribution and eliminating the mandatory requirement for third-party dependencies like uber-go/zap or rs/zerolog in most projects.

Structured logging outputs machine-readable key-value pairs (typically JSON), making logs indexable and queryable in tools like Datadog, ElasticSearch, and Grafana Loki.


Core Concepts of log/slog

log/slog separates logging into two main abstractions:

  1. Logger: The frontend API providing log methods (Info, Warn, Error, Debug).
  2. Handler: The backend processor that formats records into text, JSON, or downstream network streams.
less
slog.InfoContext(ctx, "msg", attrs...)
              │
              ▼
       [ slog.Record ]
              │
              ▼
   [ slog.Handler (JSON / Text) ]
              │
              ▼
   [ stdout / io.Writer ]

Setting Up JSON Structured Logging

go
package main

import (
	"context"
	"log/slog"
	"os"
)

func main() {
	opts := &slog.HandlerOptions{
		Level:     slog.LevelInfo,
		AddSource: true, // Includes file name and line number
	}

	// Create JSON handler targeting stdout
	handler := slog.NewJSONHandler(os.Stdout, opts)
	logger := slog.New(handler)

	// Set as global default logger
	slog.SetDefault(logger)

	slog.Info("server starting",
		"port", 8080,
		"env", "production",
	)
}

JSON Output

json
{
  "time": "2026-09-02T12:00:00.000Z",
  "level": "INFO",
  "source": {"function": "main.main", "file": "main.go", "line": 21},
  "msg": "server starting",
  "port": 8080,
  "env": "production"
}

Strongly-Typed Attributes vs. Loose Key-Values

While slog.Info("msg", "key", "value") is convenient, it allocates memory due to any boxing. In performance-critical hot paths, use strongly-typed attributes:

go
// Loosely typed (heap escapes)
slog.Info("user logged in", "user_id", 42, "role", "admin")

// Strongly typed (zero allocations for primitives)
slog.Info("user logged in",
    slog.Int64("user_id", 42),
    slog.String("role", "admin"),
)

Masking Sensitive Data with slog.LogValuer

In compliance environments (PCI-DSS, GDPR, HIPAA), credentials, passwords, and API tokens must never appear in clear text. slog provides the LogValuer interface to customize serialization:

go
package auth

import "log/slog"

type Password string

// LogValue implements slog.LogValuer to prevent accidental leaking
func (p Password) LogValue() slog.Value {
	return slog.StringValue("REDACTED")
}

type User struct {
	ID       string
	Email    string
	Password Password
}

func (u User) LogValue() slog.Value {
	return slog.GroupValue(
		slog.String("id", u.ID),
		slog.String("email", u.Email),
		slog.Any("password", u.Password), // Will output REDACTED
	)
}

Context-Aware Logging Middleware

A standard requirement in backend microservices is attaching request-scoped TraceID and TenantID to every log line without passing arguments manually.

go
package logging

import (
	"context"
	"log/slog"
)

type ContextHandler struct {
	slog.Handler
}

func (h *ContextHandler) Handle(ctx context.Context, r slog.Record) error {
	// Extract TraceID if present in context
	if traceID, ok := ctx.Value("trace_id").(string); ok {
		r.AddAttrs(slog.String("trace_id", traceID))
	}
	return h.Handler.Handle(ctx, r)
}

Performance Comparison: slog vs zap vs zerolog

text
BenchmarkJSONLogging/slog-16       8,901,234    134 ns/op    0 allocs/op (with Attrs)
BenchmarkJSONLogging/zap-16       10,210,432    118 ns/op    0 allocs/op
BenchmarkJSONLogging/zerolog-16   11,450,120     98 ns/op    0 allocs/op

Recommendation

For 95% of applications, log/slog in the standard library delivers top-tier performance with zero external dependency baggage. For ultra-low latency requirements, zap or zerolog remain slightly faster due to manual buffer pooling optimizations.


Conclusion

With log/slog, Go finally provides an enterprise-ready, context-aware structured logger built right into the runtime. Standardize on slog, leverage slog.LogValuer for security redaction, and pass context.Context through every log call.

About the Author

huud

huud

@huud

About →

Systems architect and software engineer building high-performance distributed platforms.