Introduction
Uber's Zap logger uses buffered I/O for performance -- log entries are written to an in-memory buffer and flushed to disk periodically or when the buffer fills. When a Go application exits (normally or due to a signal), any log entries still in the buffer are lost unless logger.Sync() is called. This is particularly problematic during shutdown sequences where the most important logs -- "closing database connection", "flushing cache", "shutdown complete" -- are the last ones written and therefore most likely to be in the buffer. Missing these logs makes post-incident debugging significantly harder.
Symptoms
Application logs are missing the final entries:
$ tail -20 /var/log/myapp/app.log
2024-03-15T10:23:40.123Z INFO Processing batch 42
2024-03-15T10:23:41.456Z INFO Batch 42 complete
2024-03-15T10:23:45.789Z INFO Received SIGTERM, shutting down
# Missing: "Database connection closed", "Cache flushed", "Shutdown complete"The application code calls these log functions but they never appear in the log file:
func shutdown() {
logger.Info("Shutting down")
db.Close()
logger.Info("Database connection closed") // Never appears in log
cache.Flush()
logger.Info("Cache flushed") // Never appears in log
}Common Causes
- Not calling logger.Sync() before exit: Buffered log entries never flushed
- defer logger.Sync() not reached: Panic or os.Exit(1) skips deferred functions
- os.Exit() bypasses defer:
os.Exit()terminates immediately without running defers - Zap configured with buffer but small size:
zapcore.BufferedWriteSyncerwith large buffer - Signal handler does not call Sync: SIGTERM handler logs shutdown steps but never syncs
- Logger replaced with NOP in tests: Test setup replaces logger but production code still references old logger
Step-by-Step Fix
Step 1: Always call Sync() with proper error handling
```go func main() { logger, _ := zap.NewProduction() defer func() { // Sync must be called with error check if err := logger.Sync(); err != nil { // stderr is always unbuffered fmt.Fprintf(os.Stderr, "failed to sync logger: %v\n", err) } }()
logger.Info("application starting") // ... application logic ... logger.Info("shutting down") // defer runs logger.Sync() automatically } ```
Step 2: Handle os.Exit() and panics
```go func main() { logger, _ := zap.NewProduction() sugar := logger.Sugar()
// Exit via function, not os.Exit() code := run(sugar) sugar.Sync() os.Exit(code) }
func run(sugar *zap.SugaredLogger) int { if err := runApp(); err != nil { sugar.Error("application failed", zap.Error(err)) return 1 } sugar.Info("application completed successfully") return 0 } ```
For panic recovery:
```go func main() { logger, _ := zap.NewProduction() defer logger.Sync()
defer func() { if r := recover(); r != nil { logger.Fatal("panic recovered", zap.Any("panic", r), zap.Stack("stack"), ) // Fatal calls Sync() internally before os.Exit(1) } }()
// ... application code ... } ```
Step 3: Sync in signal handler
```go func main() { logger, _ := zap.NewProduction() defer logger.Sync()
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop()
srv := startServer(ctx, logger)
<-ctx.Done() logger.Info("shutdown signal received")
// Sync before long shutdown operations logger.Sync()
if err := srv.Shutdown(context.Background()); err != nil { logger.Error("shutdown error", zap.Error(err)) }
logger.Info("shutdown complete") // defer logger.Sync() flushes the final messages } ```
Prevention
- Always
defer logger.Sync()immediately after logger creation - Never use
os.Exit()in production code -- return an exit code frommain()instead - Use
logger.Fatal()for fatal errors -- it callsSync()internally - Add a shutdown test that verifies the last log line appears in the log file
- Configure
zapcore.BufferedWriteSyncerwith a reasonable buffer size (default 256KB) - Monitor log file growth in production to detect missing log entries