log

package
v0.5.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 5, 2025 License: MIT Imports: 11 Imported by: 0

README

Logger Package

This package provides a structured, context-aware logging system designed for distributed tracing support. It emphasizes explicit logger dependency injection and context propagation rather than global state.

Core Design Principles

  1. No Global State: Loggers should be explicitly initialized and passed through constructors or contexts.
  2. Context Propagation: Use contexts to pass loggers across API boundaries.
  3. Structured Logging: Always use key-value pairs for structured information rather than formatted strings.
  4. Tracing Integration: Spans automatically receive log events when a logger is stored in a context with an active span.
  5. Pluggable Implementation: The Logger interface allows for different implementations (Zap, Noop, Span-aware).

Components

Logger Interface

The core Logger interface provides structured logging methods:

type Logger interface {
    Debug(msg string, keysAndValues ...any)
    Info(msg string, keysAndValues ...any)
    Warn(msg string, keysAndValues ...any)
    Error(msg string, keysAndValues ...any)
    Fatal(msg string, keysAndValues ...any)
    WithKV(key string, value any) Logger
    GetAllKV() []any
    WithName(name string) Logger
    Name() string
    AddCallerSkip(skip int) Logger
}
Available Implementations
  1. ZapLogger: Production-ready logger based on Uber's zap library
  2. NoopLogger: Discards all log messages (useful for testing)
  3. SpanLogger: Decorator that records log events to both a wrapped logger and a span

Usage Guide

Basic Initialization
// Initialize ZapLogger with configuration
conf := log.Config{
    Format: "json",    // "console", "logfmt", or "json"
    Level:  log.LevelInfo,  // LevelDebug, LevelInfo, LevelWarn, LevelError, LevelFatal
    Output: "stderr",  // "stdout", "stderr", or file path
}

logger := log.NewZapLogger(conf)

// For testing or when logging should be disabled
logger := log.NewNoopLogger()

Pass the logger explicitly to your components:

type Service struct {
    config Config
    logger log.Logger
}

func NewService(config Config, logger log.Logger) *Service {
    // Enhance logger with service info
    serviceLogger := logger.WithName("service").WithKV("serviceID", config.ServiceID)
    
    return &Service{
        config: config,
        logger: serviceLogger,
    }
}

// Use the injected logger
func (s *Service) DoSomething() {
    s.logger.Info("Operation started", "operation", "something")
    // ...
}
Context Propagation

For request-scoped operations with automatic span integration:

// In your HTTP handler or request entry point
func (s *Service) HandleRequest(ctx context.Context, req Request) {
    // Create a request-scoped logger
    logger := s.logger.WithKV("requestID", req.ID)
    
    // Store it in the context
    // If ctx has a valid OpenTelemetry span, the logger will be wrapped
    // with a SpanLogger that records events to both the logger and span
    ctx = log.SetContextLogger(ctx, logger)
    
    // Process the request
    result, err := s.processRequest(ctx, req)
    // ...
}

// In downstream functions
func (s *Service) processRequest(ctx context.Context, req Request) (Result, error) {
    // Extract logger from context (returns NoopLogger if none found)
    logger := log.FromContext(ctx)
    
    logger.Debug("Processing request", "id", req.ID)
    // ...
}
Working with OpenTelemetry Spans

When a context contains a valid OpenTelemetry span, SetContextLogger automatically wraps the logger with a SpanLogger:

// Start a span
ctx, span := tracer.Start(ctx, "operation")
defer span.End()

// Set logger in context - automatically creates SpanLogger
ctx = log.SetContextLogger(ctx, logger)

// All logs will be recorded to both the logger output and the span
log.FromContext(ctx).Info("Operation info", "key", "value")
// This creates a span event with the log data

Best Practices

Structured Logging

When writing logs, focus on events that are meaningful for understanding application behavior, diagnosing issues, or auditing important actions. Use clear, concise messages that describe what happened, not just that something happened.

Always use key-value pairs for structured logging instead of embedding details in formatted strings. This makes logs easier to search, filter, and analyze.

Guidelines for key-value pairs:

  • Use string keys that clearly describe the context (e.g., "userID", "orderID", "status").
  • Include identifiers, parameters, or state relevant to the log message.
  • Prefer structured data over embedding details in the message string.
  • Avoid sensitive information unless necessary and ensure compliance with privacy requirements.

Lifecycle Logging Practice:
If you emit a log/event that represents the start of a process (e.g., "processing", "starting", "pending"), always emit a corresponding terminal log/event (e.g., "processed", "completed", "stopped", or "failed").
This ensures:

  • Symmetry and Traceability: Every initiated action is explicitly finished or failed, avoiding ambiguity about system state.
  • Observability/Monitoring: Enables accurate measurement of durations, success rates, and detection of anomalies (such as stuck processes) in dashboards and alerting systems.
  • Consistency with State Machines: Logs should reflect state transitions, where each non-terminal state (like "processing") eventually leads to a terminal state ("completed" or "failed").
  • Debugging and Replay: Having both start and end logs allows for reliable system state reconstruction and easier debugging.

Examples:

// GOOD: Logs both start and completion of a process
logger.Info("Order processing started", "orderID", order.ID)
err := processOrder(order)
if err != nil {
    logger.Error("Order processing failed", "orderID", order.ID, "error", err)
} else {
    logger.Info("Order processed", "orderID", order.ID)
}

// BAD: Only logs the start, leaving completion ambiguous
logger.Info("Order processing started", "orderID", order.ID)
// ... no corresponding "processed" or "failed" log
Contextual Enrichment

Use WithKV() and WithName() to create derived loggers with additional context:

// Base component logger
logger := log.NewZapLogger(config)

// Enrich with component name
orderLogger := logger.WithName("orderbook")

// Further enrich with specific context
btcLogger := orderLogger.WithKV("market", "BTCUSD")
Level Usage Guidelines
  • Fatal: Critical failures requiring application termination
  • Error: Operation failures that don't require termination
  • Warn: Unexpected conditions that deserve attention
  • Info: Significant business events or state changes
  • Debug: Detailed information for debugging
Error Handling

Log errors at business boundaries with appropriate context:

// Low-level function - returns error without logging
func readConfig(path string) (Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return Config{}, fmt.Errorf("reading config: %w", err)
    }
    // ...
}

// Business function - logs with context
func (s *Service) Initialize(ctx context.Context) error {
    logger := log.FromContext(ctx)
    
    config, err := readConfig(s.configPath)
    if err != nil {
        logger.Error("Failed to initialize service", 
            "configPath", s.configPath,
            "error", err)
        return err
    }
    
    logger.Info("Service initialized", "config", config.Name)
    return nil
}
Key-Value Pairs

Ensure key-value pairs are properly formatted:

// GOOD - Even number of arguments with string keys
logger.Info("Message", "key1", "value1", "key2", 42)

// BAD - Odd number of arguments (missing value)
logger.Info("Message", "key1", "value1", "key2") // "MISSING" will be appended

// BAD - Non-string key
logger.Info("Message", 123, "value") // Will use "invalidKeysAndValues"

Testing

For unit tests, use the NoopLogger or create a test logger:

func TestService(t *testing.T) {
    // Option 1: Use noop logger
    logger := log.NewNoopLogger()
    
    // Option 2: Use real logger for debugging
    logger := log.NewZapLogger(log.Config{
        Format: "console",
        Level:  log.LevelDebug,
        Output: "stdout",
    })
    
    service := NewService(testConfig, logger)
    // ... run tests
}

Configuration

The Config struct supports environment variables:

type Config struct {
    Format string `env:"LOG_FORMAT" env-default:"console"`
    Level  Level  `env:"LOG_LEVEL" env-default:"info"`
    Output string `env:"LOG_OUTPUT" env-default:"stderr"`
}

Environment variables:

  • LOG_FORMAT: Output format (console, logfmt, json)
  • LOG_LEVEL: Minimum log level (debug, info, warn, error, fatal)
  • LOG_OUTPUT: Output destination (stderr, stdout, or file path)

Migration from Global Loggers

When migrating from global logger patterns:

  1. Replace global logger variables with constructor injection
  2. Update function signatures to accept logger or context parameters
  3. Use SetContextLogger and FromContext for cross-cutting concerns
  4. Replace string formatting with structured key-value logging

Performance Considerations

For high-frequency logging paths:

  1. Pre-create loggers with common fields using WithKV()
  2. Use appropriate log levels to reduce output volume
  3. Consider using NoopLogger in performance-critical paths where logging is optional

OpenTelemetry Integration

The package provides seamless integration with OpenTelemetry tracing:

  • SpanLogger: Automatically created when setting a logger in a context with an active span
  • OtelSpanEventRecorder: Records log events as span events with appropriate attributes
  • Error and Fatal levels are recorded as span errors with proper status

This ensures that logs are correlated with traces, providing better observability in distributed systems.

Documentation

Overview

Package log provides a structured, context-aware logging system with distributed tracing support.

The package is designed around explicit dependency injection and context propagation, avoiding global state and encouraging clean, testable code.

Core Types

The package centers around the Logger interface, which provides structured logging methods:

type Logger interface {
    Debug(msg string, keysAndValues ...any)
    Info(msg string, keysAndValues ...any)
    Warn(msg string, keysAndValues ...any)
    Error(msg string, keysAndValues ...any)
    Fatal(msg string, keysAndValues ...any)
    WithKV(key string, value any) Logger
    GetAllKV() []any
    WithName(name string) Logger
    Name() string
    AddCallerSkip(skip int) Logger
}

Three implementations are provided:

  • ZapLogger: A production-ready logger based on Uber's zap library
  • NoopLogger: A logger that discards all messages (useful for testing)
  • SpanLogger: A decorator that records logs to both a wrapped logger and a trace span

Basic Usage

Create a logger and use it directly:

conf := log.Config{
    Format: "json",
    Level:  log.LevelInfo,
    Output: "stderr",
}
logger := log.NewZapLogger(conf)
logger.Info("Application started", "version", "1.0.0")

Context Integration

The package provides context-aware logging with automatic span integration:

// Store logger in context
ctx = log.SetContextLogger(ctx, logger)

// Retrieve logger from context
logger := log.FromContext(ctx)

When SetContextLogger is called with a context containing a valid OpenTelemetry span, the logger is automatically wrapped with a SpanLogger that records events to both the logger output and the trace span.

Structured Logging

All logging methods accept key-value pairs for structured data:

logger.Info("User action",
    "userID", user.ID,
    "action", "login",
    "ip", request.RemoteAddr,
)

Logger Enrichment

Create derived loggers with additional context:

// Add a name hierarchy
serviceLogger := logger.WithName("auth-service")

// Add persistent key-value pairs
userLogger := serviceLogger.WithKV("userID", userID)

OpenTelemetry Integration

The package seamlessly integrates with OpenTelemetry tracing. When a logger is set in a context with an active span, log events are automatically recorded as span events:

ctx, span := tracer.Start(ctx, "operation")
defer span.End()

ctx = log.SetContextLogger(ctx, logger)
log.FromContext(ctx).Info("Operation started") // Recorded in both log and span

Error and Fatal level logs are recorded as span errors with appropriate status codes.

Using AddCallerSkip for Helper Functions

When you wrap logging calls in helper functions, use AddCallerSkip(1) to ensure the log output reports the correct source line from your application code, not the helper itself.

func handleError(logger log.Logger, err error) {
    // Skip this helper frame so the log points to the real caller
    logger.AddCallerSkip(1).Error("operation failed", "err", err)
}

func doSomething(logger log.Logger) {
    err := someOperation()
    if err != nil {
        handleError(logger, err) // Log will point here, not inside handleError
    }
}

Testing

For unit tests, use NoopLogger to avoid log output:

func TestSomething(t *testing.T) {
    logger := log.NewNoopLogger()
    service := NewService(logger)
    // ... test service
}

Environment Configuration

The Config struct supports environment variables:

  • LOG_FORMAT: Output format (console, logfmt, json)
  • LOG_LEVEL: Minimum log level (debug, info, warn, error, fatal)
  • LOG_OUTPUT: Output destination (stderr, stdout, or file path)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SetContextLogger

func SetContextLogger(ctx context.Context, lg Logger) context.Context

SetContextLogger attaches the provided logger to the context. If the context contains a valid OpenTelemetry span, the logger is wrapped with a SpanLogger that automatically records log events to the span. If logger is nil, a NoopLogger is used.

Types

type Config

type Config struct {
	Format string `env:"LOG_FORMAT" env-default:"console"` // console, logfmt or json
	Level  Level  `env:"LOG_LEVEL" env-default:"info"`     // debug, info, warn, error, fatal, trace
	Output string `env:"LOG_OUTPUT" env-default:"stderr"`  // stderr, stdout or file path
}

Config is used to configure the ZapLogger. It supports environment variable configuration with default values.

type Level

type Level string

Level represents the severity level of a log message. It can be used to filter log output based on importance.

const (
	// LevelDebug is the most verbose level, used for debugging purposes.
	LevelDebug Level = "debug"
	// LevelInfo is used for informational messages.
	LevelInfo Level = "info"
	// LevelWarn is used for warning messages that indicate potential issues.
	LevelWarn Level = "warn"
	// LevelError is used for error messages that indicate something went wrong.
	LevelError Level = "error"
	// LevelFatal is used for fatal errors that typically cause the program to exit.
	LevelFatal Level = "fatal"
)

type Logger

type Logger interface {
	// Debug logs a message for low-level debugging.
	// Use for detailed information useful during development.
	// keysAndValues lets you add structured context (e.g., "user", id).
	Debug(msg string, keysAndValues ...any)
	// Info logs general information about application progress.
	// Use for routine events or state changes.
	// keysAndValues lets you add structured context (e.g., "module", name).
	Info(msg string, keysAndValues ...any)
	// Warn logs a message for unexpected situations that aren't errors.
	// Use when something might be wrong but the app can continue.
	// keysAndValues lets you add structured context (e.g., "attempt", n).
	Warn(msg string, keysAndValues ...any)
	// Error logs an error that prevents normal operation.
	// Use for failures or problems that need attention.
	// keysAndValues lets you add structured context (e.g., "error", err).
	Error(msg string, keysAndValues ...any)
	// Fatal logs a critical error and may terminate the program.
	// Use for unrecoverable failures.
	// keysAndValues lets you add structured context (e.g., "reason", reason).
	Fatal(msg string, keysAndValues ...any)
	// WithKV returns a logger with an extra key-value pair for all future logs.
	// Use to add persistent context (e.g., component, request ID).
	WithKV(key string, value any) Logger
	// GetAllKV returns all persistent key-value pairs for this logger.
	// Use to inspect logger context.
	GetAllKV() []any
	// WithName returns a logger with a specific name (e.g., module or component).
	// Use to identify the source of logs.
	WithName(name string) Logger
	// Name returns the logger's name.
	Name() string
	// AddCallerSkip returns a logger that skips extra stack frames when reporting log source.
	// Use when wrapping the logger in helpers; returns itself if unsupported.
	AddCallerSkip(skip int) Logger
}

Logger is a logger interface.

func FromContext

func FromContext(ctx context.Context) Logger

FromContext retrieves the logger stored in the context. If no logger is found in the context, it returns a NoopLogger as a safe default.

func NewNoopLogger

func NewNoopLogger() Logger

NewNoopLogger creates a new NoopLogger instance. All logging operations on the returned logger will be silently discarded.

func NewSpanLogger

func NewSpanLogger(lg Logger, ser SpanEventRecorder) Logger

NewSpanLogger creates a new SpanLogger that wraps the provided logger and records events to the given SpanEventRecorder. The wrapped logger's caller skip is incremented by 1 to account for the SpanLogger wrapper.

func NewZapLogger

func NewZapLogger(conf Config, extraWriters ...zapcore.WriteSyncer) Logger

NewZapLogger creates a new ZapLogger with the given configuration. It supports multiple output formats (console, logfmt, json) and destinations (stderr, stdout, file). Additional write syncers can be provided to write logs to multiple destinations.

type NoopLogger

type NoopLogger struct{}

NoopLogger is a logger implementation that discards all log messages. It implements the Logger interface but performs no actual logging operations. This is useful for testing or when logging needs to be disabled.

func (NoopLogger) AddCallerSkip

func (n NoopLogger) AddCallerSkip(skip int) Logger

AddCallerSkip implements Logger.AddCallerSkip but returns the same NoopLogger instance.

func (NoopLogger) Debug

func (n NoopLogger) Debug(msg string, keysAndValues ...any)

Debug implements Logger.Debug but performs no operation.

func (NoopLogger) Error

func (n NoopLogger) Error(msg string, keysAndValues ...any)

Error implements Logger.Error but performs no operation.

func (NoopLogger) Fatal

func (n NoopLogger) Fatal(msg string, keysAndValues ...any)

Fatal implements Logger.Fatal but performs no operation.

func (NoopLogger) GetAllKV

func (n NoopLogger) GetAllKV() []any

GetAllKV implements Logger.GetAllKV and returns an empty slice.

func (NoopLogger) Info

func (n NoopLogger) Info(msg string, keysAndValues ...any)

Info implements Logger.Info but performs no operation.

func (NoopLogger) Name

func (n NoopLogger) Name() string

Name implements Logger.Name and always returns "noop".

func (NoopLogger) Warn

func (n NoopLogger) Warn(msg string, keysAndValues ...any)

Warn implements Logger.Warn but performs no operation.

func (NoopLogger) WithKV

func (n NoopLogger) WithKV(key string, value any) Logger

WithKV implements Logger.WithKV but returns the same NoopLogger instance.

func (NoopLogger) WithName

func (n NoopLogger) WithName(name string) Logger

WithName implements Logger.WithName but returns the same NoopLogger instance.

type OtelSpanEventRecorder

type OtelSpanEventRecorder struct {
	// contains filtered or unexported fields
}

OtelSpanEventRecorder is a SpanEventRecorder implementation that records events to an OpenTelemetry span. It converts log messages and their associated key-value pairs into span events and attributes.

func NewOtelSpanEventRecorder

func NewOtelSpanEventRecorder(span trace.Span) *OtelSpanEventRecorder

NewOtelSpanEventRecorder creates a new OtelSpanEventRecorder that will record events to the provided OpenTelemetry span.

func (*OtelSpanEventRecorder) RecordError

func (ser *OtelSpanEventRecorder) RecordError(name string, keysAndValues ...any)

RecordError records an error event to the span with the given name and attributes. It also sets the span status to error.

func (*OtelSpanEventRecorder) RecordEvent

func (ser *OtelSpanEventRecorder) RecordEvent(name string, keysAndValues ...any)

RecordEvent records an event to the span with the given name and attributes. The keysAndValues are converted to OpenTelemetry attributes.

func (*OtelSpanEventRecorder) SpanID

func (ser *OtelSpanEventRecorder) SpanID() string

SpanID returns the span ID of the span as a string.

func (*OtelSpanEventRecorder) TraceID

func (ser *OtelSpanEventRecorder) TraceID() string

TraceID returns the trace ID of the span as a string.

type SpanEventRecorder

type SpanEventRecorder interface {
	// TraceID returns the trace ID of the span.
	TraceID() string
	// SpanID returns the span ID of the span.
	SpanID() string

	// RecordEvent records an event to the span.
	// keysAndValues are treated as key-value pairs (e.g., "key1", value1, "key2", value2).
	RecordEvent(name string, keysAndValues ...any)
	// RecordError records an error to the span.
	// keysAndValues are treated as key-value pairs (e.g., "key1", value1, "key2", value2).
	RecordError(name string, keysAndValues ...any)
}

SpanEventRecorder is an interface for recording events and errors to a span.

type SpanLogger

type SpanLogger struct {
	// contains filtered or unexported fields
}

SpanLogger is a logger that wraps another logger and additionally records log events to a span using a SpanEventRecorder. This allows log messages to be correlated with distributed traces.

func (SpanLogger) AddCallerSkip

func (sl SpanLogger) AddCallerSkip(skip int) Logger

AddCallerSkip returns a new SpanLogger with increased caller skip on the wrapped logger.

func (SpanLogger) Debug

func (sl SpanLogger) Debug(msg string, keysAndValues ...any)

Debug logs a debug message to both the wrapped logger and the span.

func (SpanLogger) Error

func (sl SpanLogger) Error(msg string, keysAndValues ...any)

Error logs an error message to both the wrapped logger and the span. The error is recorded as an error event in the span.

func (SpanLogger) Fatal

func (sl SpanLogger) Fatal(msg string, keysAndValues ...any)

Fatal logs a fatal message to both the wrapped logger and the span. The error is recorded as an error event in the span.

func (SpanLogger) GetAllKV

func (sl SpanLogger) GetAllKV() []any

GetAllKV returns all key-value pairs from the wrapped logger.

func (SpanLogger) Info

func (sl SpanLogger) Info(msg string, keysAndValues ...any)

Info logs an info message to both the wrapped logger and the span.

func (SpanLogger) Name

func (sl SpanLogger) Name() string

Name returns the name of the wrapped logger.

func (SpanLogger) Warn

func (sl SpanLogger) Warn(msg string, keysAndValues ...any)

Warn logs a warning message to both the wrapped logger and the span.

func (SpanLogger) WithKV

func (sl SpanLogger) WithKV(key string, value any) Logger

WithKV returns a new SpanLogger with the key-value pair added to the wrapped logger. The SpanEventRecorder remains the same.

func (SpanLogger) WithName

func (sl SpanLogger) WithName(name string) Logger

WithName returns a new SpanLogger with the given name set on the wrapped logger. The SpanEventRecorder remains the same.

type ZapLogger

type ZapLogger struct {
	// contains filtered or unexported fields
}

ZapLogger is a logger implementation backed by Uber's zap logger. It provides structured logging with high performance and supports various output formats and destinations.

func (*ZapLogger) AddCallerSkip

func (l *ZapLogger) AddCallerSkip(skip int) Logger

AddCallerSkip returns a new ZapLogger that skips additional stack frames when determining the caller.

func (*ZapLogger) Debug

func (l *ZapLogger) Debug(msg string, keysAndValues ...any)

Debug logs a message at debug level.

func (*ZapLogger) Error

func (l *ZapLogger) Error(msg string, keysAndValues ...any)

Error logs a message at error level.

func (*ZapLogger) Fatal

func (l *ZapLogger) Fatal(msg string, keysAndValues ...any)

Fatal logs a message at fatal level.

func (*ZapLogger) GetAllKV

func (l *ZapLogger) GetAllKV() []any

GetAllKV returns all key-value pairs that have been added to this logger instance.

func (*ZapLogger) Info

func (l *ZapLogger) Info(msg string, keysAndValues ...any)

Info logs a message at info level.

func (*ZapLogger) Name

func (l *ZapLogger) Name() string

Name returns the current name of the logger.

func (*ZapLogger) Warn

func (l *ZapLogger) Warn(msg string, keysAndValues ...any)

Warn logs a message at warn level.

func (*ZapLogger) WithKV

func (l *ZapLogger) WithKV(key string, value any) Logger

WithKV returns a new ZapLogger with the key-value pair added to all future log messages.

func (*ZapLogger) WithName

func (l *ZapLogger) WithName(name string) Logger

WithName returns a new ZapLogger with the given name. The name is added to the logger hierarchy separated by dots.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL