Core logging in zerolog centers around three primary types: Logger, Event, and Level. These form the foundation of all logging operations.
import "github.com/rs/zerolog"The Logger type is the main logging object. It creates log events and manages configuration like minimum level, output writer, samplers, and hooks.
// Create a root logger with an io.Writer
func New(w io.Writer) LoggerExample:
import (
"os"
"github.com/rs/zerolog"
)
logger := zerolog.New(os.Stderr)// Create a disabled logger (no-op, for testing or conditional logging)
func Nop() LoggerExample:
logger := zerolog.Nop()
logger.Info().Msg("this message is discarded")Level methods create log events at specific severity levels. Each returns an *Event that can be configured with fields and terminated with a message.
// Create debug level event
func (l Logger) Debug() *Event
// Create info level event
func (l Logger) Info() *Event
// Create warn level event
func (l Logger) Warn() *Event
// Create error level event
func (l Logger) Error() *Event
// Create fatal level event (calls os.Exit(1) after logging)
func (l Logger) Fatal() *Event
// Create panic level event (calls panic() after logging)
func (l Logger) Panic() *Event
// Create trace level event
func (l Logger) Trace() *Event
// Create event with no level
func (l Logger) Log() *EventExample:
logger.Info().Msg("application started")
logger.Debug().Str("var", value).Msg("debug info")
logger.Error().Err(err).Msg("operation failed")// Create event at specified level
func (l Logger) WithLevel(level Level) *EventExample:
level := zerolog.InfoLevel
logger.WithLevel(level).Msg("dynamic level message")// Create error or info level event based on whether err is nil
func (l Logger) Err(err error) *EventExample:
err := doSomething()
logger.Err(err).Msg("operation completed")
// Logs at error level if err != nil, info level if err == nilThese methods return a new Logger with different configuration. The original logger is not modified.
// Create child logger with minimum level
func (l Logger) Level(lvl Level) Logger
// Get current minimum level
func (l Logger) GetLevel() LevelExample:
debugLogger := logger.Level(zerolog.DebugLevel)
currentLevel := debugLogger.GetLevel()// Create child logger with sampler
func (l Logger) Sample(s Sampler) LoggerExample:
// Log only every 10th message
sampledLogger := logger.Sample(&zerolog.BasicSampler{N: 10})// 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")
})
hookedLogger := logger.Hook(hook)// Create child logger with different output writer
func (l Logger) Output(w io.Writer) LoggerExample:
fileLogger := logger.Output(logFile)// Create Context for building child logger with persistent fields
func (l Logger) With() ContextExample:
childLogger := logger.With().
Str("service", "api").
Str("version", "1.0").
Logger()See Contextual Logging for complete Context API.
// Update logger's context (NOT concurrency-safe)
func (l Logger) UpdateContext(update func(c Context) Context)Example:
logger.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("request_id", requestID)
})Warning: UpdateContext is not thread-safe. Do not call it concurrently with logging operations.
// Attach logger to Go context.Context
func (l Logger) WithContext(ctx context.Context) context.ContextExample:
ctx := logger.WithContext(context.Background())
// Later retrieve with zerolog.Ctx(ctx)// Retrieve logger from context.Context
func Ctx(ctx context.Context) *LoggerExample:
logger := zerolog.Ctx(ctx)
logger.Info().Msg("using logger from context")If no logger is found in the context, Ctx() returns DefaultContextLogger (which defaults to a disabled logger if not set).
For compatibility with standard library logger interface:
// Print at trace level
func (l Logger) Print(v ...interface{})
// Printf at trace level
func (l Logger) Printf(format string, v ...interface{})
// Println at trace level
func (l Logger) Println(v ...interface{})Example:
logger.Printf("count: %d", 42)Logger implements io.Writer, allowing it to be used as a writer destination:
func (l Logger) Write(p []byte) (n int, err error)Example:
stdlog := log.New(logger, "", 0)
stdlog.Print("message from standard logger")The Event type represents a single log event. Events are created by calling level methods on a Logger and are terminated by calling Msg(), Msgf(), or Send().
Events use a fluent, chainable API for adding fields. Events are immutable after termination and should not be reused.
// 1. Create event
event := logger.Info()
// 2. Add fields (chainable)
event.Str("user", "alice").
Int("age", 30).
Bool("active", true)
// 3. Terminate event
event.Msg("user logged in")// Send event with message
func (e *Event) Msg(msg string)
// Send event with formatted message
func (e *Event) Msgf(format string, v ...interface{})
// Send event without message
func (e *Event) Send()
// Send event with lazy message (only evaluated if event is enabled)
func (e *Event) MsgFunc(createMsg func() string)Example:
logger.Info().Msg("simple message")
logger.Info().Msgf("user %s logged in", username)
logger.Info().Str("status", "ok").Send()
logger.Debug().MsgFunc(func() string {
return fmt.Sprintf("expensive: %v", computeExpensiveValue())
})// Check if event will be logged
func (e *Event) Enabled() bool
// Disable the event (subsequent operations are no-ops)
func (e *Event) Discard() *Event
// Execute function only if event is enabled
func (e *Event) Func(f func(e *Event)) *EventExample:
// Conditional expensive operations
if e := logger.Debug(); e.Enabled() {
result := computeExpensiveValue()
e.Str("result", result).Msg("debug info")
}
// Conditional discard
logger.Info().
Func(func(e *zerolog.Event) {
if shouldDiscard {
e.Discard()
}
}).
Msg("conditional message")// Store Go context in event (accessible to hooks)
func (e *Event) Ctx(ctx context.Context) *Event
// Get stored Go context
func (e *Event) GetCtx() context.ContextExample:
ctx := context.WithValue(context.Background(), "trace_id", "123")
logger.Info().Ctx(ctx).Msg("message with context")// Add caller file and line information
func (e *Event) Caller(skip ...int) *Event
// Add caller with specific frame skip count
func (e *Event) CallerSkipFrame(skip int) *Event
// Add type name of value
func (e *Event) Type(key string, val interface{}) *EventExample:
logger.Info().Caller().Msg("log with caller info")
// Output includes: "caller":"/path/to/file.go:42"
logger.Info().Type("value_type", myStruct).Msg("log with type")
// Output includes: "value_type":"main.MyStruct"Events have numerous field methods for adding typed data. See Field Methods for the complete reference.
Common field methods:
func (e *Event) Str(key, val string) *Event
func (e *Event) Int(key string, i int) *Event
func (e *Event) Bool(key string, b bool) *Event
func (e *Event) Err(err error) *Event
func (e *Event) Time(key string, t time.Time) *Event
func (e *Event) Dur(key string, d time.Duration) *EventThe Level type represents log severity levels as an int8 enumeration.
type Level int8const (
TraceLevel Level = -1 // Finest-grained information
DebugLevel Level = 0 // Debug information
InfoLevel Level = 1 // General informational messages
WarnLevel Level = 2 // Warning messages
ErrorLevel Level = 3 // Error conditions
FatalLevel Level = 4 // Fatal errors (logs then calls os.Exit(1))
PanicLevel Level = 5 // Panic conditions (logs then calls panic())
NoLevel Level = 6 // No level (for events created with Log())
Disabled Level = 7 // Completely disabled
)// Get string representation of level
func (l Level) String() stringExample:
level := zerolog.InfoLevel
fmt.Println(level.String()) // "info"// Implement encoding.TextMarshaler
func (l Level) MarshalText() ([]byte, error)
// Implement encoding.TextUnmarshaler
func (l *Level) UnmarshalText(text []byte) error// Parse level from string
func ParseLevel(levelStr string) (Level, error)Example:
level, err := zerolog.ParseLevel("info")
if err != nil {
log.Fatal(err)
}
// level == zerolog.InfoLevelAccepted strings: "trace", "debug", "info", "warn", "warning", "error", "fatal", "panic", "disabled", "" (NoLevel).
// Set global minimum level for all loggers
func SetGlobalLevel(l Level)Example:
// Disable debug and trace in production
zerolog.SetGlobalLevel(zerolog.InfoLevel)Note: Global level affects all loggers created before and after the call. Individual logger level settings via Logger.Level() override the global setting.
package main
import (
"os"
"github.com/rs/zerolog"
)
func main() {
logger := zerolog.New(os.Stderr)
logger.Info().Msg("application started")
logger.Debug().Str("config", "dev").Msg("configuration loaded")
}err := database.Connect()
if err != nil {
logger.Error().
Err(err).
Str("host", "localhost").
Int("port", 5432).
Msg("database connection failed")
return
}var logger zerolog.Logger
if production {
logger = zerolog.New(os.Stderr).Level(zerolog.InfoLevel)
} else {
logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).
Level(zerolog.DebugLevel)
}if e := logger.Debug(); e.Enabled() {
// This expensive operation only runs if debug level is enabled
data := collectDebugData()
e.Interface("data", data).Msg("debug snapshot")
}// Fatal level calls os.Exit(1) after logging
if criticalError {
logger.Fatal().
Err(err).
Msg("critical failure, shutting down")
// Program exits here
}
// Panic level calls panic() after logging
if invariantViolated {
logger.Panic().
Str("invariant", "balance >= 0").
Int64("balance", balance).
Msg("invariant violation")
// panic() is called here
}// Log without a level field
logger.Log().
Str("metric", "request_count").
Int("value", 1000).
Send()
// Output: {"metric":"request_count","value":1000}// Create hierarchy of loggers
appLogger := zerolog.New(os.Stderr).
With().Timestamp().Logger()
requestLogger := appLogger.With().
Str("request_id", requestID).
Logger()
dbLogger := requestLogger.With().
Str("component", "database").
Logger()
dbLogger.Info().Msg("query executed")
// Includes: request_id, component, and timestampCreate loggers once and reuse them. Loggers are lightweight and can be copied by value.
// Good
type Service struct {
logger zerolog.Logger
}
func NewService(logger zerolog.Logger) *Service {
return &Service{
logger: logger.With().Str("service", "my-service").Logger(),
}
}Use Enabled() to avoid expensive computations for disabled log levels:
if e := logger.Debug(); e.Enabled() {
expensiveData := computeExpensiveDebugInfo()
e.Interface("data", expensiveData).Msg("debug info")
}Always use Err() instead of Interface() or Str() for error values:
// Good
logger.Error().Err(err).Msg("operation failed")
// Avoid
logger.Error().Interface("error", err).Msg("operation failed")Set the global level during application initialization:
func init() {
if os.Getenv("DEBUG") == "true" {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
} else {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
}
}UpdateContext() is not thread-safe. Use it only during initialization or create new child loggers instead:
// Good - create new logger
requestLogger := logger.With().Str("request_id", id).Logger()
// Avoid in concurrent code
logger.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Str("request_id", id)
})Enabled() checks for expensive operationsLoggers are not thread-safe by default. For concurrent access:
zerolog.SyncWriter() to wrap output writers with mutexdiode.Writer for lock-free non-blocking writesSee Writers and Output for thread-safe writers.