logging

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Dec 18, 2025 License: Apache-2.0 Imports: 19 Imported by: 6

README

logging

Structured logging for Rivaas using Go's standard log/slog package.

Features

  • Multiple Output Formats: JSON, text, and human-friendly console output
  • Context-Aware Logging: Automatic trace correlation with OpenTelemetry
  • Sensitive Data Redaction: Automatic sanitization of passwords, tokens, and secrets
  • Functional Options API: Clean, composable configuration
  • Router Integration: Seamless integration following metrics/tracing patterns
  • Zero External Dependencies: Uses only Go standard library (except OpenTelemetry for trace correlation)

Installation

go get rivaas.dev/logging

Dependencies

Dependency Purpose Required
Go stdlib (log/slog) Core logging Yes
go.opentelemetry.io/otel/trace Trace correlation in ContextLogger Optional*
github.com/stretchr/testify Test utilities Test only

* The OpenTelemetry trace dependency is only used by NewContextLogger() for automatic trace/span ID extraction. If you don't use context-aware logging with tracing, this dependency has no runtime impact.

Quick Start

Basic Usage
package main

import (
    "rivaas.dev/logging"
)

func main() {
    // Create a logger with console output
    log := logging.MustNew(
        logging.WithConsoleHandler(),
        logging.WithDebugLevel(),
    )

    log.Info("service started", "port", 8080, "env", "production")
    log.Debug("debugging information", "key", "value")
    log.Error("operation failed", "error", "connection timeout")
}
Output Formats
JSON Handler (Default)
log := logging.MustNew(
    logging.WithJSONHandler(),
)

log.Info("user action", "user_id", "123", "action", "login")
// Output: {"time":"2024-01-15T10:30:45.123Z","level":"INFO","msg":"user action","user_id":"123","action":"login"}
Text Handler
log := logging.MustNew(
    logging.WithTextHandler(),
)

log.Info("request processed", "method", "GET", "path", "/api/users")
// Output: time=2024-01-15T10:30:45.123Z level=INFO msg="request processed" method=GET path=/api/users
Console Handler
log := logging.MustNew(
    logging.WithConsoleHandler(),
)

log.Info("server starting", "port", 8080)
// Output: 10:30:45.123 INFO  server starting port=8080
// (with colors!)

Configuration Options

Handler Types
// JSON structured logging (default, best for production)
logging.WithJSONHandler()

// Text key=value logging
logging.WithTextHandler()

// Human-readable colored console (best for development)
logging.WithConsoleHandler()
Log Levels
// Set minimum log level
logging.WithLevel(logging.LevelDebug)
logging.WithLevel(logging.LevelInfo)
logging.WithLevel(logging.LevelWarn)
logging.WithLevel(logging.LevelError)

// Convenience function
logging.WithDebugLevel()
Output Destination
// Write to file
logFile, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
logging.WithOutput(logFile)

// Write to custom io.Writer
var buf bytes.Buffer
logging.WithOutput(&buf)
Service Information

When configured, service metadata is automatically added to every log entry:

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

logger.Info("server started", "port", 8080)
// Output: {"level":"INFO","msg":"server started","service":"my-api","version":"v1.0.0","env":"production","port":8080}
Source Code Location
// Add file:line information to logs
logging.WithSource(true)
// Output: ... (logging.go:123)
Custom Attribute Replacer
logging.WithReplaceAttr(func(groups []string, a slog.Attr) slog.Attr {
    // Custom logic for transforming attributes
    if a.Key == "sensitive_data" {
        return slog.String(a.Key, "***HIDDEN***")
    }
    return a
})
Global Logger Registration

By default, loggers are not registered globally, allowing multiple logger instances to coexist. Use WithGlobalLogger() to set your logger as the slog default:

// Register as the global slog default
logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithServiceName("my-api"),
    logging.WithGlobalLogger(), // Now slog.Info() uses this logger
)
defer logger.Shutdown(context.Background())

// These now use your configured logger
slog.Info("using global logger", "key", "value")

This is useful when:

  • You want third-party libraries using slog to use your configured logger
  • You prefer using slog.Info() directly instead of logger.Info()
  • You're migrating from direct slog usage to Rivaas logging

Sensitive Data Redaction

The logger automatically redacts sensitive fields:

log.Info("user login", 
    "username", "john",
    "password", "secret123",    // Will be redacted
    "token", "abc123",          // Will be redacted
    "api_key", "xyz789",        // Will be redacted
)

// Output: {"msg":"user login","username":"john","password":"***REDACTED***","token":"***REDACTED***","api_key":"***REDACTED***"}

Automatically redacted fields:

  • password
  • token
  • secret
  • api_key
  • authorization

Context-Aware Logging

With OpenTelemetry Tracing

When using with tracing, logs automatically include trace and span IDs:

import (
    "rivaas.dev/logging"
    "rivaas.dev/tracing"
)

// Create logger
log := logging.MustNew(logging.WithJSONHandler())

// In a traced request
func handler(ctx context.Context) {
    cl := logging.NewContextLogger(ctx, log)
    
    cl.Info("processing request", "user_id", "123")
    // Output includes: "trace_id":"abc123...", "span_id":"def456..."
}
Structured Context
// Add context fields that persist across log calls
contextLogger := log.With(
    "request_id", "req-123",
    "user_id", "user-456",
)

contextLogger.Info("validation started")
contextLogger.Info("validation completed")
// Both logs include request_id and user_id
Grouped Attributes
requestLogger := log.WithGroup("request")
requestLogger.Info("received", 
    "method", "POST",
    "path", "/api/users",
)
// Output: {"msg":"received","request":{"method":"POST","path":"/api/users"}}

Router Integration

The logging package integrates with the Rivaas router via the SetLogger method.

Basic Integration
import (
    "rivaas.dev/router"
    "rivaas.dev/logging"
)

func main() {
    // Create logger
    logger := logging.MustNew(
        logging.WithConsoleHandler(),
        logging.WithDebugLevel(),
    )
    
    // Create router and set logger
    r := router.MustNew()
    r.SetLogger(logger)
    
    r.GET("/", func(c *router.Context) {
        c.Logger().Info("handling request")
        c.JSON(200, map[string]string{"status": "ok"})
    })
    
    r.Run(":8080")
}
With Full Observability

For full observability (logging, metrics, tracing), use the app package which wires everything together:

import (
    "rivaas.dev/app"
    "rivaas.dev/logging"
    "rivaas.dev/tracing"
)

a, err := app.New(
    app.WithServiceName("my-api"),
    app.WithObservability(
        app.WithLogging(logging.WithJSONHandler()),
        app.WithMetrics(), // Prometheus is default
        app.WithTracing(tracing.WithOTLP("localhost:4317")),
    ),
)

Environment Variables

The logger respects standard OpenTelemetry environment variables:

# Service identification
export OTEL_SERVICE_NAME=my-api
export OTEL_SERVICE_VERSION=v1.0.0
export RIVAAS_ENVIRONMENT=production

Advanced Usage

Custom Logger

You can provide your own slog.Logger:

import "log/slog"

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

cfg := logging.MustNew(
    logging.WithCustomLogger(customLogger),
)
Multiple Loggers

Create different loggers for different purposes:

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

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

// Audit logger
auditFile, _ := os.OpenFile("audit.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
auditLog := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithOutput(auditFile),
)
Graceful Shutdown
log := logging.MustNew(logging.WithJSONHandler())

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

if err := log.Shutdown(ctx); err != nil {
    // Handle shutdown error
}

Best Practices

Use Structured Logging
// BAD - string concatenation
log.Info("User " + userID + " logged in from " + ipAddress)

// GOOD - structured fields
log.Info("user logged in",
    "user_id", userID,
    "ip_address", ipAddress,
    "session_id", sessionID,
)
Log Appropriate Levels
// DEBUG - detailed information for debugging
log.Debug("cache hit", "key", cacheKey, "ttl", ttl)

// INFO - general informational messages
log.Info("server started", "port", 8080)

// WARN - warning but not an error
log.Warn("high memory usage", "used_mb", 8192, "total_mb", 16384)

// ERROR - errors that need attention
log.Error("database connection failed", "error", err, "retry_count", retries)
Don't Log in Tight Loops
// BAD - logs thousands of times
for _, item := range items {
    log.Debug("processing", "item", item)
    process(item)
}

// GOOD - log once with summary
log.Info("processing batch", "count", len(items))
for _, item := range items {
    process(item)
}
log.Info("batch completed", "processed", len(items), "duration", elapsed)
Include Context
// Minimal context
log.Error("failed to save", "error", err)

// Better - includes relevant context
log.Error("failed to save user data",
    "error", err,
    "user_id", user.ID,
    "operation", "update_profile",
    "retry_count", retries,
)

Performance Considerations

The logging package is designed for high-performance production use with minimal overhead.

Optimization Tips
  1. Set appropriate log levels: Debug logging has overhead; use INFO+ in production
  2. Avoid logging in tight loops: Log batch summaries instead
  3. Use structured fields: More efficient than string concatenation
  4. Reuse loggers: Create once, use many times
Memory Usage
  • Logger creation: ~1KB per logger instance
  • No allocations: Zero allocs for standard log calls
  • Pooling: Consider sync.Pool for ContextLogger in extreme high-load scenarios
Performance Best Practices
// GOOD - Structured, efficient
logger.Info("user action",
    "user_id", userID,
    "action", "login",
    "duration_ms", elapsed.Milliseconds(),
)

// BAD - String concatenation, inefficient
logger.Info(fmt.Sprintf("User %s performed %s in %dms", userID, "login", elapsed.Milliseconds()))

// GOOD - Use router accesslog middleware for HTTP logging
// See router/middleware/accesslog for details

// GOOD - Appropriate log level for production
logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithLevel(logging.LevelInfo),  // Skip debug logs
)
Profiling

Run benchmarks to measure performance:

# Run all benchmarks
go test -bench=. -benchmem ./logging

# Benchmark specific handler
go test -bench=BenchmarkJSONHandler -benchmem

# CPU profiling
go test -bench=BenchmarkLogging -cpuprofile=cpu.prof
go tool pprof cpu.prof

# Memory profiling
go test -bench=BenchmarkConcurrentLogging -memprofile=mem.prof
go tool pprof mem.prof

Examples

See the examples directory for complete working examples:

  • Basic logging
  • Router integration
  • Context-aware logging
  • Custom handlers
  • Multiple output formats

Migration Guides

Switching from other popular Go logging libraries to Rivaas logging is straightforward.

From logrus

logrus is a popular structured logger, but Rivaas logging offers better performance and stdlib integration.

// BEFORE (logrus)
import "github.com/sirupsen/logrus"

log := logrus.New()
log.SetFormatter(&logrus.JSONFormatter{})
log.SetLevel(logrus.InfoLevel)
log.SetOutput(os.Stdout)

log.WithFields(logrus.Fields{
    "user_id": "123",
    "action": "login",
}).Info("User logged in")

// AFTER (rivaas/logging)
import "rivaas.dev/logging"

log := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithLevel(logging.LevelInfo),
    logging.WithOutput(os.Stdout),
)

log.Info("User logged in",
    "user_id", "123",
    "action", "login",
)

Key Differences:

  • Use logging.WithJSONHandler() instead of &logrus.JSONFormatter{}
  • Fields are inline, not in WithFields() map
  • logging.LevelInfo instead of logrus.InfoLevel
  • Faster performance, fewer allocations
From zap

zap is very fast, but Rivaas logging offers similar performance with simpler API.

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

logger, _ := zap.NewProduction()
defer logger.Sync()

logger.Info("User logged in",
    zap.String("user_id", "123"),
    zap.String("action", "login"),
    zap.Int("status", 200),
)

// AFTER (rivaas/logging)
import "rivaas.dev/logging"

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

logger.Info("User logged in",
    "user_id", "123",
    "action", "login",
    "status", 200,
)

Key Differences:

  • No need for zap.String() wrappers - pass values directly
  • Shutdown() instead of Sync()
  • Simpler API, similar performance
  • Standard library based
From zerolog

zerolog is very fast, but Rivaas logging is simpler and uses stdlib.

// BEFORE (zerolog)
import "github.com/rs/zerolog"

logger := zerolog.New(os.Stdout).With().
    Str("service", "myapp").
    Str("version", "1.0.0").
    Logger()

logger.Info().
    Str("user_id", "123").
    Str("action", "login").
    Msg("User logged in")

// AFTER (rivaas/logging)
import "rivaas.dev/logging"

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithServiceName("myapp"),
    logging.WithServiceVersion("1.0.0"),
    logging.WithEnvironment("production"),
)

logger.Info("User logged in",
    "user_id", "123",
    "action", "login",
)

Key Differences:

  • Simpler syntax - no chaining
  • WithServiceName(), WithServiceVersion(), and WithEnvironment() for service metadata
  • Fields passed directly, not via chainable methods
  • Better integration with OpenTelemetry
From stdlib log

Standard library log is simple but unstructured. Rivaas logging adds structure while using stdlib slog.

// BEFORE (stdlib log)
import "log"

log.SetOutput(os.Stdout)
log.SetPrefix("[INFO] ")
log.Printf("User %s logged in from %s", userID, ipAddress)

// AFTER (rivaas/logging)
import "rivaas.dev/logging"

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

logger.Info("User logged in",
    "user_id", userID,
    "ip_address", ipAddress,
)

Key Benefits:

  • Structured logging (machine parseable)
  • Log levels (Debug, Info, Warn, Error)
  • Automatic sensitive data redaction
  • OpenTelemetry integration
  • Multiple output formats
Migration Checklist

When migrating from another logger:

  • Replace logger initialization
  • Update all log calls to structured format (key-value pairs)
  • Replace log level constants (logrus.InfoLevellogging.LevelInfo)
  • Update context/field methods (WithFields() → inline fields)
  • Replace typed field methods (zap.String() → direct values)
  • Update error handling (Sync()Shutdown())
  • Test with new logger
  • Update imports
  • Remove old logger dependency

Troubleshooting

Common Issues
Logs Not Appearing
// Check log level - debug logs won't show at INFO level
logger := logging.MustNew(
    logging.WithDebugLevel(),  // Enable debug logs
)
Sensitive Data Not Redacted
// Custom fields need 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
    }),
)
No Trace IDs in Logs
// Ensure tracing is initialized and context is propagated
tracer := tracing.MustNew(tracing.WithOTLP("localhost:4317"))
ctx, span := tracer.Start(context.Background(), "operation")
defer span.End()

// Use context logger
cl := logging.NewContextLogger(logger, ctx)
cl.Info("traced message")  // Will include trace_id and span_id
Access Log Not Working
// Ensure accesslog middleware is applied and logger is configured
r := router.New()
logger := logging.MustNew(logging.WithJSONHandler())
r.SetLogger(logger)
r.Use(accesslog.New())
High Memory Usage
// Reduce log volume
logger := logging.MustNew(
    logging.WithLevel(logging.LevelWarn),  // Only warnings and errors
)

// Use router accesslog middleware with path exclusions
r.Use(accesslog.New(
    accesslog.WithExcludePaths("/health", "/metrics", "/ready"),
))
Debugging

Enable source location to see where logs originate:

logger := logging.MustNew(
    logging.WithSource(true),  // Adds file:line to logs
    logging.WithDebugLevel(),
)

// Output includes: ... "source":{"file":"main.go","line":42} ...
Getting Help

Testing

# Run all tests
go test ./logging

# Run with coverage
go test ./logging -cover

# Run with verbose output
go test ./logging -v

# Run with race detector
go test ./logging -race

# Run benchmarks
go test -bench=. -benchmem ./logging

API Reference

Core Types
  • Logger - Main logging type (created via New() or MustNew())
  • Option - Functional option type
  • HandlerType - Log output format type
  • Level - Log level type
  • ContextLogger - Context-aware logger with trace correlation
Main Functions
  • New(opts ...Option) (*Logger, error) - Create new logger
  • MustNew(opts ...Option) *Logger - Create new logger or panic
  • NewContextLogger(ctx context.Context, logger *Logger) *ContextLogger - Create context logger
Logger Methods
  • Logger() *slog.Logger - Get underlying slog logger
  • With(args ...any) *slog.Logger - Create logger with attributes
  • WithGroup(name string) *slog.Logger - Create logger with group
  • Debug(msg string, args ...any) - Log debug message
  • Info(msg string, args ...any) - Log info message
  • Warn(msg string, args ...any) - Log warning message
  • Error(msg string, args ...any) - Log error message
  • SetLevel(level Level) error - Change log level dynamically
  • Level() Level - Get current log level
  • Shutdown(ctx context.Context) error - Graceful shutdown
Convenience Methods
  • LogRequest(r *http.Request, extra ...any) - Log HTTP request with standard fields
  • LogError(err error, msg string, extra ...any) - Log error with context
  • LogDuration(msg string, start time.Time, extra ...any) - Log operation duration
Configuration Functions
  • WithJSONHandler() - Use JSON output
  • WithTextHandler() - Use text output
  • WithConsoleHandler() - Use colored console output
  • WithLevel(level Level) - Set minimum log level
  • WithDebugLevel() - Enable debug logging
  • WithOutput(w io.Writer) - Set output destination
  • WithServiceName(name string) - Set service name
  • WithServiceVersion(version string) - Set service version
  • WithEnvironment(env string) - Set environment
  • WithSource(enabled bool) - Add source location to logs
  • WithReplaceAttr(fn) - Custom attribute replacer
  • WithCustomLogger(logger *slog.Logger) - Use custom logger
  • WithGlobalLogger() - Register as global slog default
  • WithSampling(cfg SamplingConfig) - Configure log sampling
Error Types
  • ErrNilLogger - Custom logger is nil
  • ErrInvalidHandler - Invalid handler type
  • ErrLoggerShutdown - Logger is shut down
  • ErrInvalidLevel - Invalid log level
  • ErrCannotChangeLevel - Cannot change level on custom logger

License

MIT License - see LICENSE file for details

Contributing

Contributions are welcome! Please see the main repository for contribution guidelines.

  • router - High-performance HTTP router
  • metrics - OpenTelemetry metrics
  • tracing - Distributed tracing
  • app - Batteries-included framework

Documentation

Overview

Package logging provides structured logging with multiple provider backends.

Design philosophy: This package abstracts logging providers to enable:

  • Zero-dependency default (slog in stdlib)
  • Drop-in replacements for existing logging infrastructure
  • Testing with in-memory or no-op providers

The abstraction enables operational flexibility: production systems can switch providers without code changes (e.g., migrating from ELK to Datadog, or adding sampling in high-traffic systems).

Basic usage:

logger := logging.MustNew(logging.WithConsoleHandler())
defer logger.Shutdown(context.Background())
logger.Info("service started", "port", 8080)

With structured logging:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithServiceName("my-service"),
    logging.WithDebugLevel(),
)
defer logger.Shutdown(context.Background())
logger.Info("request processed",
    "method", http.MethodGet,
    "path", "/api/users",
    "status", 200,
)

To register as the global slog default (for use with slog.Info(), etc.):

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithGlobalLogger(), // Sets slog.SetDefault()
)

Sensitive data (password, token, secret, api_key, authorization) is automatically redacted from all log output. Additional sanitization can be configured using WithReplaceAttr.

See the README for more examples and configuration options.

Index

Examples

Constants

View Source
const (
	// LevelDebug is the debug log level.
	LevelDebug = slog.LevelDebug
	LevelInfo  = slog.LevelInfo
	LevelWarn  = slog.LevelWarn
	LevelError = slog.LevelError
)

Variables

View Source
var (
	// ErrNilLogger indicates a nil custom logger was provided to [WithCustomLogger].
	// This is a programmer error and should be caught during initialization.
	ErrNilLogger = errors.New("custom logger is nil")

	// ErrInvalidHandler indicates an unsupported handler type was specified.
	// Valid types: JSONHandler, TextHandler, ConsoleHandler.
	ErrInvalidHandler = errors.New("invalid handler type")

	// ErrLoggerShutdown indicates the logger has been shut down via [Logger.Shutdown].
	// Further log attempts are silently dropped (not an error condition).
	// This error is returned by operations that require an active logger.
	ErrLoggerShutdown = errors.New("logger is shut down")

	// ErrInvalidLevel indicates an invalid log level was provided.
	// Valid levels: LevelDebug, LevelInfo, LevelWarn, LevelError.
	ErrInvalidLevel = errors.New("invalid log level")

	// ErrCannotChangeLevel indicates log level cannot be changed dynamically.
	// Returned by [Logger.SetLevel] when using a custom logger (level controlled externally).
	ErrCannotChangeLevel = errors.New("cannot change level on custom logger")
)

Error types for better error handling and testing.

Design rationale:

  • Sentinel errors (package-level vars) enable errors.Is checks
  • Descriptive names make error handling self-documenting
  • Explicit error types improve testability vs string comparison

Usage pattern:

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

Functions

func ExampleTestHelper

func ExampleTestHelper()

ExampleTestHelper demonstrates using the test helper.

Types

type BatchLogger

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

BatchLogger accumulates log records and flushes them in batches.

Instead of writing each log entry immediately, entries are accumulated and written together.

Trade-offs:

  • Latency: Adds delay before logs appear (up to flush interval)
  • Memory: Buffers entries until flush
  • Durability: Crash before flush loses buffered entries

Mitigation strategies:

  • Periodic flush timer (logs appear within timeout even if batch not full)
  • Crash recovery: External log aggregation systems provide durability
  • Critical logs: Use synchronous logging for audit trails

Typical configuration:

  • Batch size: 100-1000 entries
  • Flush interval: 1-5 seconds

Thread-safe: Safe to use concurrently by multiple goroutines.

func NewBatchLogger

func NewBatchLogger(logger *Logger, batchSize int, flushInterval time.Duration) *BatchLogger

NewBatchLogger creates a logger that batches entries before writing.

Parameters:

  • logger: Underlying Logger for final output
  • batchSize: Maximum entries before automatic flush (typical: 100-1000)
  • flushInterval: Maximum time between flushes (typical: 1-5 seconds)

Choosing batchSize:

  • Too small (< 10): May not provide meaningful batching
  • Too large (> 10000): Increases memory usage and delays
  • Typical: 100-1000 for most applications

Choosing flushInterval:

  • Too short (< 100ms): May not provide meaningful batching
  • Too long (> 30s): Unacceptable log delay
  • Typical: 1-5 seconds for most applications

Example:

logger := logging.MustNew(logging.WithJSONHandler())
batchLogger := logging.NewBatchLogger(logger, 100, time.Second)
defer batchLogger.Close()

// High-frequency logging
for i := 0; i < 10000; i++ {
    batchLogger.Info("high frequency event", "id", i)
}
// Logs are written in batches of 100 or every 1 second
Example

ExampleNewBatchLogger demonstrates batched logging for performance.

package main

import (
	"context"
	"fmt"
	"io"
	"time"

	"rivaas.dev/logging"
)

func main() {
	logger := logging.MustNew(
		logging.WithJSONHandler(),
		logging.WithOutput(io.Discard),
	)
	defer logger.Shutdown(context.Background())

	// Create batch logger that flushes every 100 entries or 1 second
	bl := logging.NewBatchLogger(logger, 100, time.Second)
	defer bl.Close()

	// Add log entries to the batch
	bl.Info("first event")
	bl.Info("second event")

	fmt.Printf("batch size: %d\n", bl.Size())

	// Manual flush writes all pending entries
	bl.Flush()

	fmt.Printf("after flush: %d\n", bl.Size())
}
Output:

batch size: 2
after flush: 0

func (*BatchLogger) Close

func (bl *BatchLogger) Close()

Close stops the batch logger and flushes any remaining entries.

Important: Always call Close() to ensure buffered entries are written. Use defer immediately after creating the BatchLogger:

batchLogger := logging.NewBatchLogger(cfg, 100, time.Second)
defer batchLogger.Close()

Failure to call Close() will result in lost log entries (up to batchSize).

func (*BatchLogger) Debug

func (bl *BatchLogger) Debug(msg string, args ...any)

Debug logs a debug message (batched). The msg and args are held in memory until the batch is flushed.

func (*BatchLogger) Error

func (bl *BatchLogger) Error(msg string, args ...any)

Error logs an error message (batched). The msg and args are held in memory until the batch is flushed.

func (*BatchLogger) Flush

func (bl *BatchLogger) Flush()

Flush writes all batched entries to the underlying logger.

func (*BatchLogger) Info

func (bl *BatchLogger) Info(msg string, args ...any)

Info logs an info message (batched). The msg and args are held in memory until the batch is flushed.

func (*BatchLogger) Size

func (bl *BatchLogger) Size() int

Size returns the current number of batched entries.

func (*BatchLogger) Warn

func (bl *BatchLogger) Warn(msg string, args ...any)

Warn logs a warning message (batched). The msg and args are held in memory until the batch is flushed.

type ContextLogger

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

ContextLogger provides context-aware logging with automatic trace correlation.

Why this exists:

  • Distributed tracing requires trace/span IDs in logs to correlate requests
  • Manually passing trace IDs to every log call is error-prone and verbose
  • This extracts them automatically from OpenTelemetry context

When to use:

✓ Request handlers with OpenTelemetry tracing enabled
✓ Background jobs that propagate context
✗ Package-level loggers (no request context available)

Thread-safe: Safe to use concurrently. Each instance is typically created per-request and used by a single goroutine.

func NewContextLogger

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

NewContextLogger creates a context-aware logger. If the context contains an active OpenTelemetry span, trace and span IDs will be automatically added to all log entries.

Parameters:

  • ctx: Context to extract trace information from
  • logger: The Logger instance to wrap
Example

ExampleNewContextLogger demonstrates context-aware logging.

package main

import (
	"bytes"
	"context"
	"fmt"

	"rivaas.dev/logging"
)

func main() {
	buf := &bytes.Buffer{}
	logger := logging.MustNew(
		logging.WithJSONHandler(),
		logging.WithOutput(buf),
	)
	defer logger.Shutdown(context.Background())

	// Create a context logger and add request-scoped fields via With()
	ctx := context.Background()
	cl := logging.NewContextLogger(ctx, logger)

	// Use With() to add fields to the underlying logger
	clWithFields := cl.With("request_id", "req-123", "user_id", "user-456")
	clWithFields.Info("processing request")

	entries, _ := logging.ParseJSONLogEntries(buf)
	fmt.Printf("request_id: %s\n", entries[0].Attrs["request_id"])
	fmt.Printf("user_id: %s\n", entries[0].Attrs["user_id"])
}
Output:

request_id: req-123
user_id: user-456

func (*ContextLogger) Debug

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

Debug logs a debug message with context.

func (*ContextLogger) Error

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

Error logs an error message with context.

func (*ContextLogger) Info

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

Info logs an info message with context.

func (*ContextLogger) Logger

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

Logger returns the underlying slog.Logger.

func (*ContextLogger) SpanID

func (cl *ContextLogger) SpanID() string

SpanID returns the span ID if available.

func (*ContextLogger) TraceID

func (cl *ContextLogger) TraceID() string

TraceID returns the trace ID if available.

func (*ContextLogger) Warn

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

Warn logs a warning message with context.

func (*ContextLogger) With

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

With returns a slog.Logger with additional attributes.

type CountingWriter

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

CountingWriter counts bytes written without storing them.

Use cases:

  • Tests that need to verify log volume without storing content
  • Volume verification without memory constraints
  • Long-running tests that would exhaust memory with MockWriter

Example:

cw := &CountingWriter{}
logger := logging.MustNew(logging.WithOutput(cw))

for i := 0; i < 1000000; i++ {
    logger.Info("test")
}

t.Logf("Total bytes logged: %d", cw.Count())

func (*CountingWriter) Count

func (cw *CountingWriter) Count() int64

Count returns the total bytes written.

func (*CountingWriter) Write

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

Write implements io.Writer.

type HandlerSpy

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

HandlerSpy implements slog.Handler and records all Handle calls for testing.

Use cases:

  • Test custom handler implementations
  • Verify handler receives expected records
  • Test handler filtering logic
  • Verify attribute and group handling

Example:

spy := &HandlerSpy{}
logger := slog.New(spy)

logger.Info("test", "key", "value")

if spy.RecordCount() != 1 {
    t.Error("expected one record")
}

records := spy.Records()
if records[0].Message != "test" {
    t.Error("unexpected message")
}

func (*HandlerSpy) Enabled

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

Enabled implements slog.Handler.Enabled.

func (*HandlerSpy) Handle

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

Handle implements slog.Handler.Handle.

func (*HandlerSpy) RecordCount

func (hs *HandlerSpy) RecordCount() int

RecordCount returns the number of captured records.

func (*HandlerSpy) Records

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

Records returns all captured records.

func (*HandlerSpy) Reset

func (hs *HandlerSpy) Reset()

Reset clears all captured records.

func (*HandlerSpy) WithAttrs

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

WithAttrs implements slog.Handler.WithAttrs.

func (*HandlerSpy) WithGroup

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

WithGroup implements slog.Handler.WithGroup.

type HandlerType

type HandlerType string

HandlerType represents the type of logging handler.

const (
	// JSONHandler outputs structured JSON logs.
	JSONHandler HandlerType = "json"
	// TextHandler outputs key=value text logs.
	TextHandler HandlerType = "text"
	// ConsoleHandler outputs human-readable colored logs.
	ConsoleHandler HandlerType = "console"
)

type Level

type Level = slog.Level

Level represents log level.

type LogEntry

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

LogEntry represents a parsed log entry for testing.

func ParseJSONLogEntries

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

ParseJSONLogEntries parses JSON log entries from buffer into LogEntry slices. It creates a copy of the buffer so the original is not consumed.

Example

ExampleParseJSONLogEntries demonstrates parsing JSON log entries from a buffer.

package main

import (
	"bytes"
	"context"
	"fmt"

	"rivaas.dev/logging"
)

func main() {
	buf := &bytes.Buffer{}
	logger := logging.MustNew(
		logging.WithJSONHandler(),
		logging.WithOutput(buf),
	)
	defer logger.Shutdown(context.Background())

	logger.Info("first message")
	logger.Warn("second message", "count", 42)

	entries, err := logging.ParseJSONLogEntries(buf)
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	fmt.Printf("entry count: %d\n", len(entries))
	fmt.Printf("first level: %s\n", entries[0].Level)
	fmt.Printf("second level: %s\n", entries[1].Level)
}
Output:

entry count: 2
first level: INFO
second level: WARN

type Logger

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

Logger is the main logging type that provides structured logging capabilities.

Thread-safety: All public methods are safe for concurrent use. The slogger field is accessed atomically, while mu protects initialization and reconfiguration operations.

func MustNew

func MustNew(opts ...Option) *Logger

MustNew creates a new Logger or panics on error.

Example

ExampleMustNew demonstrates creating a logger that panics on error. This is useful for application initialization where errors are fatal.

package main

import (
	"context"

	"rivaas.dev/logging"
)

func main() {
	logger := logging.MustNew(
		logging.WithConsoleHandler(),
		logging.WithDebugLevel(),
	)
	defer logger.Shutdown(context.Background())

	logger.Info("application initialized")
	logger.Debug("debug information", "key", "value")
	// Output is non-deterministic (contains timestamps and colors)
}

func New

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

New creates a new Logger with the given options.

By default, this function does NOT set the global slog default logger. Use WithGlobalLogger() if you want to register this logger as the global default.

This allows multiple Logger instances to coexist in the same process, and makes it easier to integrate Rivaas into larger binaries that already manage their own global logger.

Example

ExampleNew demonstrates creating a new logger with basic configuration. The logger outputs JSON-formatted logs to stdout.

package main

import (
	"context"
	"fmt"

	"rivaas.dev/logging"
)

func main() {
	logger, err := logging.New(
		logging.WithJSONHandler(),
		logging.WithServiceName("my-service"),
		logging.WithServiceVersion("1.0.0"),
	)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer logger.Shutdown(context.Background())

	logger.Info("service started", "port", 8080)
	// Output is non-deterministic (contains timestamps)
}
Example (Validation)

ExampleNew_validation demonstrates that New validates configuration.

package main

import (
	"fmt"

	"rivaas.dev/logging"
)

func main() {
	_, err := logging.New(logging.WithOutput(nil))
	if err != nil {
		fmt.Println("validation error:", err != nil)
	}
}
Output:

validation error: true

func NewTestLogger

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

NewTestLogger creates a Logger for testing with an in-memory buffer. The returned buffer can be used with ParseJSONLogEntries to inspect log output.

Example

ExampleNewTestLogger demonstrates creating a logger for testing.

package main

import (
	"context"
	"fmt"

	"rivaas.dev/logging"
)

func main() {
	logger, buf := logging.NewTestLogger()
	defer logger.Shutdown(context.Background())

	logger.Info("test message", "key", "value")

	entries, _ := logging.ParseJSONLogEntries(buf)
	fmt.Printf("logged %d entries\n", len(entries))
	fmt.Printf("message: %s\n", entries[0].Message)
}
Output:

logged 1 entries
message: test message

func (*Logger) Debug

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

Debug logs a debug message with structured attributes. Thread-safe and safe to call concurrently.

func (*Logger) DebugInfo added in v0.2.0

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

DebugInfo returns diagnostic information about the logger.

func (*Logger) Environment added in v0.2.0

func (l *Logger) Environment() string

Environment returns the environment. This field is immutable after initialization, so no lock is needed.

func (*Logger) Error

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

Error logs an error message with structured attributes. Thread-safe and safe to call concurrently. Note: Errors bypass sampling and are always logged.

func (*Logger) ErrorWithStack added in v0.2.0

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

ErrorWithStack logs an error with optional stack trace.

When to use stack traces:

✓ Critical errors that require debugging
✓ Unexpected error conditions (panics, invariant violations)
✗ Expected errors (validation failures, not found)
✗ High-frequency errors where stack capture cost is undesirable

Thread-safe and safe to call concurrently.

Example

ExampleLogger_ErrorWithStack demonstrates error logging with stack traces. Stack traces should only be enabled for critical errors to avoid performance overhead.

package main

import (
	"context"
	"errors"

	"rivaas.dev/logging"
)

func main() {
	logger := logging.MustNew(logging.WithConsoleHandler())
	defer logger.Shutdown(context.Background())

	err := errors.New("critical error occurred")
	logger.ErrorWithStack("critical error", err, true,
		"component", "payment-processor",
		"transaction_id", "tx-12345",
	)
}

func (*Logger) FlushBuffer added in v0.2.0

func (l *Logger) FlushBuffer() error

FlushBuffer replays all buffered log records to the output and disables buffering. If buffering was not enabled, this is a no-op.

Example:

logger.StartBuffering()
// ... initialization that produces logs ...
printBanner()
logger.FlushBuffer() // Now all startup logs appear after the banner

func (*Logger) Info

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

Info logs an informational message with structured attributes. Thread-safe and safe to call concurrently.

func (*Logger) IsBuffering added in v0.2.0

func (l *Logger) IsBuffering() bool

IsBuffering returns whether the logger is currently buffering logs.

func (*Logger) IsEnabled added in v0.2.0

func (l *Logger) IsEnabled() bool

IsEnabled returns true if logging is enabled and not shutting down.

Example

ExampleLogger_IsEnabled demonstrates checking if logging is enabled.

package main

import (
	"context"
	"fmt"
	"io"

	"rivaas.dev/logging"
)

func main() {
	logger := logging.MustNew(
		logging.WithJSONHandler(),
		logging.WithOutput(io.Discard),
	)

	fmt.Printf("before shutdown: %v\n", logger.IsEnabled())

	logger.Shutdown(context.Background())

	fmt.Printf("after shutdown: %v\n", logger.IsEnabled())
}
Output:

before shutdown: true
after shutdown: false

func (*Logger) Level added in v0.2.0

func (l *Logger) Level() Level

Level returns the current minimum log level. Note: This requires a lock because level can be changed dynamically via SetLevel.

Example

ExampleLogger_Level demonstrates accessing and checking the log level.

package main

import (
	"context"
	"fmt"
	"io"

	"rivaas.dev/logging"
)

func main() {
	logger := logging.MustNew(
		logging.WithJSONHandler(),
		logging.WithOutput(io.Discard),
		logging.WithLevel(logging.LevelWarn),
	)
	defer logger.Shutdown(context.Background())

	level := logger.Level()
	fmt.Printf("level: %s\n", level.String())
}
Output:

level: WARN

func (*Logger) LogDuration added in v0.2.0

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

LogDuration logs an operation duration with timing information.

Automatically includes:

  • duration_ms: Duration in milliseconds (for easy filtering/alerting)
  • duration: Human-readable duration string (e.g., "1.5s", "250ms")

Thread-safe and safe to call concurrently.

Example:

start := time.Now()
result, err := processData(data)
logger.LogDuration("data processing completed", start,
    "rows_processed", result.Count,
    "errors", result.Errors,
)
Example

ExampleLogger_LogDuration demonstrates logging operation duration. Both human-readable duration and milliseconds are automatically included.

package main

import (
	"context"
	"time"

	"rivaas.dev/logging"
)

func main() {
	logger := logging.MustNew(logging.WithJSONHandler())
	defer logger.Shutdown(context.Background())

	start := time.Now()
	// Simulate some work
	time.Sleep(10 * time.Millisecond)

	logger.LogDuration("data processing completed", start,
		"rows_processed", 1000,
		"errors", 0,
	)
	// Output is non-deterministic (contains timestamps and duration)
}

func (*Logger) LogError added in v0.2.0

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

LogError logs an error with additional context fields.

Why use this instead of Error():

  • Automatically includes "error" field with error message
  • Convenient for error handling patterns
  • Consistent error logging format across codebase

Thread-safe and safe to call concurrently.

Example:

if err := db.Insert(user); err != nil {
    logger.LogError(err, "database operation failed",
        "operation", "INSERT",
        "table", "users",
        "retry_count", 3,
    )
    return err
}
Example

ExampleLogger_LogError demonstrates logging errors with context. The error message is automatically added as the "error" attribute.

package main

import (
	"context"
	"errors"

	"rivaas.dev/logging"
)

func main() {
	logger := logging.MustNew(logging.WithConsoleHandler())
	defer logger.Shutdown(context.Background())

	err := errors.New("database connection failed")
	logger.LogError(err, "database operation failed",
		"operation", "INSERT",
		"table", "users",
		"retry_count", 3,
	)
	// Output is non-deterministic (contains timestamps and colors)
}

func (*Logger) LogRequest added in v0.2.0

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

LogRequest logs an HTTP request with standard fields.

Standard fields included:

  • method: HTTP method (GET, POST, etc.)
  • path: Request path (without query string)
  • remote: Client remote address
  • user_agent: Client User-Agent header
  • query: Query string (only if non-empty)

Additional fields can be passed via 'extra' (e.g., "status", 200, "duration_ms", 45).

Thread-safe and safe to call concurrently.

Example:

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

ExampleLogger_LogRequest demonstrates logging HTTP requests. This helper method automatically extracts common request fields.

package main

import (
	"context"
	"net/http"

	"rivaas.dev/logging"
)

func main() {
	ctx := context.Background()
	logger := logging.MustNew(logging.WithJSONHandler())
	defer logger.Shutdown(ctx)

	// Simulate an HTTP request
	req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "/api/users?page=1", nil)
	req.RemoteAddr = "192.168.1.1:12345"
	req.Header.Set("User-Agent", "MyApp/1.0")

	logger.LogRequest(req, "status", 200, "duration_ms", 45)
	// Output is non-deterministic (contains timestamps)
}

func (*Logger) Logger added in v0.2.0

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

Logger returns the underlying slog.Logger. This method is safe for concurrent access.

func (*Logger) ServiceName added in v0.2.0

func (l *Logger) ServiceName() string

ServiceName returns the service name. This field is immutable after initialization, so no lock is needed.

Example

ExampleLogger_ServiceName demonstrates accessing logger configuration.

package main

import (
	"context"
	"fmt"
	"io"

	"rivaas.dev/logging"
)

func main() {
	logger := logging.MustNew(
		logging.WithJSONHandler(),
		logging.WithOutput(io.Discard),
		logging.WithServiceName("my-api"),
		logging.WithServiceVersion("2.0.0"),
		logging.WithEnvironment("production"),
	)
	defer logger.Shutdown(context.Background())

	fmt.Printf("service: %s\n", logger.ServiceName())
	fmt.Printf("version: %s\n", logger.ServiceVersion())
	fmt.Printf("env: %s\n", logger.Environment())
}
Output:

service: my-api
version: 2.0.0
env: production

func (*Logger) ServiceVersion added in v0.2.0

func (l *Logger) ServiceVersion() string

ServiceVersion returns the service version. This field is immutable after initialization, so no lock is needed.

func (*Logger) SetLevel added in v0.2.0

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

SetLevel dynamically changes the minimum log level at runtime.

Use cases:

  • Enable debug logging temporarily for troubleshooting
  • Reduce log volume during high traffic periods
  • Runtime configuration via HTTP endpoint or signal handler

Limitations:

  • Not supported with custom loggers (returns ErrCannotChangeLevel)
  • Brief initialization window where old/new levels may race

Thread-safe: Safe to call concurrently, but multiple SetLevel calls will serialize.

Example

ExampleLogger_SetLevel demonstrates dynamic level changes at runtime.

package main

import (
	"context"
	"fmt"
	"io"

	"rivaas.dev/logging"
)

func main() {
	logger := logging.MustNew(
		logging.WithJSONHandler(),
		logging.WithOutput(io.Discard),
		logging.WithLevel(logging.LevelInfo),
	)
	defer logger.Shutdown(context.Background())

	fmt.Printf("initial: %s\n", logger.Level().String())

	err := logger.SetLevel(logging.LevelDebug)
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	fmt.Printf("changed: %s\n", logger.Level().String())
}
Output:

initial: INFO
changed: DEBUG

func (*Logger) Shutdown added in v0.2.0

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

Shutdown gracefully shuts down the logger.

func (*Logger) StartBuffering added in v0.2.0

func (l *Logger) StartBuffering()

StartBuffering enables log buffering on the logger. While buffering is enabled, log records are stored in memory instead of being written. Call FlushBuffer to replay all buffered logs to the output.

This is useful for delaying startup logs until after a banner or other output is printed.

Example:

logger.StartBuffering()
// ... initialization that produces logs ...
printBanner()
logger.FlushBuffer()

func (*Logger) Validate added in v0.2.0

func (l *Logger) Validate() error

Validate checks if the configuration is valid.

func (*Logger) Warn

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

Warn logs a warning message with structured attributes. Thread-safe and safe to call concurrently.

func (*Logger) With added in v0.2.0

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

With returns a slog.Logger with additional attributes.

func (*Logger) WithGroup added in v0.2.0

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

WithGroup returns a slog.Logger with a group name.

type MockWriter

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

MockWriter is a mock io.Writer that records all writes for test assertions.

Use cases:

  • Verify number of write calls (batching behavior)
  • Inspect write contents (log format validation)
  • Simulate write errors (error handling tests)

Example:

mw := &MockWriter{}
logger := logging.MustNew(logging.WithOutput(mw))
logger.Info("test")

if mw.WriteCount() != 1 {
    t.Error("expected exactly one write")
}

func (*MockWriter) BytesWritten

func (mw *MockWriter) BytesWritten() int

BytesWritten returns total bytes written.

func (*MockWriter) LastWrite

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

LastWrite returns the most recent write.

func (*MockWriter) Reset

func (mw *MockWriter) Reset()

Reset clears all recorded writes.

func (*MockWriter) Write

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

Write implements io.Writer.

func (*MockWriter) WriteCount

func (mw *MockWriter) WriteCount() int

WriteCount returns the number of write calls.

type Option

type Option func(*Logger)

Option is a functional option for configuring the logger.

func WithConsoleHandler

func WithConsoleHandler() Option

WithConsoleHandler uses human-readable console logging.

func WithCustomLogger

func WithCustomLogger(customLogger *slog.Logger) Option

WithCustomLogger uses a custom slog.Logger instead of creating one. When using a custom logger, Logger.SetLevel is not supported.

func WithDebugLevel

func WithDebugLevel() Option

WithDebugLevel enables debug logging.

func WithDebugMode

func WithDebugMode(enabled bool) Option

WithDebugMode enables verbose debugging information.

func WithEnvironment

func WithEnvironment(env string) Option

WithEnvironment sets the environment. When set, the environment is automatically added to all log entries.

func WithGlobalLogger

func WithGlobalLogger() Option

WithGlobalLogger registers this logger as the global slog default logger. By default, loggers are not registered globally to allow multiple logger instances to coexist in the same process.

Example:

logger := logging.MustNew(
    logging.WithJSONHandler(),
    logging.WithGlobalLogger(), // Register as global default
)

func WithHandlerType

func WithHandlerType(t HandlerType) Option

WithHandlerType sets the logging handler type.

func WithJSONHandler

func WithJSONHandler() Option

WithJSONHandler uses JSON structured logging (default).

func WithLevel

func WithLevel(level Level) Option

WithLevel sets the minimum log level.

func WithOutput

func WithOutput(w io.Writer) Option

WithOutput sets the output writer.

func WithReplaceAttr

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

WithReplaceAttr sets a custom attribute replacer function. The function receives groups and an slog.Attr, and returns a modified attribute. Return an empty slog.Attr to drop the attribute from output.

Example

ExampleWithReplaceAttr demonstrates custom attribute replacement. This is useful for custom redaction rules beyond the built-in sensitive fields.

package main

import (
	"context"
	"log/slog"

	"rivaas.dev/logging"
)

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

			return a
		}),
	)
	defer logger.Shutdown(context.Background())

	logger.Info("user login", "username", "alice", "password", "secret123")
	// Output is non-deterministic (contains timestamps)
}

func WithSampling

func WithSampling(cfg SamplingConfig) Option

WithSampling enables log sampling to reduce volume in high-traffic scenarios. See SamplingConfig for configuration options.

Example

ExampleWithSampling demonstrates log sampling for high-traffic scenarios. Sampling reduces log volume while maintaining visibility.

package main

import (
	"context"
	"time"

	"rivaas.dev/logging"
)

func main() {
	logger := logging.MustNew(
		logging.WithJSONHandler(),
		logging.WithSampling(logging.SamplingConfig{
			Initial:    100,         // Log first 100 entries unconditionally
			Thereafter: 100,         // Then log 1 in every 100
			Tick:       time.Minute, // Reset counter every minute
		}),
	)
	defer logger.Shutdown(context.Background())

	// In high-traffic scenarios, this will sample logs
	for i := range 500 {
		logger.Info("request processed", "request_id", i)
	}
	// Output is non-deterministic (sampling behavior varies)
}

func WithServiceName

func WithServiceName(name string) Option

WithServiceName sets the service name. When set, the service name is automatically added to all log entries.

func WithServiceVersion

func WithServiceVersion(version string) Option

WithServiceVersion sets the service version. When set, the version is automatically added to all log entries.

func WithSource

func WithSource(enabled bool) Option

WithSource enables source code location in logs.

func WithTextHandler

func WithTextHandler() Option

WithTextHandler uses text key=value logging.

type SamplingConfig

type SamplingConfig struct {
	Initial    int           // Log first N occurrences unconditionally
	Thereafter int           // After Initial, log 1 of every M entries (0 = log all)
	Tick       time.Duration // Reset sampling counter every interval (0 = never reset)
}

SamplingConfig configures log sampling to reduce volume in high-traffic scenarios.

Sampling algorithm:

  1. Log the first 'Initial' entries unconditionally (e.g., first 100)
  2. After that, log 1 in every 'Thereafter' entries (e.g., 1 in 100)
  3. Reset the counter every 'Tick' interval to avoid indefinite accumulation

Example: Initial=100, Thereafter=100, Tick=1m means:

  • Always log first 100 entries
  • Then log 1% of entries (1 in 100)
  • Every minute, reset counter (log next 100 again)

This ensures you always see some recent activity while managing log volume.

type SlowWriter

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

SlowWriter simulates slow I/O for testing timeouts and backpressure.

Use cases:

  • Test timeout handling in logging middleware
  • Simulate network latency in distributed logging
  • Verify non-blocking behavior under slow I/O
  • Test context cancellation during logging

Example:

// Simulate 100ms network latency
sw := NewSlowWriter(100*time.Millisecond, &bytes.Buffer{})
logger := logging.MustNew(logging.WithOutput(sw))

start := time.Now()
logger.Info("test")
duration := time.Since(start)

if duration < 100*time.Millisecond {
    t.Error("expected write to be delayed")
}

func NewSlowWriter

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

NewSlowWriter creates a writer that delays each write.

func (*SlowWriter) Write

func (sw *SlowWriter) Write(p []byte) (n int, err error)

Write implements io.Writer with delay.

type TestHelper

type TestHelper struct {
	Logger *Logger
	Buffer *bytes.Buffer
}

TestHelper provides utilities for testing with the logging package.

func NewTestHelper

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

NewTestHelper creates a TestHelper with in-memory logging. Additional Option values can be passed to customize the logger.

func (*TestHelper) AssertLog

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

AssertLog checks that a log entry exists with the given properties.

func (*TestHelper) ContainsAttr

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

ContainsAttr checks if any log entry contains the given attribute.

func (*TestHelper) ContainsLog

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

ContainsLog checks if any log entry contains the given message.

func (*TestHelper) CountLevel

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

CountLevel returns the number of log entries at the given level.

func (*TestHelper) LastLog

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

LastLog returns the most recent log entry.

func (*TestHelper) Logs

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

Logs returns all parsed log entries.

func (*TestHelper) Reset

func (th *TestHelper) Reset()

Reset clears the buffer for fresh testing.

Directories

Path Synopsis
examples
01-quickstart command
Package main demonstrates basic logging setup for development.
Package main demonstrates basic logging setup for development.
02-production command
Package main demonstrates production-ready logging configuration.
Package main demonstrates production-ready logging configuration.
03-helper-methods command
Package main demonstrates logging helper methods and utilities.
Package main demonstrates logging helper methods and utilities.
04-testing command
Package main demonstrates logging utilities for testing.
Package main demonstrates logging utilities for testing.

Jump to

Keyboard shortcuts

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