Skip to content

Golang slog Standard Library

For the longest time, I avoided using slog. The name collision created a mental blocker with so many third-party variants floating around:

It wasn't until recently that I learned slog has been part of Go's standard library for 2 years. Once I realised this, I decided to give it a serious try.

Why slog Matters

I've always liked logrus for its structured logging capabilities, but I'd move to any standard library solution in a heartbeat if it's available. The appeal is simple: fewer dependencies, long-term stability, and one less thing to worry about when dependencies evolve.

log.Printf() was never an option for production applications. Without structured logging, you're stuck with string formatting that makes parsing and querying logs painful. slog finally fills this void in the standard library.

Basic Usage

For production use, configure JSON output:

jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
logger := slog.New(jsonHandler)
logger.Info("server starting", "port", 8080)

This produces machine-parseable JSON:

{"time":"2024-09-15T10:30:00Z","level":"INFO","msg":"server starting","port":8080}

Context-Aware Logging Pattern

The real power of slog comes when you need to pass loggers through your application. This is where a thin abstraction layer becomes helpful:

package logger

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

var (
    // G is a shorthand for GetLogger
    G = GetLogger
    // L is the default logger instance
    L = slog.Default()
)

func Init() {
    jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
    slog.SetDefault(slog.New(jsonHandler))
    L = slog.Default()
}

type loggerKey struct{}

// IntoContext stores a logger in the context
func IntoContext(ctx context.Context, logger *slog.Logger) context.Context {
    return context.WithValue(ctx, loggerKey{}, logger)
}

// GetLogger retrieves logger from context.Context
// Falls back to default logger if not found
func GetLogger(ctx context.Context) *slog.Logger {
    if ctx == nil {
        return L
    }
    if logger := ctx.Value(loggerKey{}); logger != nil {
        return logger.(*slog.Logger)
    }
    return L
}

This pattern provides two key benefits:

1. Short syntax for common cases

Instead of repeatedly typing slog.Info(), you can use logger.G(ctx).Info(). The G shorthand makes it quick to type while remaining clear.

2. Request-scoped logging

You can enrich loggers with request-specific attributes and pass them through context:

func HandleRequest(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // Create a logger with request-specific fields
    reqLogger := logger.G(ctx).With(
        "request_id", uuid.New().String(),
        "path", r.URL.Path,
        "method", r.Method,
    )

    // Store it in context
    ctx = logger.IntoContext(ctx, reqLogger)

    // All downstream code gets the enriched logger
    ProcessRequest(ctx)
}

func ProcessRequest(ctx context.Context) {
    // Automatically includes request_id, path, and method
    logger.G(ctx).Info("processing started")

    // ... do work ...

    logger.G(ctx).Info("processing completed", "duration_ms", 42)
}

The output automatically includes the request context:

{"time":"2024-09-15T10:30:00Z","level":"INFO","msg":"processing started","request_id":"...","path":"/api/users","method":"GET"}
{"time":"2024-09-15T10:30:00Z","level":"INFO","msg":"processing completed","request_id":"...","path":"/api/users","method":"GET","duration_ms":42}

Common Patterns

Adding Structured Context

Instead of string formatting, use key-value pairs:

// Bad - loses structure
logger.Info(fmt.Sprintf("user %d logged in from %s", userID, ip))

// Good - maintains structure
logger.Info("user logged in", "user_id", userID, "ip", ip)

Use With() to create loggers with shared attributes:

dbLogger := logger.G(ctx).With("component", "database")
dbLogger.Info("connection established", "host", "localhost")
dbLogger.Warn("slow query detected", "duration_ms", 1500)

The Verdict

After avoiding it for years due to name confusion, I'm glad I gave slog a serious try. Having structured logging in the standard library removes a common external dependency while providing a solid foundation for modern Go applications.

The context-aware pattern shown above strikes a good balance between convenience and flexibility. It's concise enough for everyday use while powerful enough to handle complex logging requirements in production systems.

If you're still using log.Printf() or considering third-party logging libraries, give slog a look. It's been in the standard library since Go 1.21, and it's ready for production use.