handler

package
v0.0.0-...-5973f6b Latest Latest
Warning

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

Go to latest
Published: Nov 24, 2025 License: MIT Imports: 12 Imported by: 0

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

View Source
const DefaultKeySeparator = "_"

DefaultKeySeparator is the default separator for group key prefixes.

Variables

View Source
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

func ComplianceTest(t *testing.T, newHandler func() (Handler, error))

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

func IsValidLogLevel(level LogLevel) bool

IsValidLogLevel returns true if the given log level is valid.

func NewAtomicWriterError

func NewAtomicWriterError(err error) error

NewAtomicWriterError returns an error wrapping ErrAtomicWriterFail.

func NewInvalidFormatError

func NewInvalidFormatError(format string, accepted []string) error

NewInvalidFormatError returns an error wrapping ErrInvalidFormat.

func NewInvalidLogLevelError

func NewInvalidLogLevelError(level LogLevel) error

NewInvalidLogLevelError returns an error wrapping ErrInvalidLogLevel.

func NewOptionApplyError

func NewOptionApplyError(option string, err error) error

NewOptionApplyError returns an error wrapping ErrOptionApplyFailed.

func ValidateLogLevel

func ValidateLogLevel(level LogLevel) error

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:

  1. sync.RWMutex (mu) protects format, callerSkip, keyPrefix, separator
  2. 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:

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 WithFormat

func WithFormat(format string) BaseOption

WithFormat sets the output format.

func WithLevel

func WithLevel(level LogLevel) BaseOption

WithLevel sets the minimum log level.

func WithOutput

func WithOutput(w io.Writer) BaseOption

WithOutput sets the output writer.

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
)

func (Feature) String

func (f Feature) String() string

String returns a stable name for a single Feature bit. If the feature value does not match a known single-bit feature it returns "0x<hex>".

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

func ParseLevel(levelStr string) (LogLevel, error)

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.

func (LogLevel) String

func (l LogLevel) String() string

String returns a human-readable representation of the log level.

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.

type StateFlag

type StateFlag uint32

StateFlag is a set of flags used to track handler state.

const (
	FlagCaller StateFlag = 1 << iota // Enable caller location reporting
	FlagTrace                        // Enable stack trace reporting for ERROR and above
)

type Syncer

type Syncer interface {
	Handler

	// Sync flushes buffered log entries. Returns error on flush failure.
	Sync() error
}

Syncer flushes any buffered log entries.

Directories

Path Synopsis
log15 module
logrus module
slog module
stdlog module
zap module
zerolog module

Jump to

Keyboard shortcuts

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