This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Logging Package

API reference for rivaas.dev/logging - Structured logging for Go applications

This is the API reference for the rivaas.dev/logging package. For learning-focused documentation, see the Logging Guide.

Package Information

Package Overview

The logging package provides structured logging for Rivaas applications using Go’s standard log/slog package, with additional features for production environments.

Core Features

  • Multiple output formats (JSON, Text, Console)
  • Context-aware logging with OpenTelemetry trace correlation
  • Automatic sensitive data redaction
  • Log sampling for high-traffic scenarios
  • Dynamic log level changes at runtime
  • Convenience methods for common patterns
  • Comprehensive testing utilities
  • Zero external dependencies (except OpenTelemetry for tracing)

Architecture

The package is organized around key components:

Main Types

Logger - Main logging type with structured logging methods

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

ContextLogger - Context-aware logger with automatic trace correlation

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

Option - Functional option for logger configuration

type Option func(*Logger)

Quick API Index

Logger Creation

logger, err := logging.New(options...)     // With error handling
logger := logging.MustNew(options...)      // Panics on error

Logging Methods

logger.Debug(msg string, args ...any)
logger.Info(msg string, args ...any)
logger.Warn(msg string, args ...any)
logger.Error(msg string, args ...any)

Convenience Methods

logger.LogRequest(r *http.Request, extra ...any)
logger.LogError(err error, msg string, extra ...any)
logger.LogDuration(msg string, start time.Time, extra ...any)
logger.ErrorWithStack(msg string, err error, includeStack bool, extra ...any)

Context-Aware Logging

cl := logging.NewContextLogger(ctx context.Context, logger *Logger)
cl.Info(msg string, args ...any)  // Includes trace_id and span_id

Configuration Methods

logger.SetLevel(level Level) error
logger.Level() Level
logger.Shutdown(ctx context.Context) error

Reference Pages

API Reference

Logger and ContextLogger types with all methods.

View →

Options

Configuration options for handlers and output.

View →

Testing Utilities

Test helpers and mocking utilities.

View →

Troubleshooting

Common logging issues and solutions.

View →

User Guide

Step-by-step tutorials and examples.

View →

Type Reference

Logger

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

Main logging type. Thread-safe for concurrent access.

Creation:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithLevel(logging.LevelInfo),
)

Key Methods:

  • Debug, Info, Warn, Error - Logging at different levels
  • LogRequest, LogError, LogDuration - Convenience methods
  • SetLevel, Level - Dynamic level management
  • Shutdown - Graceful shutdown

ContextLogger

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

Context-aware logger with automatic trace correlation.

Creation:

cl := logging.NewContextLogger(ctx, logger)

Key Methods:

  • Debug, Info, Warn, Error - Logging with trace correlation
  • TraceID, SpanID - Access trace information
  • Logger - Get underlying slog.Logger

HandlerType

type HandlerType string

const (
    JSONHandler    HandlerType = "json"
    TextHandler    HandlerType = "text"
    ConsoleHandler HandlerType = "console"
)

Output format type.

Level

type Level = slog.Level

const (
    LevelDebug = slog.LevelDebug  // -4
    LevelInfo  = slog.LevelInfo   // 0
    LevelWarn  = slog.LevelWarn   // 4
    LevelError = slog.LevelError  // 8
)

Log level constants.

SamplingConfig

type SamplingConfig struct {
    Initial    int           // Log first N entries unconditionally
    Thereafter int           // After Initial, log 1 of every M entries
    Tick       time.Duration // Reset sampling counter every interval
}

Configuration for log sampling.

Error Types

The package defines sentinel errors for better error handling:

var (
    ErrNilLogger         = errors.New("custom logger is nil")
    ErrInvalidHandler    = errors.New("invalid handler type")
    ErrLoggerShutdown    = errors.New("logger is shut down")
    ErrInvalidLevel      = errors.New("invalid log level")
    ErrCannotChangeLevel = errors.New("cannot change level on custom logger")
)

Usage:

if err := logger.SetLevel(level); err != nil {
    if errors.Is(err, logging.ErrCannotChangeLevel) {
        // Handle immutable logger case
    }
}

Common Patterns

Basic Usage

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithLevel(logging.LevelInfo),
)
defer logger.Shutdown(context.Background())

logger.Info("operation completed", "items", 100)

With Service Metadata

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithServiceName("payment-api"),
    logging.WithServiceVersion("v2.1.0"),
    logging.WithEnvironment("production"),
)

With Context and Tracing

cl := logging.NewContextLogger(ctx, logger)
cl.Info("processing request", "user_id", userID)
// Automatically includes trace_id and span_id

With Sampling

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithSampling(logging.SamplingConfig{
        Initial:    1000,
        Thereafter: 100,
        Tick:       time.Minute,
    }),
)

Thread Safety

The Logger type is thread-safe for:

  • Concurrent logging operations
  • Concurrent SetLevel calls (serialized internally)
  • Mixed logging and configuration operations

Not thread-safe for:

  • Concurrent modification during initialization (use synchronization)

Performance Notes

  • Logging overhead: ~500ns per log entry
  • Level checks: ~5ns per check
  • Sampling overhead: ~20ns per log entry
  • Zero allocations: Standard log calls with inline fields
  • Stack traces: ~150µs capture cost (only when requested)

Version Compatibility

The logging package follows semantic versioning. The API is stable for the v1 series.

Minimum Go version: 1.25

Next Steps

For learning-focused guides, see the Logging Guide.

1 - API Reference

Complete API reference for all types and methods in the logging package

Complete API reference for all public types and methods in the logging package.

Core Functions

New

func New(opts ...Option) (*Logger, error)

Creates a new Logger with the given options. Returns an error if configuration is invalid.

Parameters:

  • opts - Variadic list of configuration options.

Returns:

  • *Logger - Configured logger instance.
  • error - Configuration error, if any.

Example:

logger, err := logging.New(
    logging.WithJSONHandler(),
    logging.WithLevel(logging.LevelInfo),
)
if err != nil {
    log.Fatalf("failed to create logger: %v", err)
}

MustNew

func MustNew(opts ...Option) *Logger

Creates a new Logger or panics on error. Use for initialization where errors are fatal.

Parameters:

  • opts - Variadic list of configuration options

Returns:

  • *Logger - Configured logger instance

Panics:

  • If configuration is invalid

Example:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithLevel(logging.LevelInfo),
)

NewContextLogger

func NewContextLogger(ctx context.Context, logger *Logger) *ContextLogger

Creates a context-aware logger that automatically extracts trace and span IDs from OpenTelemetry context.

Parameters:

  • ctx - Context to extract trace information from.
  • logger - Base Logger instance.

Returns:

  • *ContextLogger - Context-aware logger.

Example:

cl := logging.NewContextLogger(ctx, logger)
cl.Info("processing request")  // Includes trace_id and span_id

Logger Type

Logging Methods

Debug

func (l *Logger) Debug(msg string, args ...any)

Logs a debug message with structured attributes.

Parameters:

  • msg - Log message
  • args - Key-value pairs (must be even number of arguments)

Example:

logger.Debug("cache lookup", "key", cacheKey, "hit", true)

Info

func (l *Logger) Info(msg string, args ...any)

Logs an informational message with structured attributes.

Parameters:

  • msg - Log message
  • args - Key-value pairs

Example:

logger.Info("server started", "port", 8080, "version", "v1.0.0")

Warn

func (l *Logger) Warn(msg string, args ...any)

Logs a warning message with structured attributes.

Parameters:

  • msg - Log message
  • args - Key-value pairs

Example:

logger.Warn("high memory usage", "used_mb", 8192, "threshold_mb", 10240)

Error

func (l *Logger) Error(msg string, args ...any)

Logs an error message with structured attributes. Errors bypass log sampling.

Parameters:

  • msg - Log message
  • args - Key-value pairs

Example:

logger.Error("database connection failed", "error", err, "retry_count", 3)

Convenience Methods

LogRequest

func (l *Logger) LogRequest(r *http.Request, extra ...any)

Logs an HTTP request with standard fields (method, path, remote, user_agent, query).

Parameters:

  • r - HTTP request
  • extra - Additional key-value pairs

Example:

logger.LogRequest(r, "status", 200, "duration_ms", 45)

LogError

func (l *Logger) LogError(err error, msg string, extra ...any)

Logs an error with automatic error field.

Parameters:

  • err - Error to log
  • msg - Log message
  • extra - Additional key-value pairs

Example:

logger.LogError(err, "operation failed", "operation", "INSERT", "table", "users")

LogDuration

func (l *Logger) LogDuration(msg string, start time.Time, extra ...any)

Logs operation duration with automatic duration_ms and duration fields.

Parameters:

  • msg - Log message
  • start - Operation start time
  • extra - Additional key-value pairs

Example:

start := time.Now()
// ... operation ...
logger.LogDuration("processing completed", start, "items", 100)

ErrorWithStack

func (l *Logger) ErrorWithStack(msg string, err error, includeStack bool, extra ...any)

Logs an error with optional stack trace.

Parameters:

  • msg - Log message
  • err - Error to log
  • includeStack - Whether to capture and include stack trace
  • extra - Additional key-value pairs

Example:

logger.ErrorWithStack("critical failure", err, true, "user_id", userID)

Context Methods

Logger

func (l *Logger) Logger() *slog.Logger

Returns the underlying slog.Logger for advanced usage.

Returns:

  • *slog.Logger - Underlying logger

Example:

slogger := logger.Logger()

With

func (l *Logger) With(args ...any) *slog.Logger

Returns a slog.Logger with additional attributes that persist across log calls.

Parameters:

  • args - Key-value pairs to add as persistent attributes

Returns:

  • *slog.Logger - Logger with added attributes

Example:

requestLogger := logger.With("request_id", "req-123", "user_id", "user-456")
requestLogger.Info("processing")  // Includes request_id and user_id

WithGroup

func (l *Logger) WithGroup(name string) *slog.Logger

Returns a slog.Logger with a group name for nested attributes.

Parameters:

  • name - Group name

Returns:

  • *slog.Logger - Logger with group

Example:

dbLogger := logger.WithGroup("database")
dbLogger.Info("query", "sql", "SELECT * FROM users")
// Output: {...,"database":{"sql":"SELECT * FROM users"}}

Configuration Methods

SetLevel

func (l *Logger) SetLevel(level Level) error

Dynamically changes the minimum log level at runtime.

Parameters:

  • level - New log level

Returns:

  • error - ErrCannotChangeLevel if using custom logger

Example:

if err := logger.SetLevel(logging.LevelDebug); err != nil {
    log.Printf("failed to change level: %v", err)
}

Level

func (l *Logger) Level() Level

Returns the current minimum log level.

Returns:

  • Level - Current log level

Example:

currentLevel := logger.Level()
fmt.Printf("Current level: %s\n", currentLevel)

Metadata Methods

ServiceName

func (l *Logger) ServiceName() string

Returns the configured service name.

Returns:

  • string - Service name, or empty if not configured

ServiceVersion

func (l *Logger) ServiceVersion() string

Returns the configured service version.

Returns:

  • string - Service version, or empty if not configured

Environment

func (l *Logger) Environment() string

Returns the configured environment.

Returns:

  • string - Environment, or empty if not configured

Lifecycle Methods

IsEnabled

func (l *Logger) IsEnabled() bool

Returns true if logging is enabled (not shut down).

Returns:

  • bool - Whether logger is active

DebugInfo

func (l *Logger) DebugInfo() map[string]any

Returns diagnostic information about logger state.

Returns:

  • map[string]any - Diagnostic information

Example:

info := logger.DebugInfo()
fmt.Printf("Handler: %s\n", info["handler_type"])
fmt.Printf("Level: %s\n", info["level"])

Shutdown

func (l *Logger) Shutdown(ctx context.Context) error

Gracefully shuts down the logger, flushing any buffered logs.

Parameters:

  • ctx - Context for timeout control

Returns:

  • error - Shutdown error, if any

Example:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := logger.Shutdown(ctx); err != nil {
    fmt.Fprintf(os.Stderr, "shutdown error: %v\n", err)
}

ContextLogger Type

Logging Methods

Debug

func (cl *ContextLogger) Debug(msg string, args ...any)

Logs a debug message with automatic trace correlation.

Info

func (cl *ContextLogger) Info(msg string, args ...any)

Logs an info message with automatic trace correlation.

Warn

func (cl *ContextLogger) Warn(msg string, args ...any)

Logs a warning message with automatic trace correlation.

Error

func (cl *ContextLogger) Error(msg string, args ...any)

Logs an error message with automatic trace correlation.

Context Methods

Logger

func (cl *ContextLogger) Logger() *slog.Logger

Returns the underlying slog.Logger.

Returns:

  • *slog.Logger - Underlying logger

TraceID

func (cl *ContextLogger) TraceID() string

Returns the trace ID if available.

Returns:

  • string - Trace ID, or empty if not available

Example:

if traceID := cl.TraceID(); traceID != "" {
    fmt.Printf("Trace ID: %s\n", traceID)
}

SpanID

func (cl *ContextLogger) SpanID() string

Returns the span ID if available.

Returns:

  • string - Span ID, or empty if not available

With

func (cl *ContextLogger) With(args ...any) *slog.Logger

Returns a slog.Logger with additional attributes.

Parameters:

  • args - Key-value pairs

Returns:

  • *slog.Logger - Logger with added attributes

Next Steps

For usage guides, see the Logging Guide.

2 - Options Reference

Complete reference for all logger configuration options

Complete reference for all configuration options available in the logging package.

Handler Options

Configure the output format for logs.

WithHandlerType

func WithHandlerType(t HandlerType) Option

Sets the logging handler type directly.

Parameters:

  • t - Handler type. Use JSONHandler, TextHandler, or ConsoleHandler.

Example:

logging.WithHandlerType(logging.JSONHandler)

WithJSONHandler

func WithJSONHandler() Option

Uses JSON structured logging. This is the default. Best for production and log aggregation.

Example:

logger := logging.MustNew(logging.WithJSONHandler())

Output format:

{"time":"2024-01-15T10:30:45.123Z","level":"INFO","msg":"test","key":"value"}

WithTextHandler

func WithTextHandler() Option

Uses text key=value logging. Good for systems that prefer this format.

Example:

logger := logging.MustNew(logging.WithTextHandler())

Output format:

time=2024-01-15T10:30:45.123Z level=INFO msg=test key=value

WithConsoleHandler

func WithConsoleHandler() Option

Uses human-readable console logging with colors. Best for development.

Example:

logger := logging.MustNew(logging.WithConsoleHandler())

Output format:

10:30:45.123 INFO  test key=value

Level Options

Configure the minimum log level.

WithLevel

func WithLevel(level Level) Option

Sets the minimum log level.

Parameters:

  • level - Minimum level (LevelDebug, LevelInfo, LevelWarn, LevelError)

Example:

logger := logging.MustNew(
    logging.WithLevel(logging.LevelInfo),
)

WithDebugLevel

func WithDebugLevel() Option

Convenience function to enable debug logging. Equivalent to WithLevel(LevelDebug).

Example:

logger := logging.MustNew(logging.WithDebugLevel())

Output Options

Configure where logs are written.

WithOutput

func WithOutput(w io.Writer) Option

Sets the output destination for logs.

Parameters:

  • w - io.Writer to write logs to

Default: os.Stdout

Example:

logFile, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
logger := logging.MustNew(
    logging.WithOutput(logFile),
)

Multiple outputs:

logger := logging.MustNew(
    logging.WithOutput(io.MultiWriter(os.Stdout, logFile)),
)

Service Metadata Options

Configure service identification fields automatically added to every log entry.

WithServiceName

func WithServiceName(name string) Option

Sets the service name, automatically added to all log entries as service field.

Parameters:

  • name - Service name

Example:

logger := logging.MustNew(
    logging.WithServiceName("payment-api"),
)

WithServiceVersion

func WithServiceVersion(version string) Option

Sets the service version, automatically added to all log entries as version field.

Parameters:

  • version - Service version

Example:

logger := logging.MustNew(
    logging.WithServiceVersion("v2.1.0"),
)

WithEnvironment

func WithEnvironment(env string) Option

Sets the environment, automatically added to all log entries as env field.

Parameters:

  • env - Environment name

Example:

logger := logging.MustNew(
    logging.WithEnvironment("production"),
)

Combined example:

logger := logging.MustNew(
    logging.WithServiceName("payment-api"),
    logging.WithServiceVersion("v2.1.0"),
    logging.WithEnvironment("production"),
)
// All logs include: "service":"payment-api","version":"v2.1.0","env":"production"

Feature Options

Enable additional logging features.

WithSource

func WithSource(enabled bool) Option

Enables source code location (file and line number) in logs.

Parameters:

  • enabled - Whether to include source location

Default: false

Example:

logger := logging.MustNew(
    logging.WithSource(true),
)
// Output includes: "source":{"file":"main.go","line":42}

Note: Source location adds overhead. Use only for debugging.

WithDebugMode

func WithDebugMode(enabled bool) Option

Enables verbose debugging mode. Automatically enables debug level and source location.

Parameters:

  • enabled - Whether to enable debug mode

Example:

logger := logging.MustNew(
    logging.WithDebugMode(true),
)
// Equivalent to:
// WithDebugLevel() + WithSource(true)

WithGlobalLogger

func WithGlobalLogger() Option

Registers this logger as the global slog default logger. Allows third-party libraries using slog to use your configured logger.

Example:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithGlobalLogger(),
)
// Now slog.Info() uses this logger

Default: Not registered globally (allows multiple independent loggers)

WithSampling

func WithSampling(cfg SamplingConfig) Option

Enables log sampling to reduce volume in high-traffic scenarios.

Parameters:

  • cfg - Sampling configuration

Example:

logger := logging.MustNew(
    logging.WithSampling(logging.SamplingConfig{
        Initial:    1000,         // First 1000 logs
        Thereafter: 100,          // Then 1% sampling
        Tick:       time.Minute,  // Reset every minute
    }),
)

SamplingConfig fields:

  • Initial (int) - Log first N entries unconditionally
  • Thereafter (int) - After Initial, log 1 of every M entries (0 = log all)
  • Tick (time.Duration) - Reset counter every interval (0 = never reset)

Note: Errors (level >= ERROR) always bypass sampling.

Advanced Options

Advanced configuration for specialized use cases.

WithReplaceAttr

func WithReplaceAttr(fn func(groups []string, a slog.Attr) slog.Attr) Option

Sets a custom attribute replacer function for transforming or filtering log attributes.

Parameters:

  • fn - Function to transform attributes

Example - Custom redaction:

logger := logging.MustNew(
    logging.WithReplaceAttr(func(groups []string, a slog.Attr) slog.Attr {
        if a.Key == "credit_card" {
            return slog.String(a.Key, "***REDACTED***")
        }
        return a
    }),
)

Example - Dropping attributes:

logger := logging.MustNew(
    logging.WithReplaceAttr(func(groups []string, a slog.Attr) slog.Attr {
        if a.Key == "internal_field" {
            return slog.Attr{}  // Drop this field
        }
        return a
    }),
)

Example - Transforming values:

logger := logging.MustNew(
    logging.WithReplaceAttr(func(groups []string, a slog.Attr) slog.Attr {
        if a.Key == "time" {
            if t, ok := a.Value.Any().(time.Time); ok {
                return slog.String(a.Key, t.Format(time.RFC3339))
            }
        }
        return a
    }),
)

WithCustomLogger

func WithCustomLogger(customLogger *slog.Logger) Option

Uses a custom slog.Logger instead of creating one. For advanced use cases where you need full control over the logger.

Parameters:

  • customLogger - Pre-configured slog.Logger

Example:

customLogger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level:     slog.LevelDebug,
    AddSource: true,
}))

logger := logging.MustNew(
    logging.WithCustomLogger(customLogger),
)

Limitations:

  • Dynamic level changes (SetLevel) not supported
  • Service metadata must be added to custom logger directly

Configuration Examples

Development Configuration

logger := logging.MustNew(
    logging.WithConsoleHandler(),
    logging.WithDebugLevel(),
    logging.WithSource(true),
)

Production Configuration

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithLevel(logging.LevelInfo),
    logging.WithServiceName(os.Getenv("SERVICE_NAME")),
    logging.WithServiceVersion(os.Getenv("VERSION")),
    logging.WithEnvironment("production"),
    logging.WithSampling(logging.SamplingConfig{
        Initial:    1000,
        Thereafter: 100,
        Tick:       time.Minute,
    }),
)

Testing Configuration

buf := &bytes.Buffer{}
logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithOutput(buf),
    logging.WithLevel(logging.LevelDebug),
)

File Logging Configuration

logFile, _ := os.OpenFile("app.log",
    os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithOutput(logFile),
    logging.WithServiceName("myapp"),
)

Multiple Output Configuration

logFile, _ := os.OpenFile("app.log",
    os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithOutput(io.MultiWriter(os.Stdout, logFile)),
)

Next Steps

For usage guides, see the Configuration Guide.

3 - Testing Utilities

Complete reference for logging test utilities and helpers

Complete reference for testing utilities provided by the logging package.

Test Logger Creation

NewTestLogger

func NewTestLogger() (*Logger, *bytes.Buffer)

Creates a Logger for testing with an in-memory buffer. The logger is configured with JSON handler, debug level, and writes to the returned buffer.

Returns:

  • *Logger - Configured test logger
  • *bytes.Buffer - Buffer containing log output

Example:

func TestMyFunction(t *testing.T) {
    logger, buf := logging.NewTestLogger()
    
    myFunction(logger)
    
    entries, err := logging.ParseJSONLogEntries(buf)
    require.NoError(t, err)
    assert.Len(t, entries, 1)
}

ParseJSONLogEntries

func ParseJSONLogEntries(buf *bytes.Buffer) ([]LogEntry, error)

Parses JSON log entries from buffer into LogEntry slices. Creates a copy of the buffer so the original is not consumed.

Parameters:

  • buf - Buffer containing JSON log entries (one per line)

Returns:

  • []LogEntry - Parsed log entries
  • error - Parse error, if any

Example:

entries, err := logging.ParseJSONLogEntries(buf)
require.NoError(t, err)

for _, entry := range entries {
    fmt.Printf("%s: %s\n", entry.Level, entry.Message)
}

TestHelper

High-level testing utility with convenience methods.

NewTestHelper

func NewTestHelper(t *testing.T, opts ...Option) *TestHelper

Creates a TestHelper with in-memory logging and additional options.

Parameters:

  • t - Testing instance
  • opts - Optional configuration options

Returns:

  • *TestHelper - Test helper instance

Example:

func TestService(t *testing.T) {
    th := logging.NewTestHelper(t)
    
    svc := NewService(th.Logger)
    svc.DoSomething()
    
    th.AssertLog(t, "INFO", "operation completed", map[string]any{
        "status": "success",
    })
}

With custom configuration:

th := logging.NewTestHelper(t,
    logging.WithLevel(logging.LevelWarn),  // Only warnings and errors
)

TestHelper.Logs

func (th *TestHelper) Logs() ([]LogEntry, error)

Returns all parsed log entries.

Returns:

  • []LogEntry - All log entries
  • error - Parse error, if any

Example:

logs, err := th.Logs()
require.NoError(t, err)
assert.Len(t, logs, 3)

TestHelper.LastLog

func (th *TestHelper) LastLog() (*LogEntry, error)

Returns the most recent log entry.

Returns:

  • *LogEntry - Most recent log entry
  • error - Error if no logs or parse error

Example:

last, err := th.LastLog()
require.NoError(t, err)
assert.Equal(t, "INFO", last.Level)

TestHelper.ContainsLog

func (th *TestHelper) ContainsLog(msg string) bool

Checks if any log entry contains the given message.

Parameters:

  • msg - Message to search for

Returns:

  • bool - True if message found

Example:

if !th.ContainsLog("user created") {
    t.Error("expected user created log")
}

TestHelper.ContainsAttr

func (th *TestHelper) ContainsAttr(key string, value any) bool

Checks if any log entry contains the given attribute.

Parameters:

  • key - Attribute key
  • value - Attribute value

Returns:

  • bool - True if attribute found

Example:

if !th.ContainsAttr("user_id", "123") {
    t.Error("expected user_id attribute")
}

TestHelper.CountLevel

func (th *TestHelper) CountLevel(level string) int

Returns the number of log entries at the given level.

Parameters:

  • level - Log level (“DEBUG”, “INFO”, “WARN”, “ERROR”)

Returns:

  • int - Count of logs at that level

Example:

errorCount := th.CountLevel("ERROR")
assert.Equal(t, 2, errorCount)

TestHelper.Reset

func (th *TestHelper) Reset()

Clears the buffer for fresh testing.

Example:

th.Reset()  // Start fresh for next test phase

TestHelper.AssertLog

func (th *TestHelper) AssertLog(t *testing.T, level, msg string, attrs map[string]any)

Checks that a log entry exists with the given properties. Fails the test if not found.

Parameters:

  • t - Testing instance
  • level - Expected log level
  • msg - Expected message
  • attrs - Expected attributes

Example:

th.AssertLog(t, "INFO", "user created", map[string]any{
    "username": "alice",
    "email":    "alice@example.com",
})

LogEntry Type

type LogEntry struct {
    Time    time.Time
    Level   string
    Message string
    Attrs   map[string]any
}

Represents a parsed log entry for testing.

Fields:

  • Time - Log timestamp
  • Level - Log level (“DEBUG”, “INFO”, “WARN”, “ERROR”)
  • Message - Log message
  • Attrs - All other fields as map

Example:

entry := logs[0]
assert.Equal(t, "INFO", entry.Level)
assert.Equal(t, "test message", entry.Message)
assert.Equal(t, "value", entry.Attrs["key"])

Mock Writers

MockWriter

Records all writes for inspection.

Type:

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

Methods:

Write

func (mw *MockWriter) Write(p []byte) (n int, err error)

Implements io.Writer. Records the write.

WriteCount

func (mw *MockWriter) WriteCount() int

Returns the number of write calls.

BytesWritten

func (mw *MockWriter) BytesWritten() int

Returns total bytes written.

LastWrite

func (mw *MockWriter) LastWrite() []byte

Returns the most recent write.

Reset

func (mw *MockWriter) Reset()

Clears all recorded writes.

Example:

func TestWriteBehavior(t *testing.T) {
    mw := &logging.MockWriter{}
    logger := logging.MustNew(
        logging.WithJSONHandler(),
        logging.WithOutput(mw),
    )
    
    logger.Info("test 1")
    logger.Info("test 2")
    logger.Info("test 3")
    
    assert.Equal(t, 3, mw.WriteCount())
    assert.Contains(t, string(mw.LastWrite()), "test 3")
    assert.Greater(t, mw.BytesWritten(), 0)
}

CountingWriter

Counts bytes written without storing content.

Type:

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

Methods:

Write

func (cw *CountingWriter) Write(p []byte) (n int, err error)

Implements io.Writer. Counts bytes.

Count

func (cw *CountingWriter) Count() int64

Returns the total bytes written.

Example:

func TestLogVolume(t *testing.T) {
    cw := &logging.CountingWriter{}
    logger := logging.MustNew(
        logging.WithJSONHandler(),
        logging.WithOutput(cw),
    )
    
    for i := 0; i < 1000; i++ {
        logger.Info("test message", "index", i)
    }
    
    bytesLogged := cw.Count()
    t.Logf("Total bytes: %d", bytesLogged)
}

SlowWriter

Simulates slow I/O for testing timeouts.

Type:

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

Constructor:

NewSlowWriter

func NewSlowWriter(delay time.Duration, inner io.Writer) *SlowWriter

Creates a writer that delays each write.

Parameters:

  • delay - Delay duration for each write
  • inner - Optional inner writer to actually write to

Returns:

  • *SlowWriter - Slow writer instance

Example:

func TestSlowLogging(t *testing.T) {
    buf := &bytes.Buffer{}
    sw := logging.NewSlowWriter(100*time.Millisecond, buf)
    
    logger := logging.MustNew(
        logging.WithJSONHandler(),
        logging.WithOutput(sw),
    )
    
    start := time.Now()
    logger.Info("test")
    duration := time.Since(start)
    
    assert.GreaterOrEqual(t, duration, 100*time.Millisecond)
}

HandlerSpy

Implements slog.Handler and records all Handle calls.

Type:

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

Methods:

Enabled

func (hs *HandlerSpy) Enabled(_ context.Context, _ slog.Level) bool

Always returns true.

Handle

func (hs *HandlerSpy) Handle(_ context.Context, r slog.Record) error

Records the log record.

WithAttrs

func (hs *HandlerSpy) WithAttrs(_ []slog.Attr) slog.Handler

Returns the same handler (for compatibility).

WithGroup

func (hs *HandlerSpy) WithGroup(_ string) slog.Handler

Returns the same handler (for compatibility).

Records

func (hs *HandlerSpy) Records() []slog.Record

Returns all captured records.

RecordCount

func (hs *HandlerSpy) RecordCount() int

Returns the number of captured records.

Reset

func (hs *HandlerSpy) Reset()

Clears all captured records.

Example:

func TestHandlerBehavior(t *testing.T) {
    spy := &logging.HandlerSpy{}
    logger := slog.New(spy)
    
    logger.Info("test", "key", "value")
    
    assert.Equal(t, 1, spy.RecordCount())
    
    records := spy.Records()
    assert.Equal(t, "test", records[0].Message)
}

Testing Patterns

Testing Error Logging

func TestErrorHandling(t *testing.T) {
    th := logging.NewTestHelper(t)
    
    svc := NewService(th.Logger)
    err := svc.DoSomethingThatFails()
    
    require.Error(t, err)
    th.AssertLog(t, "ERROR", "operation failed", map[string]any{
        "error": "expected failure",
    })
}

Table-Driven Tests

func TestLogLevels(t *testing.T) {
    tests := []struct {
        name         string
        level        logging.Level
        expectLogged bool
    }{
        {"debug at info", logging.LevelInfo, false},
        {"info at info", logging.LevelInfo, true},
        {"error at warn", logging.LevelWarn, true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            th := logging.NewTestHelper(t,
                logging.WithLevel(tt.level),
            )
            
            th.Logger.Debug("test")
            
            logs, _ := th.Logs()
            if tt.expectLogged {
                assert.Len(t, logs, 1)
            } else {
                assert.Len(t, logs, 0)
            }
        })
    }
}

Next Steps

For complete testing patterns, see the Testing Guide.

4 - Troubleshooting

Common issues and solutions for the logging package

Common issues and solutions when using the logging package.

Logs Not Appearing

Debug Logs Not Showing

Problem: Debug logs don’t appear in output.

Cause: Log level is set higher than Debug.

Solution: Enable debug level:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithDebugLevel(),  // Enable debug logs
)

Or check current level:

currentLevel := logger.Level()
fmt.Printf("Current level: %s\n", currentLevel)

No Logs at All

Problem: No logs appear, even errors.

Possible causes:

  1. Logger shutdown: Check if logger was shut down.
if !logger.IsEnabled() {
    fmt.Println("Logger is shut down")
}
  1. Wrong output: Verify output destination.
logger := logging.MustNew(
    logging.WithOutput(os.Stdout),  // Not stderr
)
  1. Sampling too aggressive: Check sampling configuration.
info := logger.DebugInfo()
if sampling, ok := info["sampling"]; ok {
    fmt.Printf("Sampling: %+v\n", sampling)
}

Logs Disappear After Some Time

Problem: Logs stop appearing after initial burst.

Cause: Log sampling is dropping logs.

Solution: Adjust sampling or disable:

// Less aggressive sampling
logger := logging.MustNew(
    logging.WithSampling(logging.SamplingConfig{
        Initial:    1000,
        Thereafter: 10,  // 10% instead of 1%
        Tick:       time.Minute,
    }),
)

// Or disable sampling
logger := logging.MustNew(
    logging.WithJSONHandler(),
    // No WithSampling() call
)

Sensitive Data Issues

Sensitive Data Not Redacted

Problem: Custom sensitive fields not being redacted.

Cause: Only built-in fields are automatically redacted.

Solution: Add custom redaction:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithReplaceAttr(func(groups []string, a slog.Attr) slog.Attr {
        // Redact custom fields
        if a.Key == "credit_card" || a.Key == "ssn" {
            return slog.String(a.Key, "***REDACTED***")
        }
        return a
    }),
)

Built-in redacted fields:

  • password
  • token
  • secret
  • api_key
  • authorization

Too Much Redaction

Problem: Fields being redacted unnecessarily.

Cause: Field names match redaction patterns.

Solution: Rename fields to avoid keywords:

// Instead of "token" (redacted)
log.Info("processing", "request_token_id", tokenID)

// Instead of "secret" (redacted)
log.Info("config", "shared_secret_name", secretName)

Trace Correlation Issues

No Trace IDs in Logs

Problem: Logs don’t include trace_id and span_id.

Possible causes:

  1. Tracing not initialized:
// Initialize tracing
tracer := tracing.MustNew(
    tracing.WithOTLP("localhost:4317"),
)
defer tracer.Shutdown(context.Background())
  1. Not using ContextLogger:
// Wrong - plain logger
logger.Info("message")

// Right - context logger
cl := logging.NewContextLogger(ctx, logger)
cl.Info("message")  // Includes trace_id and span_id
  1. Context has no active span:
// Start a span
ctx, span := tracer.Start(context.Background(), "operation")
defer span.End()

cl := logging.NewContextLogger(ctx, logger)
cl.Info("message")  // Now includes trace IDs

Wrong Trace IDs

Problem: Trace IDs don’t match distributed trace.

Cause: Context not properly propagated.

Solution: Ensure context flows through call chain:

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()  // Get context with trace
    
    // Pass context down
    result := processRequest(ctx)
    
    w.Write(result)
}

func processRequest(ctx context.Context) []byte {
    // Use context
    cl := logging.NewContextLogger(ctx, logger)
    cl.Info("processing")
    
    return data
}

Performance Issues

High CPU Usage

Problem: Logging causes high CPU usage.

Possible causes:

  1. Logging in tight loops:
// Bad - logs thousands of times
for _, item := range items {
    logger.Debug("processing", "item", item)
}

// Good - log summary
logger.Info("processing batch", "count", len(items))
  1. Source location enabled in production:
// Bad for production
logger := logging.MustNew(
    logging.WithSource(true),  // Adds overhead
)

// Good for production
logger := logging.MustNew(
    logging.WithJSONHandler(),
    // No source location
)
  1. Debug level in production:
// Bad - debug logs have overhead even if filtered
logger := logging.MustNew(
    logging.WithDebugLevel(),
)

// Good - appropriate level
logger := logging.MustNew(
    logging.WithLevel(logging.LevelInfo),
)

High Memory Usage

Problem: Memory usage grows over time.

Possible causes:

  1. No log rotation: Logs written to file without rotation.

Solution: Use external log rotation (logrotate) or rotate in code:

// Use external tool like logrotate
// Or implement rotation
  1. Buffered output not flushed: Buffers growing without flush.

Solution: Ensure proper shutdown:

defer func() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    logger.Shutdown(ctx)
}()

Configuration Issues

Cannot Change Log Level

Problem: SetLevel returns error.

Cause: Using custom logger.

Error:

err := logger.SetLevel(logging.LevelDebug)
if errors.Is(err, logging.ErrCannotChangeLevel) {
    // Custom logger doesn't support dynamic level changes
}

Solution: Control level in custom logger:

var levelVar slog.LevelVar
levelVar.Set(slog.LevelInfo)

customLogger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: &levelVar,
}))

// Change level directly
levelVar.Set(slog.LevelDebug)

Service Metadata Not Appearing

Problem: Service name, version, or environment not in logs.

Cause: Not configured or using custom logger.

Solution: Configure service metadata:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithServiceName("my-api"),
    logging.WithServiceVersion("v1.0.0"),
    logging.WithEnvironment("production"),
)

For custom logger, add metadata manually:

customLogger := slog.New(handler).With(
    "service", "my-api",
    "version", "v1.0.0",
    "env", "production",
)

Router Integration Issues

Access Log Not Working

Problem: HTTP requests not being logged.

Possible causes:

  1. Logger not set on router:
r := router.MustNew()
logger := logging.MustNew(logging.WithJSONHandler())
r.SetLogger(logger)  // Must set logger
  1. Middleware not applied:
import "rivaas.dev/router/middleware/accesslog"

r.Use(accesslog.New())  // Apply middleware
  1. Path excluded:
r.Use(accesslog.New(
    accesslog.WithExcludePaths("/health", "/metrics"),
))
// /health and /metrics won't be logged

Context Logger Not Working

Problem: Router context logger has no trace IDs.

Cause: Tracing not initialized or middleware not applied.

Solution: Initialize tracing:

a, _ := app.New(
    app.WithServiceName("my-api"),
    app.WithObservability(
        app.WithLogging(logging.WithJSONHandler()),
        app.WithTracing(tracing.WithOTLP("localhost:4317")),
    ),
)

Testing Issues

Test Logs Not Captured

Problem: Logs not appearing in test buffer.

Cause: Using wrong logger instance.

Solution: Use TestHelper or ensure buffer is captured:

func TestMyFunction(t *testing.T) {
    th := logging.NewTestHelper(t)
    
    myFunction(th.Logger)  // Pass test logger
    
    logs, _ := th.Logs()
    assert.Len(t, logs, 1)
}

Parse Errors

Problem: ParseJSONLogEntries returns error.

Cause: Non-JSON output or malformed JSON.

Solution: Ensure JSON handler:

th := logging.NewTestHelper(t,
    logging.WithJSONHandler(),  // Must be JSON
)

Error Types

ErrNilLogger

var ErrNilLogger = errors.New("custom logger is nil")

When: Providing nil custom logger.

Solution:

if customLogger != nil {
    logger := logging.MustNew(
        logging.WithCustomLogger(customLogger),
    )
}

ErrInvalidHandler

var ErrInvalidHandler = errors.New("invalid handler type")

When: Invalid handler type specified.

Solution: Use valid handler types:

logging.WithHandlerType(logging.JSONHandler)
logging.WithHandlerType(logging.TextHandler)
logging.WithHandlerType(logging.ConsoleHandler)

ErrLoggerShutdown

var ErrLoggerShutdown = errors.New("logger is shut down")

When: Operations after shutdown.

Solution: Don’t use logger after shutdown:

defer logger.Shutdown(context.Background())
// Don't log after this point

ErrInvalidLevel

var ErrInvalidLevel = errors.New("invalid log level")

When: Invalid log level provided.

Solution: Use valid levels:

logging.LevelDebug
logging.LevelInfo
logging.LevelWarn
logging.LevelError

ErrCannotChangeLevel

var ErrCannotChangeLevel = errors.New("cannot change level on custom logger")

When: Calling SetLevel on custom logger.

Solution: Control level in custom logger directly or don’t use custom logger.

Getting Help

If you encounter issues not covered here:

  1. Check the API Reference for method details
  2. Review Examples for patterns
  3. See Best Practices for recommendations
  4. Check the GitHub issues

Debugging Tips

Enable Debug Info

info := logger.DebugInfo()
fmt.Printf("Logger state: %+v\n", info)

Check Sampling State

info := logger.DebugInfo()
if sampling, ok := info["sampling"]; ok {
    fmt.Printf("Sampling config: %+v\n", sampling)
}

Verify Configuration

fmt.Printf("Service: %s\n", logger.ServiceName())
fmt.Printf("Version: %s\n", logger.ServiceVersion())
fmt.Printf("Environment: %s\n", logger.Environment())
fmt.Printf("Level: %s\n", logger.Level())
fmt.Printf("Enabled: %v\n", logger.IsEnabled())

Next Steps