Zerolog provides flexible output routing through various writer implementations and adapters. Writers control where logs are written and how they're formatted, with support for level-based routing, console output, multi-destination writing, and thread-safe access.
import (
"os"
"github.com/rs/zerolog"
)The LevelWriter interface extends io.Writer with level-aware writing. Writers that implement this interface receive level information along with the payload.
type LevelWriter interface {
io.Writer
WriteLevel(level Level, p []byte) (n int, err error)
}Example:
type CustomLevelWriter struct{}
func (w CustomLevelWriter) Write(p []byte) (n int, err error) {
// Standard write without level
return os.Stdout.Write(p)
}
func (w CustomLevelWriter) WriteLevel(level zerolog.Level, p []byte) (n int, err error) {
// Write with level information
prefix := fmt.Sprintf("[%s] ", level)
os.Stdout.Write([]byte(prefix))
return os.Stdout.Write(p)
}Adapts a standard io.Writer to implement the LevelWriter interface.
type LevelWriterAdapter struct {
io.Writer
}
// WriteLevel writes to the adapted writer, ignoring level
func (lw LevelWriterAdapter) WriteLevel(l Level, p []byte) (n int, err error)
// Close calls Close on underlying writer if it's an io.Closer
func (lw LevelWriterAdapter) Close() errorExample:
file, _ := os.Create("app.log")
adapter := zerolog.LevelWriterAdapter{Writer: file}
logger := zerolog.New(adapter)
logger.Info().Msg("message")Wraps a writer with a mutex to make writes thread-safe. Use this when the underlying writer is not thread-safe.
// Wrap writer with mutex for thread-safe writes
func SyncWriter(w io.Writer) io.WriterExample:
// Make file writes thread-safe
file, _ := os.Create("app.log")
syncFile := zerolog.SyncWriter(file)
logger := zerolog.New(syncFile)
// Safe to use from multiple goroutines
go logger.Info().Msg("from goroutine 1")
go logger.Info().Msg("from goroutine 2")Note: File writes on POSIX and Windows systems are already thread-safe, so SyncWriter is not strictly necessary for os.File objects. However, it's useful for custom writers or when you need guaranteed serialization.
Write to multiple destinations simultaneously, similar to Unix tee command.
// Create writer that duplicates writes to all provided writers
func MultiLevelWriter(writers ...io.Writer) LevelWriterExample:
// Write to both stdout and file
file, _ := os.Create("app.log")
multi := zerolog.MultiLevelWriter(
os.Stdout,
file,
)
logger := zerolog.New(multi)
logger.Info().Msg("written to both stdout and file")Advanced example with different writers:
// Console for development, file for production logs
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
jsonFile, _ := os.Create("app.json")
multi := zerolog.MultiLevelWriter(
consoleWriter,
jsonFile,
)
logger := zerolog.New(multi).With().Timestamp().Logger()Human-friendly, optionally colorized console output for development. Parses JSON log output and formats it for readability.
type ConsoleWriter struct {
// Output destination (default: os.Stdout)
Out io.Writer
// Disable colorized output
NoColor bool
// Timestamp format string (default: time.Kitchen)
TimeFormat string
// Time zone for timestamps (default: time.Local)
TimeLocation *time.Location
// Order of output parts (level, time, message, caller, etc.)
PartsOrder []string
// Parts to exclude from output
PartsExclude []string
// Order of contextual fields
FieldsOrder []string
// Contextual fields to exclude
FieldsExclude []string
// Custom formatters
FormatTimestamp Formatter
FormatLevel Formatter
FormatCaller Formatter
FormatMessage Formatter
FormatFieldName Formatter
FormatFieldValue Formatter
FormatErrFieldName Formatter
FormatErrFieldValue Formatter
FormatPartValueByName FormatterByFieldName
// Format extra fields
FormatExtra func(map[string]interface{}, *bytes.Buffer) error
// Pre-process log entry
FormatPrepare func(map[string]interface{}) error
}// Create and initialize ConsoleWriter with options
func NewConsoleWriter(options ...func(w *ConsoleWriter)) ConsoleWriterExample:
// Basic console writer
writer := zerolog.ConsoleWriter{Out: os.Stdout}
logger := zerolog.New(writer)
// With constructor and options
writer := zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) {
w.TimeFormat = time.RFC3339
w.NoColor = false
})
logger := zerolog.New(writer)// Write implements io.Writer
func (w ConsoleWriter) Write(p []byte) (n int, err error)
// Close underlying writer if it's an io.Closer
func (w ConsoleWriter) Close() errorCustom time format:
writer := zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: time.RFC3339,
}
logger := zerolog.New(writer).With().Timestamp().Logger()Disable colors:
writer := zerolog.ConsoleWriter{
Out: os.Stdout,
NoColor: true,
}
logger := zerolog.New(writer)Custom parts order:
writer := zerolog.ConsoleWriter{
Out: os.Stdout,
PartsOrder: []string{
zerolog.LevelFieldName,
zerolog.TimestampFieldName,
zerolog.CallerFieldName,
zerolog.MessageFieldName,
},
}
logger := zerolog.New(writer)Exclude fields:
writer := zerolog.ConsoleWriter{
Out: os.Stdout,
PartsExclude: []string{zerolog.TimestampFieldName},
FieldsExclude: []string{"version", "host"},
}
logger := zerolog.New(writer)Custom formatters:
writer := zerolog.ConsoleWriter{
Out: os.Stdout,
FormatLevel: func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
},
FormatMessage: func(i interface{}) string {
return fmt.Sprintf(">>> %s", i)
},
FormatFieldName: func(i interface{}) string {
return fmt.Sprintf("%s:", i)
},
FormatFieldValue: func(i interface{}) string {
return fmt.Sprintf("%s;", i)
},
}
logger := zerolog.New(writer)// Transform input value to formatted string
type Formatter func(interface{}) string
// Format value based on field name
type FormatterByFieldName func(interface{}, string) stringExample:
customFormatter := func(i interface{}) string {
return fmt.Sprintf("[%v]", i)
}
writer := zerolog.ConsoleWriter{
Out: os.Stdout,
FormatFieldValue: customFormatter,
}Filter log events by minimum level. Only events at or above the specified level are written.
type FilteredLevelWriter struct {
Writer LevelWriter // Underlying writer
Level Level // Minimum level to write
}
func (w FilteredLevelWriter) Write(p []byte) (int, error)
func (w FilteredLevelWriter) WriteLevel(level Level, p []byte) (int, error)
func (w FilteredLevelWriter) Close() errorExample:
// Only write warnings and errors to file
errorFile, _ := os.Create("errors.log")
filtered := zerolog.FilteredLevelWriter{
Writer: zerolog.LevelWriterAdapter{errorFile},
Level: zerolog.WarnLevel,
}
logger := zerolog.New(filtered)
logger.Info().Msg("not written") // Below WarnLevel
logger.Warn().Msg("written") // WarnLevel
logger.Error().Msg("written") // Above WarnLevelRoute different levels to different files:
infoFile, _ := os.Create("info.log")
errorFile, _ := os.Create("errors.log")
infoWriter := zerolog.FilteredLevelWriter{
Writer: zerolog.LevelWriterAdapter{infoFile},
Level: zerolog.InfoLevel,
}
errorWriter := zerolog.FilteredLevelWriter{
Writer: zerolog.LevelWriterAdapter{errorFile},
Level: zerolog.ErrorLevel,
}
multi := zerolog.MultiLevelWriter(
zerolog.ConsoleWriter{Out: os.Stdout},
infoWriter,
errorWriter,
)
logger := zerolog.New(multi)Buffer log events until a trigger level is reached, then flush all buffered events. Useful for capturing debug context around errors.
type TriggerLevelWriter struct {
io.Writer // Destination writer (embedded)
ConditionalLevel Level // Level to buffer
TriggerLevel Level // Level that triggers flush
}
func (w *TriggerLevelWriter) WriteLevel(l Level, p []byte) (n int, err error)
func (w *TriggerLevelWriter) Trigger() error // Force flush
func (w *TriggerLevelWriter) Close() errorExample:
// Buffer debug logs, flush when error occurs
trigger := &zerolog.TriggerLevelWriter{
Writer: os.Stdout,
ConditionalLevel: zerolog.DebugLevel, // Buffer debug logs
TriggerLevel: zerolog.ErrorLevel, // Flush on error
}
logger := zerolog.New(trigger).Level(zerolog.DebugLevel)
logger.Debug().Msg("debug 1") // Buffered
logger.Debug().Msg("debug 2") // Buffered
logger.Info().Msg("info 1") // Written immediately (not buffered level)
logger.Error().Msg("error!") // Triggers flush of buffered debug logs
// Now debug 1, debug 2, and error are all writtenConstants:
// Maximum buffer size before pooling is disabled (64 KiB)
const TriggerLevelWriterBufferReuseLimit = 64 * 1024Write logs to Go testing.T or testing.B for test output.
type TestingLog interface {
Log(args ...interface{})
Logf(format string, args ...interface{})
Helper()
}
type TestWriter struct {
T TestingLog // Testing interface (testing.T or testing.B)
Frame int // Caller frame skip count
}
func NewTestWriter(t TestingLog) TestWriterExample:
func TestSomething(t *testing.T) {
logger := zerolog.New(zerolog.NewTestWriter(t))
logger.Info().Msg("test log message")
// Output appears in test output
}With ConsoleWriter for pretty test output:
func ConsoleTestWriter(t TestingLog) func(w *ConsoleWriter)Example:
func TestWithConsole(t *testing.T) {
logger := zerolog.New(
zerolog.NewConsoleWriter(zerolog.ConsoleTestWriter(t)),
)
logger.Info().Str("test", "value").Msg("pretty test log")
}Writers for syslog-compatible outputs.
type SyslogWriter interface {
io.Writer
Debug(m string) error
Info(m string) error
Warning(m string) error
Err(m string) error
Emerg(m string) error
Crit(m string) error
}// Wrap syslog writer to route by level
func SyslogLevelWriter(w SyslogWriter) LevelWriter
// Wrap syslog writer with CEE format prefix
func SyslogCEEWriter(w SyslogWriter) LevelWriterExample:
import "log/syslog"
syslogWriter, err := syslog.New(syslog.LOG_INFO, "myapp")
if err != nil {
panic(err)
}
levelWriter := zerolog.SyslogLevelWriter(syslogWriter)
logger := zerolog.New(levelWriter)
logger.Info().Msg("sent to syslog Info")
logger.Error().Msg("sent to syslog Err")var logger zerolog.Logger
if os.Getenv("ENV") == "production" {
// Production: JSON to file
file, _ := os.Create("/var/log/app.log")
logger = zerolog.New(file).With().Timestamp().Logger()
} else {
// Development: pretty console output
logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).
With().Timestamp().Caller().Logger()
}// Write JSON to file, pretty output to console
file, _ := os.Create("app.log")
multi := zerolog.MultiLevelWriter(
file,
zerolog.ConsoleWriter{Out: os.Stdout},
)
logger := zerolog.New(multi).With().Timestamp().Logger()// Info+ to main log, Error+ to error log, Debug+ to console
mainLog, _ := os.Create("app.log")
errorLog, _ := os.Create("errors.log")
mainWriter := zerolog.FilteredLevelWriter{
Writer: zerolog.LevelWriterAdapter{mainLog},
Level: zerolog.InfoLevel,
}
errorWriter := zerolog.FilteredLevelWriter{
Writer: zerolog.LevelWriterAdapter{errorLog},
Level: zerolog.ErrorLevel,
}
multi := zerolog.MultiLevelWriter(
mainWriter,
errorWriter,
zerolog.ConsoleWriter{Out: os.Stdout},
)
logger := zerolog.New(multi).Level(zerolog.DebugLevel)// Buffer debug logs, only write if error occurs
trigger := &zerolog.TriggerLevelWriter{
Writer: os.Stdout,
ConditionalLevel: zerolog.DebugLevel,
TriggerLevel: zerolog.ErrorLevel,
}
logger := zerolog.New(trigger).Level(zerolog.DebugLevel)
logger.Debug().Msg("operation started")
logger.Debug().Msg("processing item 1")
logger.Debug().Msg("processing item 2")
if err != nil {
logger.Error().Err(err).Msg("operation failed")
// All debug messages are now written
}file, _ := os.Create("app.log")
syncFile := zerolog.SyncWriter(file)
logger := zerolog.New(syncFile).With().Timestamp().Logger()
// Safe from multiple goroutines
for i := 0; i < 10; i++ {
go func(id int) {
logger.Info().Int("worker", id).Msg("processing")
}(i)
}ConsoleWriter parses JSON and reformats it, adding overhead. Use it for development, not production.
// Good for development
if debug {
logger = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr})
}
// Good for production
logger = zerolog.New(os.Stdout)Close writers that implement io.Closer to ensure buffers are flushed:
file, _ := os.Create("app.log")
defer file.Close()
logger := zerolog.New(file)
// ... logging ...For high-throughput applications, use diode.Writer for non-blocking writes:
import "github.com/rs/zerolog/diode"
file, _ := os.Create("app.log")
diodeWriter := diode.NewWriter(file, 1000, 10*time.Millisecond, nil)
defer diodeWriter.Close()
logger := zerolog.New(diodeWriter)See Diode Writer for details.
Only use SyncWriter when necessary. File writes on POSIX systems are already atomic:
// Usually unnecessary for files
file, _ := os.Create("app.log")
logger := zerolog.New(file) // Already safe
// Necessary for custom writers
customWriter := &MyWriter{}
logger := zerolog.New(zerolog.SyncWriter(customWriter))Heavy customization of ConsoleWriter adds overhead. Keep it simple:
// Simple is better
writer := zerolog.ConsoleWriter{
Out: os.Stderr,
TimeFormat: time.RFC3339,
}diode.Writer for high-throughput, non-blocking writes