Logging with Context

Learn how to implement structured logging with context in Go applications for better traceability and debugging

Structured logging with context in Go can significantly improve traceability and make debugging easier by providing additional metadata with each log entry. Starting with Go 1.21, the standard library includes the powerful slog package for structured logging.

Using Slog with Context

The slog package is part of the Go standard library and provides modern structured logging capabilities with excellent performance.

Setting Up Slog

Since slog is part of the standard library, no additional installation is needed. Here's a simple example:

package main

import (
	"context"
	"log/slog"
	"os"
)

func main() {
	// Create a JSON logger
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))

	// Set as default logger
	slog.SetDefault(logger)

	// Add default attributes to all log entries
	baseLogger := logger.With(
		"app", "example",
		"env", "production",
	)

	// Create a context with a correlation ID
	ctx := context.WithValue(context.Background(), "correlation_id", "12345")
	logWithContext(ctx, baseLogger)
}

func logWithContext(ctx context.Context, logger *slog.Logger) {
	correlationID := ctx.Value("correlation_id")

	// Create a logger with correlation ID
	l := logger.With("correlation_id", correlationID)

	l.Info("Starting processing")
	// Simulate processing
	l.LogAttrs(ctx, slog.LevelInfo, "Loading data",
		slog.String("step", "load data"))
	l.LogAttrs(ctx, slog.LevelInfo, "Processing data",
		slog.String("step", "process data"))
	l.Info("Processing completed")
}

Using Context in HTTP Handlers

Here's how to integrate slog into HTTP handlers for request logging:

package main

import (
	"context"
	"log/slog"
	"net/http"
	"os"
)

func main() {
	// Initialize JSON logger
	logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
		Level: slog.LevelInfo,
	}))
	slog.SetDefault(logger)

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// Extract or generate a request ID for tracing
		requestID := r.Header.Get("X-Request-ID")
		if requestID == "" {
			requestID = "default-id"
		}

		// Attach the request ID to context
		ctx := context.WithValue(r.Context(), "request_id", requestID)

		// Create request-scoped logger
		reqLogger := logger.With(
			"request_id", requestID,
			"path", r.URL.Path,
			"method", r.Method,
		)

		handleRequest(ctx, reqLogger, w, r)
	})

	http.ListenAndServe(":8080", nil)
}

func handleRequest(ctx context.Context, logger *slog.Logger, w http.ResponseWriter, r *http.Request) {
	logger.InfoContext(ctx, "Handling request")
	// Simulate processing
	w.Write([]byte("Request handled"))
	logger.InfoContext(ctx, "Request processed")
}

Best Practices

  • Use structural logging to include metadata, which aids in debugging and tracing.
  • Attach unique identifiers (e.g., request ID, user ID) to logs using context.
  • Keep log output consistent and meaningful by following a logging format across your application.
  • Centralize logging setup to easily adjust configurations and logging levels as needed.

Common Pitfalls

  • Failing to propagate the context through the application functions and layers might lose critical tracing information.
  • Overloading context with excessive data can lead to performance issues.
  • Logging sensitive information without appropriate care can lead to security risks.

Performance Tips

  • Use asynchronous logging if supported by the library to reduce performance overhead by avoiding I/O blocking operations.
  • Consider the verbosity level of the logs, adjusting logging level during development and production to avoid unnecessary performance overhead.
  • Regularly rotate and archive logs to prevent log files from becoming too large, which can negatively impact performance.