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.