Uber Zap

High-performance, structured, leveled logging library developed by Uber. Achieves 67 ns/op, 0 allocations performance with speed comparable to Zerolog. Surpasses Zerolog in customization capabilities, popular in Go ecosystem.

LoggingGoGolangStructured LoggingPerformanceDebug

Library

Zap

Overview

Zap is developed as "Blazing fast, structured, leveled logging in Go" and is a high-performance logging library for Go applications. Developed and maintained by Uber, it achieves exceptional performance with minimal memory allocation. Supporting everything from microservices to resource-constrained environments, it handles both structured and plain text logging for all Go applications.

Details

Zap 1.27.0 remains the de facto standard logging library in the Go ecosystem as of 2025, with active development. Prioritizing high performance above all, it boasts speeds 4-10 times faster than competing libraries while minimizing garbage collection overhead. It supports readable console output for development environments and JSON structured output for production, providing dynamic log level control and a type-safe field system.

Key Features

  • Exceptional Performance: Benchmark-proven speed, 4-10x faster than competing libraries
  • Memory Efficiency: Allocation-free logging minimizes GC overhead
  • Structured Logging: Native JSON output improves searchability and analysis
  • Two Logger Types: High-performance Logger (type-safe) and user-friendly SugaredLogger
  • Flexible Configuration: Configuration presets optimized for development and production environments
  • Dynamic Level Control: Runtime log level changes and filtering

Pros and Cons

Pros

  • Fastest logging library in Go ecosystem (benchmark proven)
  • Exceptional memory efficiency through allocation-free design
  • Excellent searchability and analysis through structured logging
  • Stable maintenance and proven track record by Uber
  • Flexible usage with two logger types (performance vs convenience)
  • Rich ecosystem and active community

Cons

  • API somewhat more complex compared to other logging libraries
  • Type-safe Logger requires detailed type specifications
  • Production configuration optimization requires deep understanding
  • Limited support for older Go versions (latest 2 versions only)
  • Need to manage error handling and Sync() calls
  • High initial setup cost for complex configurations

References

Code Examples

Basic Setup

# Install Zap package
go get go.uber.org/zap

# Additional packages (as needed)
go get go.uber.org/zap/zapcore
go get gopkg.in/natefinch/lumberjack.v2  # For log rotation

Simple Logger Usage

package main

import (
    "time"
    
    "go.uber.org/zap"
)

func main() {
    // Production logger (JSON output)
    logger, _ := zap.NewProduction()
    defer logger.Sync() // Always call Sync() before program termination

    // Structured log output (type-safe)
    url := "https://example.com"
    logger.Info("failed to fetch URL",
        zap.String("url", url),
        zap.Int("attempt", 3),
        zap.Duration("backoff", time.Second),
    )

    // Use SugaredLogger (convenience-focused)
    sugar := logger.Sugar()
    
    // Structured log (key-value pairs)
    sugar.Infow("failed to fetch URL",
        "url", url,
        "attempt", 3,
        "backoff", time.Second,
    )
    
    // Printf-style logging
    sugar.Infof("Failed to fetch URL: %s", url)
}

Development and Production Configuration

package main

import (
    "os"
    
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

func main() {
    var logger *zap.Logger
    
    // Environment-specific logger configuration
    if os.Getenv("ENV") == "production" {
        // Production configuration (JSON format)
        logger, _ = zap.NewProduction()
    } else {
        // Development configuration (readable console output)
        logger, _ = zap.NewDevelopment()
    }
    defer logger.Sync()

    // Custom configuration example
    config := zap.NewProductionConfig()
    config.Level = zap.NewAtomicLevelAt(zap.DebugLevel)
    config.OutputPaths = []string{"stdout", "app.log"}
    config.ErrorOutputPaths = []string{"stderr", "error.log"}
    
    customLogger, _ := config.Build()
    defer customLogger.Sync()

    // Log level examples
    logger.Debug("Debug message")
    logger.Info("Info message")
    logger.Warn("Warning message")
    logger.Error("Error message")
    logger.DPanic("Development panic message")

    // Field-based logging
    logger.Info("User action",
        zap.String("user_id", "user123"),
        zap.String("action", "login"),
        zap.Time("timestamp", time.Now()),
        zap.Bool("success", true),
    )
}

Advanced Logger Configuration and Customization

package main

import (
    "os"
    "time"
    
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "gopkg.in/natefinch/lumberjack.v2"
)

func main() {
    // Custom encoder configuration
    encoderConfig := zapcore.EncoderConfig{
        TimeKey:        "timestamp",
        LevelKey:       "level",
        NameKey:        "logger",
        CallerKey:      "caller",
        MessageKey:     "message",
        StacktraceKey:  "stacktrace",
        LineEnding:     zapcore.DefaultLineEnding,
        EncodeLevel:    zapcore.LowercaseLevelEncoder,
        EncodeTime:     zapcore.ISO8601TimeEncoder,
        EncodeDuration: zapcore.StringDurationEncoder,
        EncodeCaller:   zapcore.ShortCallerEncoder,
    }

    // Log rotation configuration (using lumberjack)
    w := zapcore.AddSync(&lumberjack.Logger{
        Filename:   "/var/log/myapp/app.log",
        MaxSize:    500, // megabytes
        MaxBackups: 3,   // number of backups to retain
        MaxAge:     28,  // days
        Compress:   true, // compress
    })

    // Multiple output configuration
    consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig)
    fileEncoder := zapcore.NewJSONEncoder(encoderConfig)
    
    core := zapcore.NewTee(
        zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), zapcore.DebugLevel),
        zapcore.NewCore(fileEncoder, w, zapcore.InfoLevel),
    )

    // Create custom logger
    logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
    defer logger.Sync()

    // Usage example
    logger.Info("Application started",
        zap.String("version", "1.0.0"),
        zap.String("environment", os.Getenv("ENV")),
    )
}

Web Application Integration

package main

import (
    "context"
    "net/http"
    "time"
    
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

// Logger setup for middleware
func setupLogger() *zap.Logger {
    config := zap.NewProductionConfig()
    config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
    logger, _ := config.Build()
    return logger
}

// HTTP request logging middleware
func loggingMiddleware(logger *zap.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            
            // Log request information
            logger.Info("HTTP request started",
                zap.String("method", r.Method),
                zap.String("url", r.URL.String()),
                zap.String("remote_addr", r.RemoteAddr),
                zap.String("user_agent", r.UserAgent()),
            )

            // Process request
            next.ServeHTTP(w, r)

            // Log response information
            duration := time.Since(start)
            logger.Info("HTTP request completed",
                zap.String("method", r.Method),
                zap.String("url", r.URL.String()),
                zap.Duration("duration", duration),
                zap.Int64("duration_ms", duration.Milliseconds()),
            )
        })
    }
}

// Context-based logging
func handleUser(logger *zap.Logger) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        userID := r.URL.Query().Get("user_id")
        
        // Create user-specific logger
        userLogger := logger.With(
            zap.String("user_id", userID),
            zap.String("request_id", generateRequestID()),
        )

        ctx := context.WithValue(r.Context(), "logger", userLogger)
        r = r.WithContext(ctx)

        userLogger.Info("Starting user info retrieval")

        // Business logic
        if userID == "" {
            userLogger.Warn("User ID not specified")
            http.Error(w, "User ID required", http.StatusBadRequest)
            return
        }

        // Database operation simulation
        time.Sleep(50 * time.Millisecond)

        userLogger.Info("User info retrieval completed",
            zap.String("status", "success"),
        )

        w.WriteHeader(http.StatusOK)
        w.Write([]byte("User info retrieved"))
    }
}

func generateRequestID() string {
    return "req_" + time.Now().Format("20060102150405")
}

func main() {
    logger := setupLogger()
    defer logger.Sync()

    mux := http.NewServeMux()
    mux.Handle("/user", loggingMiddleware(logger)(handleUser(logger)))

    logger.Info("Server started", zap.Int("port", 8080))
    http.ListenAndServe(":8080", mux)
}

Error Handling and Performance Optimization

package main

import (
    "context"
    "errors"
    "sync"
    "time"
    
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

func main() {
    // Performance-optimized logger configuration
    config := zap.NewProductionConfig()
    config.Sampling = &zap.SamplingConfig{
        Initial:    100,
        Thereafter: 100,
    }
    logger, _ := config.Build()
    defer logger.Sync()

    // Error handling example
    if err := processData(logger); err != nil {
        logger.Error("Data processing error",
            zap.Error(err),
            zap.String("operation", "processData"),
            zap.Time("timestamp", time.Now()),
        )
    }

    // High-frequency logging performance test
    performanceTest(logger)
}

func processData(logger *zap.Logger) error {
    logger.Info("Starting data processing")

    // Error occurs during processing
    if err := doSomething(); err != nil {
        // Wrap error and add context information
        logger.Error("Error occurred during processing",
            zap.Error(err),
            zap.String("step", "doSomething"),
            zap.Int("retry_count", 0),
        )
        return err
    }

    logger.Info("Data processing completed")
    return nil
}

func doSomething() error {
    return errors.New("simulated error")
}

func performanceTest(logger *zap.Logger) {
    // High-frequency logging performance test
    start := time.Now()
    const iterations = 100000

    var wg sync.WaitGroup
    wg.Add(iterations)

    for i := 0; i < iterations; i++ {
        go func(index int) {
            defer wg.Done()
            
            // Conditional logging (optimization with level check)
            if logger.Core().Enabled(zapcore.DebugLevel) {
                logger.Debug("Performance test",
                    zap.Int("iteration", index),
                    zap.String("test_type", "concurrent"),
                )
            }

            if index%1000 == 0 {
                logger.Info("Progress status",
                    zap.Int("completed", index),
                    zap.Float64("progress", float64(index)/float64(iterations)*100),
                )
            }
        }(i)
    }

    wg.Wait()
    duration := time.Since(start)

    logger.Info("Performance test completed",
        zap.Int("iterations", iterations),
        zap.Duration("total_duration", duration),
        zap.Float64("logs_per_second", float64(iterations)/duration.Seconds()),
    )
}

Dynamic Log Level Control

package main

import (
    "net/http"
    
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

type DynamicLogger struct {
    *zap.Logger
    level *zap.AtomicLevel
}

func NewDynamicLogger() *DynamicLogger {
    level := zap.NewAtomicLevel()
    level.SetLevel(zap.InfoLevel)

    config := zap.NewProductionConfig()
    config.Level = level

    logger, _ := config.Build()

    return &DynamicLogger{
        Logger: logger,
        level:  &level,
    }
}

func (dl *DynamicLogger) SetLevel(level zapcore.Level) {
    dl.level.SetLevel(level)
    dl.Info("Log level changed",
        zap.String("new_level", level.String()),
    )
}

func (dl *DynamicLogger) GetLevel() zapcore.Level {
    return dl.level.Level()
}

// HTTP endpoint for log level control
func (dl *DynamicLogger) handleLevelChange(w http.ResponseWriter, r *http.Request) {
    levelStr := r.URL.Query().Get("level")
    
    var level zapcore.Level
    if err := level.UnmarshalText([]byte(levelStr)); err != nil {
        dl.Error("Invalid log level",
            zap.String("level", levelStr),
            zap.Error(err),
        )
        http.Error(w, "Invalid log level", http.StatusBadRequest)
        return
    }

    dl.SetLevel(level)
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Log level updated to " + level.String()))
}

func main() {
    dl := NewDynamicLogger()
    defer dl.Sync()

    // Dynamic level change test
    dl.Debug("This is a debug message") // Not output in initial state
    dl.Info("This is an info message")  // Output

    // Change level to debug
    dl.SetLevel(zap.DebugLevel)
    
    dl.Debug("This is a debug message") // Now output
    dl.Info("This is an info message")  // Still output

    // Dynamic control via HTTP server
    http.HandleFunc("/log-level", dl.handleLevelChange)
    
    dl.Info("Dynamic log level control server started", zap.Int("port", 8080))
    // http.ListenAndServe(":8080", nil) // Uncomment for actual server startup
}