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 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
Options must not be nil; passing a nil option results in an error (or panic with MustNew).
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
Logger and ContextLogger types with all methods.
View →
Configuration options for handlers and output.
View →
Test helpers and mocking utilities.
View →
Common logging issues and solutions.
View →
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 levelsLogRequest, LogError, LogDuration - Convenience methodsSetLevel, Level - Dynamic level managementShutdown - 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 correlationTraceID, SpanID - Access trace informationLogger - 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.
Log sampling options
Configure log sampling with functional options (no config struct):
- WithSamplingInitial(initial int) — Log the first
initial entries unconditionally. Zero means no initial burst. Must be non-negative. - WithSamplingThereafter(thereafter int) — After the initial phase, log 1 in every
thereafter entries. Zero means log all. Must be non-negative. - WithSamplingTick(tick time.Duration) — Reset the sampling counter every
tick. Zero means never reset.
Sampling is enabled when any of these options is applied. Combine them as needed. Errors (level >= ERROR) always bypass 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)
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.WithSamplingInitial(1000),
logging.WithSamplingThereafter(100),
logging.WithSamplingTick(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)
- 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),
)
Automatic Trace Correlation
When you create a logger with this package (and optionally set it as the global logger with WithGlobalLogger()), trace correlation is automatic. You do not need a special logger type.
Any call to the standard library’s context-aware methods — slog.InfoContext(ctx, ...), slog.ErrorContext(ctx, ...), and so on — will automatically get trace_id and span_id added to the log record if the context contains an active OpenTelemetry span. The logging package wraps the handler with a context-aware layer that reads the span from the context and injects these fields.
Example (in an HTTP handler):
// Pass the request context when you log
slog.InfoContext(c.RequestContext(), "processing request", "order_id", orderID)
// Output includes trace_id and span_id when tracing is enabled
Use the same pattern with slog.DebugContext, slog.WarnContext, and slog.ErrorContext. No wrapper type or extra API is required.
Logger Type
Logging Methods
Debug
func (l *Logger) Debug(msg string, args ...any)
Logs a debug message with structured attributes.
Parameters:
msg - Log messageargs - 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 messageargs - 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 messageargs - 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 messageargs - 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 requestextra - 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 logmsg - Log messageextra - 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 messagestart - Operation start timeextra - 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 messageerr - Error to logincludeStack - Whether to capture and include stack traceextra - 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:
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:
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)
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)
}
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)),
)
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:
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:
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)
WithSamplingInitial
func WithSamplingInitial(initial int) Option
Sets how many log entries to emit unconditionally before sampling. Zero means no initial burst. Must be non-negative. Sampling is enabled when any of the sampling options is used.
WithSamplingThereafter
func WithSamplingThereafter(thereafter int) Option
Sets the sampling rate after the initial phase: log 1 in every N entries. Zero means log all after the initial phase. Must be non-negative.
WithSamplingTick
func WithSamplingTick(tick time.Duration) Option
Sets the interval at which the sampling counter is reset. Zero means never reset.
Example:
logger := logging.MustNew(
logging.WithJSONHandler(),
logging.WithSamplingInitial(1000), // First 1000 logs
logging.WithSamplingThereafter(100), // Then 1% sampling
logging.WithSamplingTick(time.Minute), // Reset every minute
)
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.WithSamplingInitial(1000),
logging.WithSamplingThereafter(100),
logging.WithSamplingTick(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 entrieserror - 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 instanceopts - 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 entrieserror - 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 entryerror - 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 keyvalue - 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 instancelevel - Expected log levelmsg - Expected messageattrs - 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 timestampLevel - Log level (“DEBUG”, “INFO”, “WARN”, “ERROR”)Message - Log messageAttrs - 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 writeinner - 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:
- Logger shutdown: Check if logger was shut down.
if !logger.IsEnabled() {
fmt.Println("Logger is shut down")
}
- Wrong output: Verify output destination.
logger := logging.MustNew(
logging.WithOutput(os.Stdout), // Not stderr
)
- 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.WithSamplingInitial(1000),
logging.WithSamplingThereafter(10), // 10% instead of 1%
logging.WithSamplingTick(time.Minute),
)
// Or disable sampling
logger := logging.MustNew(
logging.WithJSONHandler(),
// No sampling options (WithSamplingInitial / WithSamplingThereafter / WithSamplingTick)
)
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:
passwordtokensecretapi_keyauthorization
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:
- Tracing not initialized:
// Initialize tracing
tracer := tracing.MustNew(
tracing.WithOTLP("localhost:4317"),
)
defer tracer.Shutdown(context.Background())
- Not passing the request context when logging:
// Wrong - no context, so no trace_id/span_id
slog.Info("message")
// Right - pass context so trace_id and span_id are injected automatically
slog.InfoContext(ctx, "message")
- Context has no active span:
// Start a span so the context carries trace info
ctx, span := tracer.Start(context.Background(), "operation")
defer span.End()
slog.InfoContext(ctx, "message") // Now includes trace_id and span_id
Wrong Trace IDs
Problem: Trace IDs don’t match distributed trace.
Cause: Context not properly propagated.
Solution: Ensure context flows through the call chain and pass it when you log:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // Context carries trace from middleware
result := processRequest(ctx)
w.Write(result)
}
func processRequest(ctx context.Context) []byte {
slog.InfoContext(ctx, "processing") // Uses same context, so same trace
return data
}
High CPU Usage
Problem: Logging causes high CPU usage.
Possible causes:
- 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))
- 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
)
- 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:
- 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
- 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)
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:
- Logger not set on router:
r := router.MustNew()
logger := logging.MustNew(logging.WithJSONHandler())
r.SetLogger(logger) // Must set logger
- Middleware not applied:
import "rivaas.dev/router/middleware/accesslog"
r.Use(accesslog.New()) // Apply middleware
- Path excluded:
r.Use(accesslog.New(
accesslog.WithExcludePaths("/health", "/metrics"),
))
// /health and /metrics won't be logged
No Trace IDs in Handler Logs
Problem: Handler logs have no trace_id or span_id.
Cause: Tracing not initialized, or you are not using the context when logging.
Solution: Initialize tracing with the app, and always pass the request context when you log:
a, _ := app.New(
app.WithServiceName("my-api"),
app.WithObservability(
app.WithLogging(logging.WithJSONHandler()),
app.WithTracing(tracing.WithOTLP("localhost:4317")),
),
)
// In handlers, use: slog.InfoContext(c.RequestContext(), "message", ...)
Trace IDs are injected automatically for any slog.*Context(ctx, ...) call when the context has an active OpenTelemetry span.
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:
- Check the API Reference for method details
- Review Examples for patterns
- See Best Practices for recommendations
- 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