or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

apidiff.mdconstraints.mdebnf.mderrors.mdevent.mdgorelease.mdindex.mdio-i2c.mdio-spi.mdjsonrpc2.mdmaps.mdmmap.mdmodgraphviz.mdrand.mdshiny.mdslices.mdslog.mdstats.mdsumdb.mdtrace.mdtxtar.mdtypeparams.mdutf8string.md
tile.json

slog.mddocs/

golang.org/x/exp/slog - Structured Logging

Structured logging provides a way to emit log records with a message, severity level, and key-value pairs representing attributes. The slog package defines a Logger type with methods like Info and Error, associated with a Handler that determines how log records are formatted and output.

Package Information

  • Package Name: golang.org/x/exp/slog
  • Package Type: Go (golang.org/x/exp)
  • Language: Go
  • Installation: go get golang.org/x/exp

Core Imports

import (
    "golang.org/x/exp/slog"
    "os"
    "context"
    "io"
)

Basic Usage

package main

import (
    "os"
    "golang.org/x/exp/slog"
)

func main() {
    // Using the default logger
    slog.Info("application started", "version", "1.0")

    // Creating a logger with a text handler
    logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
    logger.Info("hello", "count", 3)

    // Creating a logger with a JSON handler
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    jsonLogger.Info("event", "type", "login", "user_id", 42)

    // Adding context to log calls
    ctx := context.Background()
    logger.InfoContext(ctx, "processing request", "request_id", "abc123")
}

Architecture

The slog package uses a handler-based architecture where:

  1. Logger - The main entry point that records structured information about each call to its Log, Debug, Info, Warn, and Error methods.
  2. Handler - Decides how to handle each log record (format, output destination, filtering).
  3. Record - Contains the time, level, message, and attributes of a log event.
  4. Attr - Key-value pairs representing log attributes.
  5. Value - Can represent any Go value efficiently without allocation for common types.

Capabilities

Default Logger Functions

Package-level functions that operate on the default Logger:

func Debug(msg string, args ...any)

func DebugContext(ctx context.Context, msg string, args ...any)

func Info(msg string, args ...any)

func InfoContext(ctx context.Context, msg string, args ...any)

func Warn(msg string, args ...any)

func WarnContext(ctx context.Context, msg string, args ...any)

func Error(msg string, args ...any)

func ErrorContext(ctx context.Context, msg string, args ...any)

func Log(ctx context.Context, level Level, msg string, args ...any)

func LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr)

func With(args ...any) *Logger

func SetDefault(l *Logger)

Description: These functions provide convenient access to the default Logger for common logging operations. Context-aware variants ending in "Context" should be preferred when a context is available.

Usage Example:

slog.Info("user login", "user_id", 42, "ip", "192.168.1.1")
ctx := context.Background()
slog.InfoContext(ctx, "processing", "request_id", "xyz")

Logger Creation and Management

func New(h Handler) *Logger

func Default() *Logger

Description:

  • New creates a new Logger with the given non-nil Handler and a nil context.
  • Default returns the default Logger that is used by package-level functions.

Usage Example:

textHandler := slog.NewTextHandler(os.Stderr, nil)
logger := slog.New(textHandler)

defaultLogger := slog.Default()

Logger Methods

func (l *Logger) Debug(msg string, args ...any)

func (l *Logger) DebugContext(ctx context.Context, msg string, args ...any)

func (l *Logger) Info(msg string, args ...any)

func (l *Logger) InfoContext(ctx context.Context, msg string, args ...any)

func (l *Logger) Warn(msg string, args ...any)

func (l *Logger) WarnContext(ctx context.Context, msg string, args ...any)

func (l *Logger) Error(msg string, args ...any)

func (l *Logger) ErrorContext(ctx context.Context, msg string, args ...any)

func (l *Logger) Log(ctx context.Context, level Level, msg string, args ...any)

func (l *Logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr)

func (l *Logger) With(args ...any) *Logger

func (l *Logger) WithGroup(name string) *Logger

func (l *Logger) Handler() Handler

func (l *Logger) Enabled(ctx context.Context, level Level) bool

Description:

  • Convenience methods (Debug, Info, Warn, Error) log at their respective levels.
  • Context variants should be used when a context is available.
  • LogAttrs is more efficient than Log as it accepts only Attrs without requiring conversion.
  • With returns a new Logger with additional attributes that appear in all output.
  • WithGroup qualifies all attributes with a group name.
  • Handler returns the Logger's Handler.
  • Enabled checks if the Logger will emit records at the given level.

Usage Example:

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

// Simple logging
logger.Info("server started", "port", 8080)

// With context
ctx := context.WithValue(context.Background(), "request_id", "123")
logger.InfoContext(ctx, "request received")

// Using attributes
logger.LogAttrs(nil, slog.LevelInfo, "event",
    slog.Int("duration_ms", 150),
    slog.String("status", "ok"))

// Building a logger with common attributes
requestLogger := logger.With("request_id", "abc123", "user_id", 42)
requestLogger.Info("processing") // includes request_id and user_id

// Grouping related attributes
groupedLogger := logger.WithGroup("request")
groupedLogger.Info("data", "method", "GET", "path", "/api/users")

Attribute Creation

type Attr struct {
    Key   string
    Value Value
}

func Any(key string, value any) Attr

func Bool(key string, v bool) Attr

func Duration(key string, v time.Duration) Attr

func Float64(key string, v float64) Attr

func Group(key string, args ...any) Attr

func Int(key string, value int) Attr

func Int64(key string, value int64) Attr

func String(key, value string) Attr

func Time(key string, v time.Time) Attr

func Uint64(key string, v uint64) Attr

func (a Attr) Equal(b Attr) bool

func (a Attr) String() string

Description: Attr represents a key-value pair. Constructor functions exist for common types to avoid allocations. Group creates an Attr for grouping related key-value pairs.

Usage Example:

// Using constructor functions
slog.Info("event",
    slog.String("name", "login"),
    slog.Int("user_id", 42),
    slog.Duration("elapsed", 150*time.Millisecond),
    slog.Bool("success", true))

// Using Group for structured data
slog.Info("request",
    slog.Group("headers",
        "content-type", "application/json",
        "authorization", "Bearer token"))

// Using Any for flexible types
var data interface{} = []int{1, 2, 3}
slog.Info("data", slog.Any("values", data))

Levels

type Level int

const (
    LevelDebug Level = -4
    LevelInfo  Level = 0
    LevelWarn  Level = 4
    LevelError Level = 8
)

func (l Level) Level() Level

func (l Level) String() string

func (l Level) MarshalJSON() ([]byte, error)

func (l Level) MarshalText() ([]byte, error)

func (l *Level) UnmarshalJSON(data []byte) error

func (l *Level) UnmarshalText(data []byte) error

Description: A Level represents the importance or severity of a log event. Higher levels indicate more severe events. Levels can be marshaled to/from JSON and text formats.

Usage Example:

// Using predefined levels
slog.Log(nil, slog.LevelDebug, "debug message")
slog.Log(nil, slog.LevelError, "error occurred")

// Using custom levels (any int)
customLevel := slog.Level(2) // Between Info(0) and Warn(4)

// Level string representation
fmt.Println(slog.LevelWarn.String()) // "WARN"
fmt.Println((slog.LevelInfo + 2).String()) // "INFO+2"

Level Variables

type LevelVar struct {
    // Has unexported fields.
}

func (v *LevelVar) Level() Level

func (v *LevelVar) Set(l Level)

func (v *LevelVar) String() string

func (v *LevelVar) MarshalText() ([]byte, error)

func (v *LevelVar) UnmarshalText(data []byte) error

Description: LevelVar is a Level variable that allows dynamic level changes. It is safe for concurrent use from multiple goroutines and implements the Leveler interface.

Usage Example:

// Create a dynamic level
programLevel := new(slog.LevelVar) // Defaults to LevelInfo

// Create handler with dynamic level
handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: programLevel})
logger := slog.New(handler)

// Change level at runtime
programLevel.Set(slog.LevelDebug)  // Now logs debug messages
programLevel.Set(slog.LevelError)  // Now only logs errors

Leveler Interface

type Leveler interface {
    Level() Level
}

Description: Leveler provides a Level value. Both Level and LevelVar implement this interface.

Record Type

type Record struct {
    // The time at which the output method was called.
    Time time.Time

    // The log message.
    Message string

    // The level of the event.
    Level Level

    // The program counter at the time the record was constructed.
    PC uintptr
}

func NewRecord(t time.Time, level Level, msg string, pc uintptr) Record

func (r *Record) Add(args ...any)

func (r *Record) AddAttrs(attrs ...Attr)

func (r Record) Attrs(f func(Attr) bool)

func (r Record) Clone() Record

func (r Record) NumAttrs() int

Description: A Record holds information about a log event. NewRecord is intended for custom logging APIs that want to support a Handler as a backend.

Usage Example:

// Creating a record manually (for custom logging wrappers)
record := slog.NewRecord(time.Now(), slog.LevelInfo, "event occurred", 0)
record.AddAttrs(
    slog.String("type", "custom"),
    slog.Int("value", 123))

// Cloning a record to avoid shared state
originalRecord := slog.NewRecord(time.Now(), slog.LevelInfo, "msg", 0)
clonedRecord := originalRecord.Clone()

// Iterating over attributes
record.Attrs(func(attr slog.Attr) bool {
    fmt.Printf("%s: %v\n", attr.Key, attr.Value)
    return true // continue iteration
})

Source Information

type Source struct {
    // Function is the package path-qualified function name.
    Function string

    // File and Line are the file name and line number (1-based).
    File string
    Line int
}

Description: Source describes the location of a line of source code, used for capturing where a log call originated.

Value Type

type Value struct {
    // Has unexported fields.
}

func AnyValue(v any) Value

func BoolValue(v bool) Value

func DurationValue(v time.Duration) Value

func Float64Value(v float64) Value

func GroupValue(as ...Attr) Value

func Int64Value(v int64) Value

func IntValue(v int) Value

func StringValue(value string) Value

func TimeValue(v time.Time) Value

func Uint64Value(v uint64) Value

func (v Value) Any() any

func (v Value) Bool() bool

func (v Value) Duration() time.Duration

func (v Value) Equal(w Value) bool

func (v Value) Float64() float64

func (v Value) Group() []Attr

func (v Value) Int64() int64

func (v Value) Kind() Kind

func (v Value) LogValuer() LogValuer

func (v Value) Resolve() Value

func (v Value) String() string

func (v Value) Time() time.Time

func (v Value) Uint64() uint64

Description: A Value can represent any Go value but, unlike type any, can represent most small values without an allocation. Specific constructor functions create Values of common types efficiently.

Usage Example:

// Creating values efficiently
stringVal := slog.StringValue("hello")
intVal := slog.Int64Value(42)
boolVal := slog.BoolValue(true)
groupVal := slog.GroupValue(
    slog.String("key", "value"),
    slog.Int("num", 10))

// Accessing values
fmt.Println(stringVal.String())  // "hello"
fmt.Println(intVal.Int64())      // 42
fmt.Println(boolVal.Bool())      // true

// Checking value kind
fmt.Println(stringVal.Kind())    // KindString
fmt.Println(groupVal.Kind())     // KindGroup

Value Kind

type Kind int

const (
    KindAny Kind = iota
    KindBool
    KindDuration
    KindFloat64
    KindInt64
    KindString
    KindTime
    KindUint64
    KindGroup
    KindLogValuer
)

func (k Kind) String() string

Description: Kind represents the type of value held by a Value.

LogValuer Interface

type LogValuer interface {
    LogValue() Value
}

Description: LogValuer is any Go value that can convert itself into a Value for logging. This allows custom types to control how they appear in logs, support deferred expensive operations, or expand into multiple attributes.

Usage Example:

type User struct {
    ID   int
    Name string
}

// Implement LogValuer to customize logging
func (u User) LogValue() slog.Value {
    return slog.GroupValue(
        slog.Int("id", u.ID),
        slog.String("name", u.Name))
}

// For deferred expensive operations
type ExpensiveValue struct {
    arg int
}

func (e ExpensiveValue) LogValue() slog.Value {
    // This is only called if the log event is enabled
    return slog.AnyValue(computeExpensiveValue(e.arg))
}

// Usage
user := User{ID: 42, Name: "Alice"}
slog.Info("user action", slog.Any("user", user))

expensive := ExpensiveValue{arg: 10}
slog.Debug("debug", slog.Any("expensive", expensive))

Handler Interface

type Handler interface {
    // Enabled reports whether the handler handles records at the given level.
    Enabled(context.Context, Level) bool

    // Handle handles the Record.
    Handle(context.Context, Record) error

    // WithAttrs returns a new Handler with attributes added.
    WithAttrs(attrs []Attr) Handler

    // WithGroup returns a new Handler with the given group appended.
    WithGroup(name string) Handler
}

func NewLogLogger(h Handler, level Level) *log.Logger

Description: A Handler decides how to handle each log record. It filters records by level, formats them, and outputs them. Users should use Logger methods, not call Handler methods directly.

Usage Example:

// Creating a custom handler (simplified example)
type MyHandler struct {
    attrs []slog.Attr
}

func (h *MyHandler) Enabled(_ context.Context, level slog.Level) bool {
    return level >= slog.LevelInfo
}

func (h *MyHandler) Handle(_ context.Context, r slog.Record) error {
    // Custom handling logic
    fmt.Printf("[%s] %s\n", r.Level, r.Message)
    return nil
}

func (h *MyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    return &MyHandler{attrs: append(h.attrs, attrs...)}
}

func (h *MyHandler) WithGroup(name string) slog.Handler {
    return h // Simplified
}

// Bridge from log package to slog
logLogger := slog.NewLogLogger(handler, slog.LevelInfo)
log.SetOutput(ioutil.Discard)
log.SetFlags(0)
log.SetLogger(logLogger) // Log package now uses the handler

Handler Options

type HandlerOptions struct {
    // AddSource causes the handler to compute the source code position
    // of the log statement and add a SourceKey attribute to the output.
    AddSource bool

    // Level reports the minimum record level that will be logged.
    // If Level is nil, the handler assumes LevelInfo.
    // Use a LevelVar to vary the level dynamically.
    Level Leveler

    // ReplaceAttr is called to rewrite each non-group attribute before logging.
    ReplaceAttr func(groups []string, a Attr) Attr
}

Description: HandlerOptions configure TextHandler and JSONHandler behavior including minimum level, source file information, and attribute replacement.

Usage Example:

// Basic options
opts := &slog.HandlerOptions{
    AddSource: true,
    Level:     slog.LevelDebug,
}

// With dynamic level
levelVar := new(slog.LevelVar)
opts := &slog.HandlerOptions{
    Level: levelVar,
}

// With attribute replacement (redact sensitive data)
opts := &slog.HandlerOptions{
    ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
        if a.Key == "password" {
            return slog.Attr{Key: "password", Value: slog.StringValue("***")}
        }
        return a
    },
}

handler := slog.NewJSONHandler(os.Stdout, opts)

TextHandler

type TextHandler struct {
    // Has unexported fields.
}

func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler

func (h *TextHandler) Enabled(_ context.Context, level Level) bool

func (h *TextHandler) Handle(_ context.Context, r Record) error

func (h *TextHandler) WithAttrs(attrs []Attr) Handler

func (h *TextHandler) WithGroup(name string) Handler

Description: TextHandler writes Records as a sequence of space-separated key=value pairs. Output format is human-readable and machine-parseable. Keys and values are quoted if they contain spaces, special characters, or non-printing characters.

Usage Example:

// Create a TextHandler
handler := slog.NewTextHandler(os.Stderr, nil)
logger := slog.New(handler)

// Simple message
logger.Info("hello", "count", 3)
// Output: time=2022-11-08T15:28:26.000-05:00 level=INFO msg=hello count=3

// With options
opts := &slog.HandlerOptions{
    AddSource: true,
    Level:     slog.LevelDebug,
}
handler := slog.NewTextHandler(os.Stderr, opts)
logger := slog.New(handler)
logger.Debug("debug message", "key", "value")

// With groups
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
requestLogger := logger.WithGroup("request")
requestLogger.Info("data", "method", "GET", "url", "/api")
// Output: time=... level=INFO msg=data request.method=GET request.url=/api

JSONHandler

type JSONHandler struct {
    // Has unexported fields.
}

func NewJSONHandler(w io.Writer, opts *HandlerOptions) *JSONHandler

func (h *JSONHandler) Enabled(_ context.Context, level Level) bool

func (h *JSONHandler) Handle(_ context.Context, r Record) error

func (h *JSONHandler) WithAttrs(attrs []Attr) Handler

func (h *JSONHandler) WithGroup(name string) Handler

Description: JSONHandler writes Records as line-delimited JSON objects. Each log record becomes a single JSON object on its own line. Groups are represented as nested JSON objects.

Usage Example:

// Create a JSONHandler
handler := slog.NewJSONHandler(os.Stdout, nil)
logger := slog.New(handler)

// Simple message
logger.Info("hello", "count", 3)
// Output: {"time":"2022-11-08T15:28:26.000000000-05:00","level":"INFO","msg":"hello","count":3}

// With options for source tracking
opts := &slog.HandlerOptions{
    AddSource: true,
    Level:     slog.LevelInfo,
}
handler := slog.NewJSONHandler(os.Stdout, opts)
logger := slog.New(handler)
logger.Info("event")
// Output: {"time":"...","level":"INFO","msg":"event","source":"main.go:42"}

// With groups
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
requestLogger := logger.WithGroup("request")
requestLogger.Info("received", "method", "POST", "path", "/api/users")
// Output: {"time":"...","level":"INFO","msg":"received","request":{"method":"POST","path":"/api/users"}}

Constants

const (
    // TimeKey is the key used by built-in handlers for the time.
    TimeKey = "time"

    // LevelKey is the key used by built-in handlers for the level.
    LevelKey = "level"

    // MessageKey is the key used by built-in handlers for the message.
    MessageKey = "msg"

    // SourceKey is the key used by built-in handlers for source location.
    SourceKey = "source"
)

Description: These constants define the standard keys used by TextHandler and JSONHandler for built-in attributes.

slogtest Subpackage

The golang.org/x/exp/slog/slogtest subpackage provides testing utilities for Handler implementations.

Import

import "golang.org/x/exp/slog/slogtest"

Handler Testing

func TestHandler(h slog.Handler, results func() []map[string]any) error

Description: TestHandler tests a slog.Handler for correct behavior. It installs the handler in a slog.Logger, makes several calls to the Logger's output methods, and verifies the output. The results function should return a slice of maps, one for each Logger call, representing the handler's output. Groups should be represented as nested maps using the standard keys (slog.TimeKey, slog.LevelKey, slog.MessageKey).

Usage Example:

// Example: Testing a custom JSON handler
type customJSONHandler struct {
    // Custom handler implementation
}

func TestMyHandler(t *testing.T) {
    var buf bytes.Buffer
    handler := &customJSONHandler{w: &buf}

    err := slogtest.TestHandler(handler, func() []map[string]any {
        // Parse the JSON output line by line
        var records []map[string]any
        scanner := bufio.NewScanner(&buf)
        for scanner.Scan() {
            var record map[string]any
            if err := json.Unmarshal(scanner.Bytes(), &record); err != nil {
                t.Fatalf("Failed to parse JSON: %v", err)
            }
            records = append(records, record)
        }
        return records
    })

    if err != nil {
        t.Fatalf("TestHandler failed: %v", err)
    }
}

// Example: Testing the built-in JSONHandler
func TestJSONHandler(t *testing.T) {
    var buf bytes.Buffer
    handler := slog.NewJSONHandler(&buf, nil)

    err := slogtest.TestHandler(handler, func() []map[string]any {
        var records []map[string]any
        scanner := bufio.NewScanner(&buf)
        for scanner.Scan() {
            var record map[string]any
            json.Unmarshal(scanner.Bytes(), &record)
            records = append(records, record)
        }
        return records
    })

    if err != nil {
        t.Fatal(err)
    }
}

Complete Usage Examples

Basic Logging

package main

import (
    "os"
    "golang.org/x/exp/slog"
)

func main() {
    // Using default logger
    slog.Info("application started", "version", "1.0", "env", "production")
    slog.Warn("deprecated feature used", "feature", "old_api")
    slog.Error("failed to connect", "host", "db.example.com", "error", "connection timeout")
}

Structured Logging with TextHandler

package main

import (
    "os"
    "golang.org/x/exp/slog"
)

func main() {
    handler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
        AddSource: true,
        Level:     slog.LevelDebug,
    })
    logger := slog.New(handler)

    logger.Debug("debug info", "request_id", "req-123")
    logger.Info("user logged in", "user_id", 42, "ip", "192.168.1.1")
    logger.Warn("unusual activity", "attempts", 5)
}

Structured Logging with JSONHandler

package main

import (
    "os"
    "golang.org/x/exp/slog"
)

func main() {
    handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        AddSource: true,
    })
    logger := slog.New(handler)

    logger.Info("user action",
        slog.Int("user_id", 42),
        slog.String("action", "login"),
        slog.String("ip", "192.168.1.1"))

    logger.Info("request completed",
        slog.Group("request",
            slog.String("method", "GET"),
            slog.String("path", "/api/users"),
            slog.Int("status", 200)))
}

Dynamic Level Configuration

package main

import (
    "os"
    "golang.org/x/exp/slog"
)

var programLevel = new(slog.LevelVar)

func main() {
    // Create handler with dynamic level
    handler := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
        Level: programLevel,
    })
    logger := slog.New(handler)
    slog.SetDefault(logger)

    // Start with LevelInfo (default)
    slog.Info("startup")
    slog.Debug("debug disabled")

    // Enable debug logging
    programLevel.Set(slog.LevelDebug)
    slog.Debug("debug now enabled")

    // Only log errors
    programLevel.Set(slog.LevelError)
    slog.Info("info suppressed")
    slog.Error("error still logged")
}

Logger with Context and Attributes

package main

import (
    "context"
    "os"
    "golang.org/x/exp/slog"
)

func handleRequest(requestID string) {
    logger := slog.Default().With("request_id", requestID)

    // All logs from this logger will include request_id
    logger.Info("request started")
    logger.Info("processing data", "items", 5)
    logger.Info("request completed", "status", 200)
}

func main() {
    handler := slog.NewJSONHandler(os.Stdout, nil)
    slog.SetDefault(slog.New(handler))

    ctx := context.Background()
    slog.InfoContext(ctx, "server started")

    handleRequest("req-123")
    handleRequest("req-456")
}

Custom Attribute Values with LogValuer

package main

import (
    "fmt"
    "os"
    "time"
    "golang.org/x/exp/slog"
)

type Request struct {
    ID     string
    Method string
    Path   string
    Status int
}

func (r Request) LogValue() slog.Value {
    return slog.GroupValue(
        slog.String("id", r.ID),
        slog.String("method", r.Method),
        slog.String("path", r.Path),
        slog.Int("status", r.Status))
}

type User struct {
    ID   int
    Name string
    Role string
}

func (u User) LogValue() slog.Value {
    return slog.GroupValue(
        slog.Int("id", u.ID),
        slog.String("name", u.Name),
        slog.String("role", u.Role))
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    req := Request{ID: "abc123", Method: "GET", Path: "/api", Status: 200}
    logger.Info("request", slog.Any("request", req))

    user := User{ID: 42, Name: "Alice", Role: "admin"}
    logger.Info("user action", slog.Any("user", user), slog.String("action", "login"))
}

Attribute Replacement for Redaction

package main

import (
    "os"
    "strings"
    "golang.org/x/exp/slog"
)

func sanitizeAttr(groups []string, a slog.Attr) slog.Attr {
    // Redact sensitive fields
    sensitiveKeys := map[string]bool{
        "password": true,
        "token": true,
        "api_key": true,
        "secret": true,
    }

    if sensitiveKeys[a.Key] {
        return slog.Attr{Key: a.Key, Value: slog.StringValue("***")}
    }

    return a
}

func main() {
    opts := &slog.HandlerOptions{
        ReplaceAttr: sanitizeAttr,
    }
    handler := slog.NewJSONHandler(os.Stdout, opts)
    logger := slog.New(handler)

    // Sensitive fields will be redacted
    logger.Info("user authenticated",
        slog.String("username", "alice"),
        slog.String("password", "secret123"),
        slog.String("token", "eyJhbGc..."))
}

Deferred Expensive Operations

package main

import (
    "fmt"
    "os"
    "golang.org/x/exp/slog"
)

type ExpensiveMetrics struct {
    value int
}

func (e ExpensiveMetrics) LogValue() slog.Value {
    // This is only called if the log event is enabled
    fmt.Println("Computing expensive metrics...")
    result := computeMetrics(e.value)
    return slog.AnyValue(result)
}

func computeMetrics(v int) map[string]int {
    // Expensive operation
    return map[string]int{"cpu": 50, "memory": 1024}
}

func main() {
    // Create handler that only logs errors
    handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelError,
    })
    logger := slog.New(handler)

    // This debug log won't be emitted, so computeMetrics won't be called
    logger.Debug("metrics", slog.Any("metrics", ExpensiveMetrics{value: 42}))

    // This error will be emitted, so computeMetrics will be called
    logger.Error("critical", slog.Any("metrics", ExpensiveMetrics{value: 42}))
}

Groups and Nesting

package main

import (
    "os"
    "golang.org/x/exp/slog"
)

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    // Inline groups
    logger.Info("event",
        slog.Group("user",
            slog.Int("id", 42),
            slog.String("name", "Alice")),
        slog.Group("request",
            slog.String("method", "POST"),
            slog.String("path", "/api/data")))

    // Logger-level groups
    requestLogger := logger.WithGroup("request")
    requestLogger.Info("processing",
        slog.String("method", "GET"),
        slog.String("path", "/api/users"),
        slog.Int("duration_ms", 150))
}

Working with Records

package main

import (
    "fmt"
    "time"
    "golang.org/x/exp/slog"
)

func main() {
    // Create and modify a record
    record := slog.NewRecord(time.Now(), slog.LevelInfo, "event", 0)

    // Add attributes
    record.AddAttrs(
        slog.String("type", "login"),
        slog.Int("user_id", 42))

    // Clone the record to avoid shared state
    cloned := record.Clone()
    cloned.Add("extra", "data")

    // Iterate over attributes
    fmt.Println("Original record attributes:")
    record.Attrs(func(a slog.Attr) bool {
        fmt.Printf("  %s = %v\n", a.Key, a.Value)
        return true
    })

    fmt.Println("Cloned record attributes:")
    cloned.Attrs(func(a slog.Attr) bool {
        fmt.Printf("  %s = %v\n", a.Key, a.Value)
        return true
    })
}

Performance Considerations

  1. Use Logger.With for common attributes: The built-in handlers format attributes added via With only once, improving performance for repeated attributes.

  2. Prefer LogAttrs over Log: Use LogAttrs when possible for better efficiency, as it avoids the overhead of converting arguments to Attrs.

  3. Defer expensive computations: Use the LogValuer interface to defer expensive operations until the log event is actually enabled.

  4. Check Enabled before expensive operations: Use Logger.Enabled to check if a level is enabled before performing expensive computations.

  5. Pass values directly: When possible, pass raw values rather than calling their String methods prematurely, allowing handlers to preserve structure.

Example of checking enabled level:

if logger.Enabled(ctx, slog.LevelDebug) {
    logger.DebugContext(ctx, "debug info", slog.Any("expensive", expensiveComputation()))
}