Zero Allocation JSON Logger - High-performance structured logging library for Go
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 writesInstall with Tessl CLI
npx tessl i tessl/golang-github-com-rs-zerolog