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.
import "github.com/rs/zerolog"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 eventmessage string - The final message string (after formatting)Use the Hook() method on Logger to create a child logger with hooks:
// Create child logger with hooks
func (l Logger) Hook(hooks ...Hook) LoggerExample:
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 eventFunction 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)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() LevelHookExample:
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 runsExample 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)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"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)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 tracehook := 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") // Discardedhook := 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 pidhook := 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)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)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)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)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)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)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_numbertype 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)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
}
})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")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
}
})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()
}
})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)
})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
})go routines for expensive operations (alerts, metrics)Event parameter is not thread-safe (don't share across goroutines)