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 ¶
- Constants
- Variables
- func ExampleTestHelper()
- type BatchLogger
- func (bl *BatchLogger) Close()
- func (bl *BatchLogger) Debug(msg string, args ...any)
- func (bl *BatchLogger) Error(msg string, args ...any)
- func (bl *BatchLogger) Flush()
- func (bl *BatchLogger) Info(msg string, args ...any)
- func (bl *BatchLogger) Size() int
- func (bl *BatchLogger) Warn(msg string, args ...any)
- type ContextLogger
- func (cl *ContextLogger) Debug(msg string, args ...any)
- func (cl *ContextLogger) Error(msg string, args ...any)
- func (cl *ContextLogger) Info(msg string, args ...any)
- func (cl *ContextLogger) Logger() *slog.Logger
- func (cl *ContextLogger) SpanID() string
- func (cl *ContextLogger) TraceID() string
- func (cl *ContextLogger) Warn(msg string, args ...any)
- func (cl *ContextLogger) With(args ...any) *slog.Logger
- type CountingWriter
- type HandlerSpy
- func (hs *HandlerSpy) Enabled(_ context.Context, _ slog.Level) bool
- func (hs *HandlerSpy) Handle(_ context.Context, r slog.Record) error
- func (hs *HandlerSpy) RecordCount() int
- func (hs *HandlerSpy) Records() []slog.Record
- func (hs *HandlerSpy) Reset()
- func (hs *HandlerSpy) WithAttrs(_ []slog.Attr) slog.Handler
- func (hs *HandlerSpy) WithGroup(_ string) slog.Handler
- type HandlerType
- type Level
- type LogEntry
- type Logger
- func (l *Logger) Debug(msg string, args ...any)
- func (l *Logger) DebugInfo() map[string]any
- func (l *Logger) Environment() string
- func (l *Logger) Error(msg string, args ...any)
- func (l *Logger) ErrorWithStack(msg string, err error, includeStack bool, extra ...any)
- func (l *Logger) FlushBuffer() error
- func (l *Logger) Info(msg string, args ...any)
- func (l *Logger) IsBuffering() bool
- func (l *Logger) IsEnabled() bool
- func (l *Logger) Level() Level
- func (l *Logger) LogDuration(msg string, start time.Time, extra ...any)
- func (l *Logger) LogError(err error, msg string, extra ...any)
- func (l *Logger) LogRequest(r *http.Request, extra ...any)
- func (l *Logger) Logger() *slog.Logger
- func (l *Logger) ServiceName() string
- func (l *Logger) ServiceVersion() string
- func (l *Logger) SetLevel(level Level) error
- func (l *Logger) Shutdown(_ context.Context) error
- func (l *Logger) StartBuffering()
- func (l *Logger) Validate() error
- func (l *Logger) Warn(msg string, args ...any)
- func (l *Logger) With(args ...any) *slog.Logger
- func (l *Logger) WithGroup(name string) *slog.Logger
- type MockWriter
- type Option
- func WithConsoleHandler() Option
- func WithCustomLogger(customLogger *slog.Logger) Option
- func WithDebugLevel() Option
- func WithDebugMode(enabled bool) Option
- func WithEnvironment(env string) Option
- func WithGlobalLogger() Option
- func WithHandlerType(t HandlerType) Option
- func WithJSONHandler() Option
- func WithLevel(level Level) Option
- func WithOutput(w io.Writer) Option
- func WithReplaceAttr(fn func(groups []string, a slog.Attr) slog.Attr) Option
- func WithSampling(cfg SamplingConfig) Option
- func WithServiceName(name string) Option
- func WithServiceVersion(version string) Option
- func WithSource(enabled bool) Option
- func WithTextHandler() Option
- type SamplingConfig
- type SlowWriter
- type TestHelper
- func (th *TestHelper) AssertLog(t *testing.T, level, msg string, attrs map[string]any)
- func (th *TestHelper) ContainsAttr(key string, value any) bool
- func (th *TestHelper) ContainsLog(msg string) bool
- func (th *TestHelper) CountLevel(level string) int
- func (th *TestHelper) LastLog() (*LogEntry, error)
- func (th *TestHelper) Logs() ([]LogEntry, error)
- func (th *TestHelper) Reset()
Examples ¶
Constants ¶
const ( // LevelDebug is the debug log level. LevelDebug = slog.LevelDebug LevelInfo = slog.LevelInfo LevelWarn = slog.LevelWarn LevelError = slog.LevelError )
Variables ¶
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.
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 ¶
Enabled implements slog.Handler.Enabled.
func (*HandlerSpy) Handle ¶
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) 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 LogEntry ¶
LogEntry represents a parsed log entry for testing.
func ParseJSONLogEntries ¶
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 ¶
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 ¶
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 ¶
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 ¶
Debug logs a debug message with structured attributes. Thread-safe and safe to call concurrently.
func (*Logger) DebugInfo ¶ added in v0.2.0
DebugInfo returns diagnostic information about the logger.
func (*Logger) Environment ¶ added in v0.2.0
Environment returns the environment. This field is immutable after initialization, so no lock is needed.
func (*Logger) Error ¶
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
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
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 ¶
Info logs an informational message with structured attributes. Thread-safe and safe to call concurrently.
func (*Logger) IsBuffering ¶ added in v0.2.0
IsBuffering returns whether the logger is currently buffering logs.
func (*Logger) IsEnabled ¶ added in v0.2.0
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
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
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
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
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
Logger returns the underlying slog.Logger. This method is safe for concurrent access.
func (*Logger) ServiceName ¶ added in v0.2.0
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
ServiceVersion returns the service version. This field is immutable after initialization, so no lock is needed.
func (*Logger) SetLevel ¶ added in v0.2.0
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) 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) Warn ¶
Warn logs a warning message with structured attributes. Thread-safe and safe to call concurrently.
func (*Logger) With ¶ added in v0.2.0
With returns a slog.Logger with additional attributes.
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) 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 ¶
WithCustomLogger uses a custom slog.Logger instead of creating one. When using a custom logger, Logger.SetLevel is not supported.
func WithDebugMode ¶
WithDebugMode enables verbose debugging information.
func WithEnvironment ¶
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 WithReplaceAttr ¶
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 ¶
WithServiceName sets the service name. When set, the service name is automatically added to all log entries.
func WithServiceVersion ¶
WithServiceVersion sets the service version. When set, the version is automatically added to all log entries.
func WithSource ¶
WithSource enables source code location in logs.
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:
- Log the first 'Initial' entries unconditionally (e.g., first 100)
- After that, log 1 in every 'Thereafter' entries (e.g., 1 in 100)
- 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.
type TestHelper ¶
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) 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.
Source Files
¶
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. |