or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

advanced-types.mdcontextual-logging.mdcore-logging.mddiode-writer.mdfield-methods.mdglobal-configuration.mdglobal-logger.mdhooks.mdhttp-middleware.mdindex.mdjournald-integration.mdpkgerrors-integration.mdsampling.mdwriters-and-output.md
tile.json

hooks.mddocs/

Hooks

Hooks intercept log events before they're written, allowing you to modify, enrich, or filter events dynamically. Hooks receive the event, level, and message, and can add fields, change values, or even discard events.

Imports

import "github.com/rs/zerolog"

Hook Interface

The Hook interface defines a single method that runs for each log event.

type Hook interface {
    // Run executes the hook with event, level, and message
    Run(e *Event, level Level, message string)
}

Parameters:

  • e *Event - The log event (can be modified by adding fields)
  • level Level - The log level of the event
  • message string - The final message string (after formatting)

Applying Hooks

Use the Hook() method on Logger to create a child logger with hooks:

// Create child logger with hooks
func (l Logger) Hook(hooks ...Hook) Logger

Example:

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Str("environment", "production")
})

logger := zerolog.New(os.Stdout).Hook(hook)

logger.Info().Msg("hello")
// Output includes: "environment":"production"

Multiple hooks can be chained:

hook1 := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Str("version", "1.0.0")
})

hook2 := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Str("service", "api")
})

logger := logger.Hook(hook1, hook2)
// Both hooks execute for each event

HookFunc

Function adapter that implements the Hook interface, allowing ordinary functions to be used as hooks.

type HookFunc func(e *Event, level Level, message string)

func (h HookFunc) Run(e *Event, level Level, message string)

Example:

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    // Add timestamp in custom format
    e.Time("hooked_at", time.Now())
})

logger := logger.Hook(hook)

Example with level checking:

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    if level >= zerolog.ErrorLevel {
        // Add stack trace for errors
        e.Stack()
    }
})

logger := logger.Hook(hook)

LevelHook

Apply different hooks to different log levels, allowing fine-grained per-level customization.

type LevelHook struct {
    NoLevelHook Hook
    TraceHook   Hook
    DebugHook   Hook
    InfoHook    Hook
    WarnHook    Hook
    ErrorHook   Hook
    FatalHook   Hook
    PanicHook   Hook
}

func (h LevelHook) Run(e *Event, level Level, message string)

// Create new LevelHook
func NewLevelHook() LevelHook

Example:

hook := zerolog.NewLevelHook()

hook.ErrorHook = zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Str("alert", "error-detected")
    e.Stack()
})

hook.WarnHook = zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Str("alert", "warning")
})

logger := logger.Hook(hook)

logger.Info().Msg("info")    // No hook runs
logger.Warn().Msg("warning") // WarnHook runs
logger.Error().Msg("error")  // ErrorHook runs

Example with multiple levels:

hook := zerolog.NewLevelHook()

// Add caller info for debug and trace
debugHook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Caller(1)
})
hook.DebugHook = debugHook
hook.TraceHook = debugHook

// Add severity for errors
hook.ErrorHook = zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Str("severity", "HIGH")
})

logger := logger.Hook(hook)

Common Use Cases

1. Add Request ID from Context

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    ctx := e.GetCtx()
    if ctx != nil {
        if requestID, ok := ctx.Value("request_id").(string); ok {
            e.Str("request_id", requestID)
        }
    }
})

logger := logger.Hook(hook)

// Use with context
ctx := context.WithValue(context.Background(), "request_id", "abc-123")
logger.Info().Ctx(ctx).Msg("request processed")
// Output includes: "request_id":"abc-123"

2. Add Cloud Trace IDs

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    ctx := e.GetCtx()
    if ctx != nil {
        // Extract trace ID from cloud provider context
        if span := trace.SpanFromContext(ctx); span != nil {
            e.Str("trace_id", span.SpanContext().TraceID().String())
        }
    }
})

logger := logger.Hook(hook)

3. Add Stack Traces to Errors

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    if level >= zerolog.ErrorLevel {
        e.Stack()
    }
})

logger := logger.Hook(hook)

logger.Error().Msg("error")  // Includes stack trace
logger.Info().Msg("info")    // No stack trace

4. Filter Sensitive Data

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    // Discard events with sensitive data
    if strings.Contains(msg, "password") || strings.Contains(msg, "secret") {
        e.Discard()
    }
})

logger := logger.Hook(hook)

logger.Info().Msg("user logged in")        // Logged
logger.Info().Msg("password is invalid")   // Discarded

5. Add Environment Info

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Str("environment", os.Getenv("ENV"))
    e.Str("hostname", hostname)
    e.Int("pid", os.Getpid())
})

logger := logger.Hook(hook)

// All events include environment, hostname, and pid

6. Send Critical Errors to Alert System

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    if level >= zerolog.ErrorLevel {
        go func() {
            // Send to alert system (PagerDuty, Slack, etc.)
            alertSystem.Send(level.String(), msg)
        }()
    }
})

logger := logger.Hook(hook)

7. Add Metrics

var (
    logCounter = prometheus.NewCounterVec(
        prometheus.CounterOpts{Name: "log_events_total"},
        []string{"level"},
    )
)

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    logCounter.WithLabelValues(level.String()).Inc()
})

logger := logger.Hook(hook)

8. Level-Specific Formatting

hook := zerolog.NewLevelHook()

hook.ErrorHook = zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Str("severity", "HIGH")
    e.Bool("requires_attention", true)
})

hook.WarnHook = zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Str("severity", "MEDIUM")
})

hook.InfoHook = zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Str("severity", "LOW")
})

logger := logger.Hook(hook)

9. Add Correlation IDs

type correlationIDKey struct{}

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    ctx := e.GetCtx()
    if ctx != nil {
        if corrID, ok := ctx.Value(correlationIDKey{}).(string); ok {
            e.Str("correlation_id", corrID)
        }
    }
})

logger := logger.Hook(hook)

10. Modify Message

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    // Note: msg parameter is the final message, but you can add fields
    // You cannot modify the message itself, but you can add a modified version
    if level == zerolog.ErrorLevel {
        e.Str("original_message", msg)
        // The original message still goes to MessageFieldName
    }
})

logger := logger.Hook(hook)

Custom Hook Implementations

Struct-based Hook

type AuditHook struct {
    UserID   string
    TenantID string
}

func (h AuditHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Str("user_id", h.UserID)
    e.Str("tenant_id", h.TenantID)
    e.Time("audit_time", time.Now())
}

// Usage
hook := AuditHook{
    UserID:   "user123",
    TenantID: "tenant456",
}

logger := logger.Hook(hook)

Stateful Hook with Counter

type CounterHook struct {
    count uint64
}

func (h *CounterHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
    count := atomic.AddUint64(&h.count, 1)
    e.Uint64("event_number", count)
}

// Usage
hook := &CounterHook{}
logger := logger.Hook(hook)

// Each event gets an incrementing event_number

Conditional Hook

type ConditionalHook struct {
    Condition func(level zerolog.Level, msg string) bool
    Hook      zerolog.Hook
}

func (h ConditionalHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
    if h.Condition(level, msg) {
        h.Hook.Run(e, level, msg)
    }
}

// Usage
hook := ConditionalHook{
    Condition: func(level zerolog.Level, msg string) bool {
        return level >= zerolog.WarnLevel
    },
    Hook: zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
        e.Str("alert", "true")
    }),
}

logger := logger.Hook(hook)

Best Practices

1. Keep Hooks Fast

Hooks run synchronously for every log event. Avoid expensive operations:

// Good - fast operation
hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Str("env", "prod")
})

// Avoid - expensive operation
hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    // Don't do database queries or API calls in hooks
    data := queryDatabase()  // BAD
    e.Interface("data", data)
})

// Better - use goroutine for expensive operations
hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    if level >= zerolog.ErrorLevel {
        go sendAlert(msg)  // Non-blocking
    }
})

2. Use Context for Dynamic Data

Store dynamic data in context.Context and retrieve it in hooks:

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    ctx := e.GetCtx()
    if ctx != nil {
        if userID, ok := ctx.Value("user_id").(string); ok {
            e.Str("user_id", userID)
        }
    }
})

// Pass context with events
logger.Info().Ctx(ctx).Msg("action")

3. Check Context for Nil

Always check if context exists before accessing it:

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    ctx := e.GetCtx()
    if ctx != nil {  // Always check
        // Safe to use ctx
    }
})

4. Use LevelHook for Level-Specific Logic

Instead of checking levels in every hook, use LevelHook:

// Good - use LevelHook
hook := zerolog.NewLevelHook()
hook.ErrorHook = zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    e.Stack()
})

// Avoid - checking level in every call
hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    if level >= zerolog.ErrorLevel {
        e.Stack()
    }
})

5. Don't Panic in Hooks

Panics in hooks will crash your application:

// Avoid
hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    data := riskyOperation()  // Might panic
    e.Interface("data", data)
})

// Better - recover from panics
hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    defer func() {
        if r := recover(); r != nil {
            e.Str("hook_error", fmt.Sprintf("%v", r))
        }
    }()
    data := riskyOperation()
    e.Interface("data", data)
})

6. Avoid Circular Logging

Don't log from within hooks on the same logger:

// BAD - creates infinite loop
hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    logger.Debug().Msg("hook executed")  // INFINITE LOOP
})

// Better - use separate logger or metrics
var hookLogger = zerolog.New(os.Stderr)

hook := zerolog.HookFunc(func(e *zerolog.Event, level zerolog.Level, msg string) {
    hookLogger.Debug().Msg("hook executed")  // Different logger
})

Performance Considerations

  • Hooks run synchronously for every log event
  • Hook overhead applies even if event is later discarded
  • Multiple hooks execute sequentially
  • Use go routines for expensive operations (alerts, metrics)
  • Consider using sampling with hooks to reduce overhead
  • Hooks are called after field evaluation but before encoding

Thread Safety

  • Hooks must be thread-safe if logger is shared across goroutines
  • Use atomic operations or mutexes for shared state in hooks
  • Event parameter is not thread-safe (don't share across goroutines)
  • Context data should be thread-safe

Hook Execution Order

  1. Logger-level hooks execute first (in order added)
  2. Event fields are evaluated
  3. Hook receives final message and level
  4. Hook can add more fields to event
  5. Event is encoded and written

See Also

  • Core Logging - Logger Hook() method
  • Contextual Logging - Using Context with hooks
  • Sampling - Alternative to filtering with hooks
  • Global Configuration - Global hook settings