Logging in Go

Logging in Go

Logging is essential for debugging, monitoring, and maintaining Go applications. Go’s standard library provides basic logging, and there are several popular third-party libraries for more advanced features.

Standard Library Logging

Go’s log package provides basic logging functionality:

package main

import (
    "log"
    "os"
)

func main() {
    // Basic logging
    log.Println("This is a log message")
    
    // Formatted logging
    name := "Alice"
    age := 30
    log.Printf("User %s is %d years old", name, age)
    
    // Fatal logging (exits program)
    // log.Fatal("This is a fatal error")
    
    // Panic logging (panics)
    // log.Panic("This is a panic")
    
    // Custom logger
    logger := log.New(os.Stdout, "MYAPP: ", log.LstdFlags)
    logger.Println("Custom prefix message")
    
    // Log to file
    file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()
    
    fileLogger := log.New(file, "LOG: ", log.LstdFlags|log.Lshortfile)
    fileLogger.Println("This goes to file")
}

Log Levels

Implementing different log levels:

package main

import (
    "log"
    "os"
)

type Logger struct {
    info  *log.Logger
    warn  *log.Logger
    error *log.Logger
}

func NewLogger() *Logger {
    flags := log.LstdFlags | log.Lshortfile
    
    return &Logger{
        info:  log.New(os.Stdout, "INFO: ", flags),
        warn:  log.New(os.Stdout, "WARN: ", flags),
        error: log.New(os.Stderr, "ERROR: ", flags),
    }
}

func (l *Logger) Info(msg string) {
    l.info.Println(msg)
}

func (l *Logger) Warn(msg string) {
    l.warn.Println(msg)
}

func (l *Logger) Error(msg string) {
    l.error.Println(msg)
}

func main() {
    logger := NewLogger()
    
    logger.Info("Application started")
    logger.Warn("This is a warning")
    logger.Error("Something went wrong")
}

Structured Logging with Logrus

Logrus is a popular structured logging library:

package main

import (
    "os"
    "time"
    
    "github.com/sirupsen/logrus"
)

func main() {
    // Create logger
    logger := logrus.New()
    
    // Set log level
    logger.SetLevel(logrus.DebugLevel)
    
    // Set format
    logger.SetFormatter(&logrus.JSONFormatter{})
    
    // Log to file
    file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        logger.Fatal(err)
    }
    logger.SetOutput(file)
    
    // Basic logging
    logger.Info("Application started")
    logger.Warn("This is a warning")
    logger.Error("This is an error")
    
    // Structured logging with fields
    logger.WithFields(logrus.Fields{
        "user_id": 123,
        "action":  "login",
        "ip":      "192.168.1.1",
    }).Info("User login successful")
    
    // Different log levels
    logger.Debug("Debug message")
    logger.Info("Info message")
    logger.Warn("Warning message")
    logger.Error("Error message")
    
    // With error
    _, err = os.Open("nonexistent.txt")
    logger.WithError(err).Error("Failed to open file")
    
    // With timing
    start := time.Now()
    time.Sleep(100 * time.Millisecond)
    logger.WithField("duration", time.Since(start)).Info("Operation completed")
}

High-Performance Logging with Zap

Zap is designed for high-performance logging:

package main

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

func main() {
    // Create logger
    logger, err := zap.NewProduction()
    if err != nil {
        panic(err)
    }
    defer logger.Sync() // Flush any buffered log entries
    
    // Basic logging
    logger.Info("Application started")
    logger.Error("Something went wrong")
    
    // Structured logging
    logger.Info("User login",
        zap.String("username", "john_doe"),
        zap.Int("user_id", 123),
        zap.String("ip", "192.168.1.1"),
    )
    
    // With error
    err = someFunction()
    if err != nil {
        logger.Error("Operation failed",
            zap.Error(err),
            zap.String("operation", "database_query"),
        )
    }
    
    // Development logger (more readable)
    devLogger, _ := zap.NewDevelopment()
    devLogger.Info("Development mode logging",
        zap.String("key", "value"),
        zap.Int("number", 42),
    )
    
    // Custom logger
    config := zap.Config{
        Level:       zap.NewAtomicLevelAt(zap.InfoLevel),
        Development: false,
        Sampling: &zap.SamplingConfig{
            Initial:    100,
            Thereafter: 100,
        },
        Encoding:         "json",
        EncoderConfig:    zap.NewProductionEncoderConfig(),
        OutputPaths:      []string{"stdout", "app.log"},
        ErrorOutputPaths: []string{"stderr"},
    }
    
    customLogger, err := config.Build()
    if err != nil {
        panic(err)
    }
    
    customLogger.Info("Custom logger message")
}

Contextual Logging

Adding context to logs:

package main

import (
    "context"
    "net/http"
    
    "github.com/sirupsen/logrus"
)

type contextKey string

const loggerKey contextKey = "logger"

func withLogger(ctx context.Context, logger *logrus.Entry) context.Context {
    return context.WithValue(ctx, loggerKey, logger)
}

func getLogger(ctx context.Context) *logrus.Entry {
    if logger, ok := ctx.Value(loggerKey).(*logrus.Entry); ok {
        return logger
    }
    // Fallback to default logger
    return logrus.NewEntry(logrus.StandardLogger())
}

// Middleware for HTTP logging
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Create request-specific logger
        requestLogger := logrus.WithFields(logrus.Fields{
            "method": r.Method,
            "url":    r.URL.Path,
            "ip":     r.RemoteAddr,
        })
        
        ctx := withLogger(r.Context(), requestLogger)
        r = r.WithContext(ctx)
        
        // Wrap response writer to capture status code
        rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        
        next.ServeHTTP(rw, r.WithContext(ctx))
        
        // Log request completion
        requestLogger.WithFields(logrus.Fields{
            "status":   rw.statusCode,
            "duration": time.Since(start),
        }).Info("Request completed")
    })
}

type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

func handler(w http.ResponseWriter, r *http.Request) {
    logger := getLogger(r.Context())
    logger.Info("Processing request")
    
    // Simulate some work
    time.Sleep(50 * time.Millisecond)
    
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Hello, World!"))
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)
    
    loggedMux := loggingMiddleware(mux)
    
    logrus.Info("Server starting on :8080")
    http.ListenAndServe(":8080", loggedMux)
}

Log Rotation

Handling log file rotation:

package main

import (
    "gopkg.in/natefinch/lumberjack.v2"
    "github.com/sirupsen/logrus"
)

func setupLogging() {
    // Create rotating logger
    lumberjackLogger := &lumberjack.Logger{
        Filename:   "app.log",
        MaxSize:    10, // megabytes
        MaxBackups: 3,
        MaxAge:     28, // days
        Compress:   true,
    }
    
    // Use lumberjack with logrus
    logrus.SetOutput(lumberjackLogger)
    logrus.SetFormatter(&logrus.JSONFormatter{})
    logrus.SetLevel(logrus.InfoLevel)
}

func main() {
    setupLogging()
    
    for i := 0; i < 1000; i++ {
        logrus.WithField("iteration", i).Info("Log message")
    }
}

Centralized Logging

Sending logs to external systems:

package main

import (
    "bytes"
    "encoding/json"
    "net/http"
    
    "github.com/sirupsen/logrus"
)

type HTTPHook struct {
    URL string
}

func (h *HTTPHook) Fire(entry *logrus.Entry) error {
    // Convert log entry to JSON
    data, err := entry.String()
    if err != nil {
        return err
    }
    
    // Send to HTTP endpoint
    resp, err := http.Post(h.URL, "application/json", bytes.NewBufferString(data))
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    return nil
}

func (h *HTTPHook) Levels() []logrus.Level {
    return []logrus.Level{
        logrus.ErrorLevel,
        logrus.FatalLevel,
        logrus.PanicLevel,
    }
}

func main() {
    logger := logrus.New()
    logger.SetFormatter(&logrus.JSONFormatter{})
    
    // Add HTTP hook for errors
    logger.AddHook(&HTTPHook{
        URL: "https://api.logservice.com/logs",
    })
    
    logger.Info("This is an info message")  // Won't be sent to HTTP
    logger.Error("This is an error message") // Will be sent to HTTP
}

Best Practices

  1. Use structured logging: Include relevant context in logs
  2. Choose appropriate log levels: Don’t log everything at info level
  3. Log errors with context: Include stack traces and relevant data
  4. Use consistent formatting: JSON for production, text for development
  5. Rotate log files: Prevent disk space issues
  6. Consider performance: Use efficient logging libraries for high-throughput apps
  7. Include timestamps: Always know when events occurred
  8. Log to multiple outputs: Console for development, files for production
  9. Handle logging errors: Don’t let logging failures crash your app
  10. Monitor logs: Set up alerts for error patterns

Choosing a Logging Library

  • Standard library: Simple applications, no external dependencies
  • Logrus: Popular, feature-rich, good for most applications
  • Zap: High performance, structured logging, complex configurations
  • ZeroLog: JSON-only, very fast, minimal allocations

Effective logging is crucial for maintaining and debugging Go applications. Choose the right logging approach based on your application’s needs and performance requirements.

For more on error handling, check our error handling tutorial. If you’re building web applications, see the web servers tutorial.

Last updated on