Zero Allocation JSON Logger - High-performance structured logging library for Go
The diode subpackage provides a thread-safe, lock-free, non-blocking writer implementation using a ring buffer (diode pattern). This is ideal for high-throughput applications where logging should never block the main execution path.
import (
"time"
"github.com/rs/zerolog/diode"
)A diode writer wraps any io.Writer and makes writes non-blocking by using a lock-free ring buffer. When the buffer is full, messages are dropped rather than blocking. This ensures logging never slows down your application.
Key features:
type Writer struct {
// unexported fields
}// Create non-blocking writer with ring buffer
func NewWriter(w io.Writer, size int, pollInterval time.Duration, f Alerter) WriterParameters:
w io.Writer - Underlying writer (e.g., os.File, network socket)size int - Ring buffer size (number of messages to buffer)pollInterval time.Duration - Polling interval (0 for waiter mode)f Alerter - Callback for dropped messages (nil to ignore)Returns: Configured diode writer
Example:
file, _ := os.Create("app.log")
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, func(missed int) {
fmt.Printf("Dropped %d log messages\n", missed)
})
defer wr.Close()
logger := zerolog.New(wr).With().Timestamp().Logger()// Callback for dropped messages
type Alerter func(missed int)The alerter is called when messages are dropped due to a full buffer. It receives the count of dropped messages.
Example:
// Log dropped messages to stderr
alerter := func(missed int) {
fmt.Fprintf(os.Stderr, "WARNING: Dropped %d log messages\n", missed)
}
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, alerter)Example with metrics:
alerter := func(missed int) {
metrics.DroppedLogs.Add(float64(missed))
fmt.Fprintf(os.Stderr, "Dropped %d logs\n", missed)
}
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, alerter)Nil alerter (silent drops):
// Silently drop messages without notification
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, nil)// Write implements io.Writer (non-blocking)
func (dw Writer) Write(p []byte) (n int, err error)
// Close stops polling and closes underlying writer if io.Closer
func (dw Writer) Close() errorExample:
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, nil)
defer wr.Close()
logger := zerolog.New(wr)
logger.Info().Msg("message") // Non-blocking writeThe diode writer has two modes for reading from the ring buffer:
Polls the buffer at regular intervals. Better for steady, high-throughput logging.
// Poll every 10 milliseconds
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, nil)Characteristics:
Blocks waiting for data in the buffer. Better for low-latency logging.
// Wait for data (no polling)
wr := diode.NewWriter(file, 1000, 0, nil)Characteristics:
package main
import (
"os"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/diode"
)
func main() {
file, err := os.Create("app.log")
if err != nil {
panic(err)
}
// Non-blocking writer with 1000 message buffer
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, func(missed int) {
fmt.Fprintf(os.Stderr, "Dropped %d messages\n", missed)
})
defer wr.Close()
logger := zerolog.New(wr).With().Timestamp().Logger()
// Log without blocking
for i := 0; i < 10000; i++ {
logger.Info().Int("i", i).Msg("iteration")
}
}// Large buffer for high throughput
wr := diode.NewWriter(file, 10000, 100*time.Millisecond, func(missed int) {
// Track drops with metrics
droppedLogsCounter.Add(float64(missed))
})
defer wr.Close()
logger := zerolog.New(wr)
// Heavy logging workload
for i := 0; i < 1000000; i++ {
logger.Debug().Int("iteration", i).Msg("processing")
}// Waiter mode for low latency
wr := diode.NewWriter(file, 500, 0, func(missed int) {
fmt.Fprintf(os.Stderr, "Dropped %d messages\n", missed)
})
defer wr.Close()
logger := zerolog.New(wr)
// Logs written with minimal latency
logger.Info().Msg("low latency message")// Combine diode writer with other writers
file, _ := os.Create("app.log")
diodeWriter := diode.NewWriter(file, 1000, 10*time.Millisecond, nil)
defer diodeWriter.Close()
// Write to both console (blocking) and file (non-blocking)
multi := zerolog.MultiLevelWriter(
os.Stdout,
diodeWriter,
)
logger := zerolog.New(multi)var droppedTotal int64
alerter := func(missed int) {
total := atomic.AddInt64(&droppedTotal, int64(missed))
// Log to stderr
fmt.Fprintf(os.Stderr, "Dropped %d messages (total: %d)\n", missed, total)
// Send to monitoring
metrics.LogDrops.Add(float64(missed))
// Alert if too many drops
if missed > 100 {
sendAlert("High log drop rate: %d", missed)
}
}
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, alerter)
defer wr.Close()// Non-blocking network logging
conn, err := net.Dial("tcp", "logserver:514")
if err != nil {
panic(err)
}
wr := diode.NewWriter(conn, 5000, 50*time.Millisecond, func(missed int) {
fmt.Fprintf(os.Stderr, "Network congestion: dropped %d logs\n", missed)
})
defer wr.Close()
logger := zerolog.New(wr)Choose buffer size based on your logging rate and acceptable loss:
Small buffer (100-500):
wr := diode.NewWriter(file, 500, 10*time.Millisecond, alerter)Medium buffer (1000-5000):
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, alerter)Large buffer (10000+):
wr := diode.NewWriter(file, 10000, 100*time.Millisecond, alerter)Short interval (1-10ms):
wr := diode.NewWriter(file, 1000, 5*time.Millisecond, nil)Medium interval (10-100ms):
wr := diode.NewWriter(file, 1000, 50*time.Millisecond, nil)Long interval (100-1000ms):
wr := diode.NewWriter(file, 1000, 500*time.Millisecond, nil)// No polling delay, wake on data
wr := diode.NewWriter(file, 1000, 0, nil)Closing flushes remaining messages and stops the poller:
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, nil)
defer wr.Close()Always provide an alerter to track drops:
// Good - monitor drops
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, func(missed int) {
metrics.DroppedLogs.Add(float64(missed))
})
// Avoid - silent drops
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, nil)Size buffer for peak load plus headroom:
// Calculate buffer size
peakLogsPerSecond := 10000
bufferSeconds := 2 // Handle 2 seconds of peak load
bufferSize := peakLogsPerSecond * bufferSeconds
wr := diode.NewWriter(file, bufferSize, 10*time.Millisecond, alerter)Only use diode writer where non-blocking is critical:
// Good - hot path needs non-blocking
func handleRequest(w http.ResponseWriter, r *http.Request) {
logger.Info().Msg("request received") // Non-blocking
// ... critical code ...
}
// Avoid - application startup doesn't need non-blocking
func main() {
logger := zerolog.New(os.Stdout) // Simple writer is fine
logger.Info().Msg("starting up")
}Test with realistic load to tune buffer size:
func TestLoggingUnderLoad(t *testing.T) {
var dropped int64
alerter := func(missed int) {
atomic.AddInt64(&dropped, int64(missed))
}
wr := diode.NewWriter(io.Discard, 1000, 10*time.Millisecond, alerter)
defer wr.Close()
logger := zerolog.New(wr)
// Generate load
for i := 0; i < 100000; i++ {
logger.Info().Int("i", i).Msg("test")
}
wr.Close()
droppedCount := atomic.LoadInt64(&dropped)
if droppedCount > 0 {
t.Logf("Dropped %d messages, consider increasing buffer", droppedCount)
}
}Diode writer is lossy by design. Don't use for critical logs:
// Good - audit logs use blocking writer
auditLogger := zerolog.New(auditFile) // Blocking, reliable
// Regular logs use non-blocking writer
appLogger := zerolog.New(diodeWriter) // Non-blocking, lossyUse diode writer when:
Don't use diode writer when:
| Feature | DiodeWriter | SyncWriter |
|---|---|---|
| Blocking | Non-blocking | Blocking |
| Locking | Lock-free | Mutex-locked |
| Reliability | Lossy | Lossless |
| Throughput | Very high | High |
| Latency | Lowest | Low |
| Use case | Hot paths | General use |
Example:
// SyncWriter - blocks but reliable
syncWr := zerolog.SyncWriter(file)
logger := zerolog.New(syncWr)
// DiodeWriter - never blocks but lossy
diodeWr := diode.NewWriter(file, 1000, 10*time.Millisecond, nil)
logger := zerolog.New(diodeWr)Diode writer is fully thread-safe and lock-free. Multiple goroutines can safely write concurrently without coordination:
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, nil)
logger := zerolog.New(wr)
// Safe concurrent access
for i := 0; i < 100; i++ {
go func(id int) {
logger.Info().Int("worker", id).Msg("processing")
}(i)
}Diode writer uses buffer pooling to minimize allocations:
Memory usage:
Memory = (buffer_size × average_log_size) + overheadExample:
// Buffer: 1000 messages
// Avg message size: 200 bytes
// Memory: ~200KB + overhead
wr := diode.NewWriter(file, 1000, 10*time.Millisecond, nil)Diode writer always returns success from Write(), even if the buffer is full:
n, err := diodeWriter.Write(data)
// n == len(data), err == nil (always)
// Message may be dropped if buffer fullActual write errors on the underlying writer are not propagated to the caller. Monitor the underlying writer separately if needed.
Install with Tessl CLI
npx tessl i tessl/golang-github-com-rs-zerolog