Documentation
¶
Overview ¶
Package handler is a feature normalization layer that allows any third-party logger (regardless of native capabilities) to satisfy unilog's unified interface through strategic backfilling of missing features.
Shared State Warning ¶
IMPORTANT: Chainer methods (WithAttrs, WithGroup) return NEW handlers that SHARE mutable state (level, output) with the parent. This enables:
- Runtime reconfiguration: parent.SetLevel(Debug) affects all children
- Efficient request chains: minimal allocations for With() calls
If you need independent loggers for different modules, use AdvancedHandler.WithLevel() or create separate handler instances.
Example:
parent := logger.With("service", "api")
child := parent.With("endpoint", "/users")
parent.SetLevel(Debug) // Also affects child
Index ¶
- Constants
- Variables
- func ComplianceTest(t *testing.T, newHandler func() (Handler, error))
- func IsValidLogLevel(level LogLevel) bool
- func NewAtomicWriterError(err error) error
- func NewInvalidFormatError(format string, accepted []string) error
- func NewInvalidLogLevelError(level LogLevel) error
- func NewOptionApplyError(option string, err error) error
- func ValidateLogLevel(level LogLevel) error
- type BaseHandler
- func (h *BaseHandler) AtomicWriter() *atomicwriter.AtomicWriter
- func (h *BaseHandler) CallerEnabled() bool
- func (h *BaseHandler) CallerSkip() int
- func (h *BaseHandler) Clone() *BaseHandler
- func (h *BaseHandler) Enabled(level LogLevel) bool
- func (h *BaseHandler) Format() string
- func (h *BaseHandler) HasFlag(flag StateFlag) bool
- func (h *BaseHandler) KeyPrefix() string
- func (h *BaseHandler) Level() LogLevel
- func (h *BaseHandler) Separator() string
- func (h *BaseHandler) SetCallerSkip(skip int) error
- func (h *BaseHandler) SetFlag(flag StateFlag, enabled bool)
- func (h *BaseHandler) SetLevel(level LogLevel) error
- func (h *BaseHandler) SetOutput(w io.Writer) error
- func (h *BaseHandler) TraceEnabled() bool
- func (h *BaseHandler) WithCaller(enabled bool) *BaseHandler
- func (h *BaseHandler) WithCallerSkip(skip int) (*BaseHandler, error)
- func (h *BaseHandler) WithCallerSkipDelta(delta int) (*BaseHandler, error)
- func (h *BaseHandler) WithKeyPrefix(prefix string) (*BaseHandler, error)
- func (h *BaseHandler) WithLevel(level LogLevel) (*BaseHandler, error)
- func (h *BaseHandler) WithOutput(w io.Writer) (*BaseHandler, error)
- func (h *BaseHandler) WithTrace(enabled bool) *BaseHandler
- type BaseOption
- type BaseOptions
- type CallerAdjuster
- type Chainer
- type ComplianceChecker
- type Configurable
- type Feature
- type FeatureToggler
- type Handler
- type HandlerFeatures
- type HandlerState
- type LevelMapper
- type LogLevel
- type MutableConfig
- type Record
- type StateFlag
- type Syncer
Constants ¶
const DefaultKeySeparator = "_"
DefaultKeySeparator is the default separator for group key prefixes.
Variables ¶
var ( ErrInvalidLogLevel = errors.New("invalid log level") ErrAtomicWriterFail = errors.New("failed to create atomic writer") ErrOptionApplyFailed = errors.New("failed to apply option") ErrInvalidFormat = errors.New("invalid format") ErrInvalidSourceSkip = errors.New("source skip must be non-negative") ErrNilWriter = errors.New("writer cannot be nil") )
Sentinel errors for common conditions
Functions ¶
func ComplianceTest ¶
ComplianceTest runs comprehensive compliance tests against a Handler implementation. Third-party handler authors can use this to verify their implementations meet the handler.Handler interface contract.
The test suite covers:
- Enabled: Verifies handler is enabled at InfoLevel
- Handle: Verifies handler processes valid records without error
- Chainer: Verifies Chainer methods return non-nil (skipped if not implemented)
Example usage:
// Optional static assertions
var _ handler.Handler = (*MyHandler)(nil)
var _ handler.Chainer = (*MyHandler)(nil) // If Chainer is implemented
// Compliance tests in the test code
func TestMyHandlerCompliance(t *testing.T) {
handler.ComplianceTest(t, func() (handler.Handler, error) {
return NewMyHandler(...)
})
}
func IsValidLogLevel ¶
IsValidLogLevel returns true if the given log level is valid.
func NewAtomicWriterError ¶
NewAtomicWriterError returns an error wrapping ErrAtomicWriterFail.
func NewInvalidFormatError ¶
NewInvalidFormatError returns an error wrapping ErrInvalidFormat.
func NewInvalidLogLevelError ¶
NewInvalidLogLevelError returns an error wrapping ErrInvalidLogLevel.
func NewOptionApplyError ¶
NewOptionApplyError returns an error wrapping ErrOptionApplyFailed.
func ValidateLogLevel ¶
ValidateLogLevel returns an error if the given log level is invalid.
Types ¶
type BaseHandler ¶
type BaseHandler struct {
// contains filtered or unexported fields
}
BaseHandler provides shared functionality for handler implementations. Handlers that embed BaseHandler can use its optional helpers or ignore them in favor of their own optimized implementations.
All methods are thread-safe. Handlers should cache flag states at init for lock-free hot-path performance.
Concurrency Model:
BaseHandler uses two synchronization primitives:
- sync.RWMutex (mu) protects format, callerSkip, keyPrefix, separator
- atomic.Uint32/Int32 for flags and level (lock-free reads)
Design rationale:
- Logging (hot path) requires lock-free flag checks
- Configuration changes (cold path) can tolerate mutex overhead
- Handlers should cache flag states at init for zero-lock logging
Performance:
- Handlers SHOULD cache immutable config (format, separator) at initialization to avoid RWMutex contention in hot path. See handler/slog for example.
- Mutable config (level, output) uses atomics/AtomicWriter for lock-free access.
Mutability semantics:
- Set* methods mutate in-place (affect shared state)
- With* methods return new instances (immutable pattern)
- Clone() creates independent copy with separate mutex but shared AtomicWriter
Example handler optimization:
type myHandler struct {
base *BaseHandler
needsCaller bool // Cached at init
format string // Cached at init
}
func (h *myHandler) Handle(...) {
if h.needsCaller { /* no lock */ }
}
State Management:
- Shared state: level, output, format (modified via Set* methods)
- Independent state: keyPrefix, callerSkip (cloned via With* methods)
Caller Detection:
- Handlers needing source location should use github.com/balinomad/go-caller.
- See github.com/balinomad/go-unilog/handler/stdlog for an example.
func NewBaseHandler ¶
func NewBaseHandler(opts *BaseOptions) (*BaseHandler, error)
NewBaseHandler initializes a new BaseHandler.
func (*BaseHandler) AtomicWriter ¶
func (h *BaseHandler) AtomicWriter() *atomicwriter.AtomicWriter
AtomicWriter returns the underlying atomic writer. Handlers use this to get the thread-safe writer for backend initialization.
func (*BaseHandler) CallerEnabled ¶
func (h *BaseHandler) CallerEnabled() bool
CallerEnabled returns whether caller information should be included.
func (*BaseHandler) CallerSkip ¶
func (h *BaseHandler) CallerSkip() int
CallerSkip returns the number of stack frames to skip for caller reporting. Handlers should add their internal skip constant to this value.
Example:
totalSkip := myHandler.internalSkipFrames + h.base.CallerSkip() + dynSkip
func (*BaseHandler) Clone ¶
func (h *BaseHandler) Clone() *BaseHandler
Clone returns a shallow copy of BaseHandler with independent mutex. The new instance shares the AtomicWriter but has separate state locks. This means SetOutput() on the clone affects the original's output destination. For fully independent handlers, create separate handler instances with different writers.
func (*BaseHandler) Enabled ¶
func (h *BaseHandler) Enabled(level LogLevel) bool
Enabled reports whether the handler processes records at the given level.
func (*BaseHandler) Format ¶
func (h *BaseHandler) Format() string
Format returns the configured format string.
func (*BaseHandler) HasFlag ¶
func (h *BaseHandler) HasFlag(flag StateFlag) bool
HasFlag checks if flag is set (lock-free).
func (*BaseHandler) KeyPrefix ¶
func (h *BaseHandler) KeyPrefix() string
KeyPrefix returns the current key prefix.
func (*BaseHandler) Level ¶
func (h *BaseHandler) Level() LogLevel
Level returns the current minimum log level.
func (*BaseHandler) Separator ¶
func (h *BaseHandler) Separator() string
Separator returns the current separator.
func (*BaseHandler) SetCallerSkip ¶
func (h *BaseHandler) SetCallerSkip(skip int) error
SetCallerSkip changes the caller skip value. Affects all instances sharing this base.
func (*BaseHandler) SetFlag ¶
func (h *BaseHandler) SetFlag(flag StateFlag, enabled bool)
SetFlag atomically sets or clears a flag. Affects all instances sharing this base.
func (*BaseHandler) SetLevel ¶
func (h *BaseHandler) SetLevel(level LogLevel) error
SetLevel changes the minimum level of logs that will be processed. Affects all instances sharing this base.
func (*BaseHandler) SetOutput ¶
func (h *BaseHandler) SetOutput(w io.Writer) error
SetOutput changes the destination for log output. Affects all instances sharing this base.
func (*BaseHandler) TraceEnabled ¶
func (h *BaseHandler) TraceEnabled() bool
TraceEnabled returns whether stack traces should be included for error-level logs.
func (*BaseHandler) WithCaller ¶
func (h *BaseHandler) WithCaller(enabled bool) *BaseHandler
WithCaller returns a shallow copy of BaseHandler with caller flag set. If the caller flag is already set, returns the original instance.
func (*BaseHandler) WithCallerSkip ¶
func (h *BaseHandler) WithCallerSkip(skip int) (*BaseHandler, error)
WithCallerSkip returns a shallow copy of BaseHandler with updated caller skip. If the skip is already set, returns the original instance.
func (*BaseHandler) WithCallerSkipDelta ¶
func (h *BaseHandler) WithCallerSkipDelta(delta int) (*BaseHandler, error)
WithCallerSkipDelta returns a shallow copy of BaseHandler with caller skip adjusted by delta. If delta is zero , returns the original instance. If the new skip is negative, it returns an error.
func (*BaseHandler) WithKeyPrefix ¶
func (h *BaseHandler) WithKeyPrefix(prefix string) (*BaseHandler, error)
WithKeyPrefix returns a shallow copy of BaseHandler with the given prefix applied. This supports WithGroup for handlers without native prefix support. Returns error if total prefix length exceeds maxKeyPrefixLength.
func (*BaseHandler) WithLevel ¶
func (h *BaseHandler) WithLevel(level LogLevel) (*BaseHandler, error)
WithLevel returns a shallow copy of BaseHandler with level set. If the level is already set, returns the original instance.
func (*BaseHandler) WithOutput ¶
func (h *BaseHandler) WithOutput(w io.Writer) (*BaseHandler, error)
WithOutput returns a new BaseHandler with output set. Error is returned for nil writer or if AtomicWriter creation fails. Current implementation only fails on nil writer, but error return is kept for future extensibility (e.g., writer validation, resource acquisition).
func (*BaseHandler) WithTrace ¶
func (h *BaseHandler) WithTrace(enabled bool) *BaseHandler
WithTrace returns a shallow copy of BaseHandler with trace flag set. If the trace flag is already set, returns the original instance.
type BaseOption ¶
type BaseOption func(*BaseOptions) error
BaseOption configures the BaseHandler.
func WithCaller ¶
func WithCaller(enabled bool) BaseOption
WithCaller enables or disables source location reporting. If enabled, the handler will include the source location of the log call site in the log record. This can be useful for debugging, but may incur a performance hit due to the additional stack frame analysis. The default value is false.
func WithSeparator ¶
func WithSeparator(separator string) BaseOption
WithSeparator sets the separator for group key prefixes.
func WithTrace ¶
func WithTrace(enabled bool) BaseOption
WithTrace enabless or disables stack traces for ERROR and above. If enabled, the handler will include the stack trace of the log call site in the log record. This can be useful for debugging, but may incur a performance hit due to the additional stack frame analysis. The default value is false.
type BaseOptions ¶
type BaseOptions struct {
Level LogLevel // Minimum log level
Output io.Writer // Output writer
// Format specifies the output format (e.g., "json", "text").
// Optional if ValidFormats is empty (handler doesn't support format selection).
// When ValidFormats is provided but Format is empty, defaults to ValidFormats[0].
Format string
// ValidFormats lists accepted format strings for this handler.
// If provided, Format must be one of these values or empty (uses first as default).
// Leave empty if handler doesn't support format configuration.
ValidFormats []string
WithCaller bool // True if caller information should be included
WithTrace bool // True if stack traces should be included
CallerSkip int // User-specified caller skip frames
Separator string // Key prefix separator (default: "_")
}
BaseOptions holds configuration common to most handlers.
type CallerAdjuster ¶
type CallerAdjuster interface {
Handler
// WithCallerSkip returns a new CallerAdjuster with the absolute
// user-visible caller skip set. Negative skip values are clamped to zero.
WithCallerSkip(skip int) CallerAdjuster
// WithCallerSkipDelta returns a new CallerAdjuster with the caller skip
// adjusted by delta.
// The delta is applied relative to the current skip. Zero delta is a no-op.
// If the new skip is negative, it is clamped to zero.
WithCallerSkipDelta(delta int) CallerAdjuster
}
CallerAdjuster is an immutable interface for handlers to expose caller adjustment.
Implementations return new CallerAdjuster instances (shallow copy semantics).
type Chainer ¶
type Chainer interface {
Handler
// WithAttrs returns a new Chainer with the given key-value pairs added.
// It returns the original handler if no key-value pairs are provided.
WithAttrs(keyValues []any) Chainer
// WithGroup returns a new Chainer that qualifies subsequent attribute keys
// with the group name. It returns the original handler if the name is empty.
WithGroup(name string) Chainer
}
Chainer extends Handler with methods for chaining log attributes.
IMPORTANT: Chainer methods return NEW handlers that SHARE mutable state (level, output) with the parent. This enables:
- Runtime reconfiguration: parent.SetLevel(Debug) affects all children
- Efficient request chains: minimal allocations for With() calls
If you need independent loggers for different modules, use AdvancedHandler.WithLevel() or create separate handler instances.
Example:
parent := logger.With("service", "api")
child := parent.With("endpoint", "/users")
parent.SetLevel(Debug) // Also affects child
Implementations return new Chainer instances (shallow copy semantics).
type ComplianceChecker ¶
type ComplianceChecker interface {
// CheckEnabled verifies that the handler correctly implements the Enabled method.
// Returns an error if the handler is not enabled at InfoLevel.
CheckEnabled(h Handler) error
// CheckHandle verifies that the handler correctly processes log records.
// Returns an error if Handle returns an error for a valid record.
CheckHandle(h Handler, r *Record) error
// CheckChainer verifies that the handler correctly implements the Chainer interface.
// Returns an error if WithAttrs or WithGroup return nil.
// This method should only be called if the handler implements Chainer.
CheckChainer(c Chainer) error
}
ComplianceChecker provides granular compliance verification for handler implementations. Most users should use ComplianceTest directly. This interface is primarily for custom test scenarios and internal testing of the compliance logic itself.
func NewComplianceChecker ¶
func NewComplianceChecker() ComplianceChecker
NewComplianceChecker returns a new compliance checker instance.
type Configurable ¶
type Configurable interface {
Handler
// WithLevel returns a new Configurable with a new minimum level applied.
// It returns the original handler if the level value is unchanged.
WithLevel(level LogLevel) Configurable
// WithOutput returns a new Configurable with the output writer set permanently.
// It returns the original handler if the writer value is unchanged.
WithOutput(w io.Writer) Configurable
}
Configurable is an immutable interface for handlers to expose configuration.
Implementations return new Configurable instances (shallow copy semantics).
type Feature ¶
type Feature uint32
Feature represents backend implementation characteristics, NOT API contracts. Use interface type assertions for API detection.
Features answer: "How does the backend work internally?" Interfaces answer: "What API methods are available?"
Example:
- FeatNativeCaller: backend accepts skip parameter (zap.AddCallerSkip)
- AdvancedHandler interface: exposes WithCallerSkip() method
A handler can implement AdvancedHandler (API) without FeatNativeCaller (backend). In this case, WithCallerSkip() would be emulated using runtime.Caller().
const ( // FeatNativeCaller: Backend supports native caller skip (e.g., zap.AddCallerSkip). // If false, unilog captures PC via runtime.Caller and passes to Record.PC. FeatNativeCaller Feature = 1 << iota // FeatNativeGroup: Backend supports native grouping (e.g., zap.Namespace, slog.WithGroup). // If false, handler must manually prefix keys using BaseHandler.keyPrefix. FeatNativeGroup // Buffers output (implements Syncer) FeatBufferedOutput // Passes context to backend (uses ctx in Handle) FeatContextPropagation // Supports SetLevel (implements MutableConfig) FeatDynamicLevel // Supports SetOutput (implements MutableConfig) FeatDynamicOutput // Backend designed for zero-allocation logging FeatZeroAlloc )
type FeatureToggler ¶
type FeatureToggler interface {
Handler
// WithCaller returns a new FeatureToggler that enables or disables caller resolution.
// It returns the original handler if the enabled value is unchanged.
// By default, caller resolution is disabled.
WithCaller(enabled bool) FeatureToggler
// WithTrace returns a new FeatureToggler that enables or disables trace logging.
// It returns the original handler if the enabled value is unchanged.
// By default, trace logging is disabled.
WithTrace(enabled bool) FeatureToggler
}
FeatureToggler is an immutable interface for handlers to expose feature toggles.
Implementations return new FeatureToggler instances (shallow copy semantics).
type Handler ¶
type Handler interface {
// Handle processes a log record. Must handle nil context gracefully.
//
// IMPORTANT: The Record pointer and its slice fields (KeyValues) are only
// valid for the duration of this call. Handlers must not retain the
// *Record pointer or alias the KeyValues slice backing array after returning.
// If retention is needed, data must be copied.
//
// Returns error only for unrecoverable failures (disk full, etc.).
Handle(ctx context.Context, record *Record) error
// Enabled reports whether the handler processes records at the given level.
// Called before building expensive Record objects.
Enabled(level LogLevel) bool
// HandlerState returns an immutable HandlerState that exposes handler state.
HandlerState() HandlerState
// Features returns the handler's supported features.
//
// MUST return a stable value; concurrent calls must be safe.
// Handler implementations should compute features once at initialization.
Features() HandlerFeatures
}
Handler is the core adapter contract that all logger implementations must satisfy.
type HandlerFeatures ¶
type HandlerFeatures struct {
// contains filtered or unexported fields
}
HandlerFeatures represents a bitmask of features supported by a handler.
func NewHandlerFeatures ¶
func NewHandlerFeatures(features Feature) HandlerFeatures
NewHandlerFeatures creates a new instance of HandlerFeatures.
func (HandlerFeatures) String ¶
func (hf HandlerFeatures) String() string
String returns a human-friendly, stable representation suitable for logging.
Example outputs:
"none" - when no features set "FeatBufferedOutput" - single feature "FeatBufferedOutput,FeatZeroAlloc" - multiple features sorted by name
Unknown/combined bits are rendered as "0x<hex>".
func (HandlerFeatures) Supports ¶
func (hf HandlerFeatures) Supports(mask Feature) bool
Supports returns true when all provided feature bits are set.
type HandlerState ¶
type HandlerState interface {
// CallerEnabled returns whether caller information should be included.
CallerEnabled() bool
// TraceEnabled returns whether stack traces should be included for error-level logs.
TraceEnabled() bool
// CallerSkip returns the current caller skip value.
CallerSkip() int
}
HandlerState is an immutable interface for handlers to expose their state.
type LevelMapper ¶
type LevelMapper[T any] struct { // contains filtered or unexported fields }
LevelMapper converts unilog levels to backend-specific levels.
func NewLevelMapper ¶
func NewLevelMapper[T any](trace, debug, info, warn, err, critical, fatal, panic T) *LevelMapper[T]
NewLevelMapper creates a mapper with the given level mappings.
func (*LevelMapper[T]) Map ¶
func (m *LevelMapper[T]) Map(level LogLevel) T
Map converts a unilog level to the backend level.
type LogLevel ¶
type LogLevel int32
LogLevel represents log severity levels.
const ( TraceLevel LogLevel = iota - 1 DebugLevel InfoLevel WarnLevel ErrorLevel CriticalLevel FatalLevel PanicLevel MaxLevel LogLevel = PanicLevel MinLevel LogLevel = TraceLevel DefaultLevel LogLevel = InfoLevel )
Log levels are ordered from least to most severe.
func ParseLevel ¶
ParseLevel converts a string to a LogLevel. It is case-insensitive. If the string is not a valid level, it returns InfoLevel and an error.
type MutableConfig ¶
type MutableConfig interface {
Handler
// SetLevel changes the minimum log level that will be processed.
SetLevel(level LogLevel) error
// SetOutput changes the destination for log output.
SetOutput(w io.Writer) error
}
MutableConfig enables mutable runtime reconfiguration of handler settings. Implementing this interface implies the Handler is mutable, allowing for high-speed atomic updates (e.g., level changes) without allocations. This performance gain comes at the cost of immutability; implementers must ensure these methods are safe for concurrent use.
type Record ¶
type Record struct {
// Time is the timestamp of the log entry.
Time time.Time
// Level is the log level of the log entry.
Level LogLevel
// Message is the log message.
Message string
// KeyValues is the list of attributes associated with the log entry.
KeyValues []any
// PC is the program counter for source location (0 if unavailable).
// It will be used for loggers that don't support source location natively.
PC uintptr
// Skip is the number of stack frames to skip for source location.
// It will be used for loggers that support source location natively.
Skip int
}
Record represents a single log entry with structured attributes.