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

writers-and-output.mddocs/

Writers and Output

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.

Imports

import (
    "os"
    "github.com/rs/zerolog"
)

LevelWriter Interface

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)
}

LevelWriterAdapter

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() error

Example:

file, _ := os.Create("app.log")
adapter := zerolog.LevelWriterAdapter{Writer: file}

logger := zerolog.New(adapter)
logger.Info().Msg("message")

SyncWriter

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.Writer

Example:

// 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.

MultiLevelWriter

Write to multiple destinations simultaneously, similar to Unix tee command.

// Create writer that duplicates writes to all provided writers
func MultiLevelWriter(writers ...io.Writer) LevelWriter

Example:

// 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()

ConsoleWriter

Human-friendly, optionally colorized console output for development. Parses JSON log output and formats it for readability.

Type Definition

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
}

Constructor

// Create and initialize ConsoleWriter with options
func NewConsoleWriter(options ...func(w *ConsoleWriter)) ConsoleWriter

Example:

// 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)

Methods

// 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() error

Customization Examples

Custom 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)

Formatter Types

// Transform input value to formatted string
type Formatter func(interface{}) string

// Format value based on field name
type FormatterByFieldName func(interface{}, string) string

Example:

customFormatter := func(i interface{}) string {
    return fmt.Sprintf("[%v]", i)
}

writer := zerolog.ConsoleWriter{
    Out:              os.Stdout,
    FormatFieldValue: customFormatter,
}

FilteredLevelWriter

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() error

Example:

// 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 WarnLevel

Route 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)

TriggerLevelWriter

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() error

Example:

// 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 written

Constants:

// Maximum buffer size before pooling is disabled (64 KiB)
const TriggerLevelWriterBufferReuseLimit = 64 * 1024

TestWriter

Write 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) TestWriter

Example:

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")
}

Syslog Writers

Writers for syslog-compatible outputs.

SyslogWriter Interface

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
}

Syslog Wrappers

// Wrap syslog writer to route by level
func SyslogLevelWriter(w SyslogWriter) LevelWriter

// Wrap syslog writer with CEE format prefix
func SyslogCEEWriter(w SyslogWriter) LevelWriter

Example:

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")

Common Patterns

Development vs Production

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()
}

Multi-Destination Logging

// 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()

Level-Based Routing

// 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)

Debug Context on Error

// 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
}

Thread-Safe File Writing

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)
}

Best Practices

1. Use ConsoleWriter for Development Only

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)

2. Close Writers When Done

Close writers that implement io.Closer to ensure buffers are flushed:

file, _ := os.Create("app.log")
defer file.Close()

logger := zerolog.New(file)
// ... logging ...

3. Consider Non-Blocking Writes

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.

4. Use SyncWriter Judiciously

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))

5. Customize ConsoleWriter Minimally

Heavy customization of ConsoleWriter adds overhead. Keep it simple:

// Simple is better
writer := zerolog.ConsoleWriter{
    Out:        os.Stderr,
    TimeFormat: time.RFC3339,
}

Performance Considerations

  • ConsoleWriter adds parsing and formatting overhead (development only)
  • SyncWriter adds mutex locking overhead
  • MultiLevelWriter multiplies write operations
  • FilteredLevelWriter adds level checking
  • TriggerLevelWriter adds buffering overhead
  • File writes are generally thread-safe on POSIX systems
  • Consider diode.Writer for high-throughput, non-blocking writes

Thread Safety

  • SyncWriter: Thread-safe (mutex-protected)
  • MultiLevelWriter: Thread-safe if underlying writers are thread-safe
  • ConsoleWriter: Not thread-safe (wrap with SyncWriter if needed)
  • FilteredLevelWriter: Thread-safe if underlying writer is thread-safe
  • TriggerLevelWriter: Not thread-safe (use with single goroutine)
  • File writers: Thread-safe on POSIX and Windows

See Also