Documentation
¶
Overview ¶
Package handler provides composable slog.Handler middleware for the go-logger library.
All handlers in this package implement the slog.Handler interface and follow the immutable clone pattern: slog.Handler.WithAttrs and slog.Handler.WithGroup always return new handler instances without modifying the receiver.
Handlers can be composed in any order to build a processing pipeline:
base := slog.NewJSONHandler(os.Stdout, nil)
h := handler.NewRedactionHandler(
handler.NewAsyncHandler(base),
handler.WithRedactKeys("password"),
)
log := slog.New(h)
Index ¶
- Variables
- type AsyncHandler
- func (h *AsyncHandler) Close() error
- func (h *AsyncHandler) CloseContext(ctx context.Context) error
- func (h *AsyncHandler) DroppedCount() uint64
- func (h *AsyncHandler) Enabled(ctx context.Context, level slog.Level) bool
- func (h *AsyncHandler) Flush() error
- func (h *AsyncHandler) FlushContext(ctx context.Context) error
- func (h *AsyncHandler) Handle(ctx context.Context, r slog.Record) error
- func (h *AsyncHandler) Stats() AsyncStats
- func (h *AsyncHandler) Unwrap() slog.Handler
- func (h *AsyncHandler) WithAttrs(attrs []slog.Attr) slog.Handler
- func (h *AsyncHandler) WithGroup(name string) slog.Handler
- type AsyncOption
- type AsyncStats
- type DropPolicy
- type ModuleConfig
- type ModuleHandler
- func (h *ModuleHandler) Enabled(ctx context.Context, level slog.Level) bool
- func (h *ModuleHandler) Handle(ctx context.Context, r slog.Record) error
- func (h *ModuleHandler) Unwrap() slog.Handler
- func (h *ModuleHandler) WithAttrs(attrs []slog.Attr) slog.Handler
- func (h *ModuleHandler) WithGroup(name string) slog.Handler
- type MultiHandler
- func (m *MultiHandler) CloseContext(ctx context.Context) error
- func (m *MultiHandler) Enabled(ctx context.Context, level slog.Level) bool
- func (m *MultiHandler) FlushContext(ctx context.Context) error
- func (m *MultiHandler) Handle(ctx context.Context, r slog.Record) error
- func (m *MultiHandler) WithAttrs(attrs []slog.Attr) slog.Handler
- func (m *MultiHandler) WithGroup(name string) slog.Handler
- type RedactOption
- type RedactionHandler
- func (h *RedactionHandler) Enabled(ctx context.Context, level slog.Level) bool
- func (h *RedactionHandler) Handle(ctx context.Context, r slog.Record) error
- func (h *RedactionHandler) Unwrap() slog.Handler
- func (h *RedactionHandler) WithAttrs(attrs []slog.Attr) slog.Handler
- func (h *RedactionHandler) WithGroup(name string) slog.Handler
- type SampleOption
- type SampleStats
- type SamplingHandler
- func (h *SamplingHandler) Enabled(ctx context.Context, level slog.Level) bool
- func (h *SamplingHandler) Handle(ctx context.Context, r slog.Record) error
- func (h *SamplingHandler) SetRate(rate float64)
- func (h *SamplingHandler) Stats() SampleStats
- func (h *SamplingHandler) Unwrap() slog.Handler
- func (h *SamplingHandler) WithAttrs(attrs []slog.Attr) slog.Handler
- func (h *SamplingHandler) WithGroup(name string) slog.Handler
Constants ¶
This section is empty.
Variables ¶
var ErrHandlerClosed = errors.New("go-logger: handler is closed")
ErrHandlerClosed is returned by AsyncHandler.Handle after the handler has been closed via AsyncHandler.Close.
Functions ¶
This section is empty.
Types ¶
type AsyncHandler ¶
type AsyncHandler struct {
// contains filtered or unexported fields
}
AsyncHandler buffers log records in a channel and processes them in a background worker goroutine, decoupling log production from log I/O.
Features:
- Configurable buffer size for burst absorption
- Three drop policies: DropNewest, Block, SyncFallback
- Bypass level for synchronous writes of critical records
- Deterministic [Flush] via barrier channel
- Idempotent [Close] via sync.Once
- Dropped record counting via atomic counter
Records are deep-copied via record.CloneRecord before being sent to the channel to prevent use-after-return bugs with stack-allocated attributes.
AsyncHandler implements slog.Handler, [Closer], and [Flusher].
func NewAsyncHandler ¶
func NewAsyncHandler(inner slog.Handler, opts ...AsyncOption) *AsyncHandler
NewAsyncHandler creates an AsyncHandler that wraps the given inner handler with asynchronous record processing.
A background worker goroutine is started immediately. The caller must call AsyncHandler.Close to stop the worker and release resources.
Defaults: bufferSize=1024, dropPolicy=[DropNewest], bypassLevel=[slog.LevelError].
func (*AsyncHandler) Close ¶
func (h *AsyncHandler) Close() error
Close stops the background worker and waits for it to finish processing all remaining buffered records.
Close delegates to [CloseContext] with a background context.
func (*AsyncHandler) CloseContext ¶
func (h *AsyncHandler) CloseContext(ctx context.Context) error
CloseContext stops the background worker and waits for it to finish processing all remaining buffered records.
CloseContext is idempotent: calling it multiple times is safe and subsequent calls return nil.
CloseContext takes an exclusive lock on acceptMu to ensure no new records can be enqueued after closed is set. This eliminates the race window between Handle checking closed and CloseContext setting it.
The context can be used to set a deadline for the drain operation. After CloseContext returns, any further calls to Handle return ErrHandlerClosed.
func (*AsyncHandler) DroppedCount ¶
func (h *AsyncHandler) DroppedCount() uint64
DroppedCount returns the total number of records dropped due to a full buffer when using the DropNewest policy.
func (*AsyncHandler) Enabled ¶
Enabled reports whether the inner handler is enabled for the given level.
func (*AsyncHandler) Flush ¶
func (h *AsyncHandler) Flush() error
Flush ensures all records submitted before this call have been written to the inner handler.
Flush delegates to [FlushContext] with a background context.
func (*AsyncHandler) FlushContext ¶
func (h *AsyncHandler) FlushContext(ctx context.Context) error
FlushContext ensures all records submitted before this call have been written to the inner handler.
FlushContext uses a deterministic barrier: it sends a sentinel item through the channel and waits for the worker to acknowledge processing it. This guarantees that all previously enqueued records have been written.
The barrier channel is buffered (cap 1) and the worker uses a non-blocking send to acknowledge. This prevents the worker from deadlocking if the caller's context times out after the barrier is enqueued but before the worker sends the ack.
The context can be used to set a deadline or cancel the flush operation. Returns an error if the handler is closed or the context is cancelled.
func (*AsyncHandler) Handle ¶
Handle sends the record to the background worker for processing.
Records at or above the bypass level are written synchronously (under mutex) to ensure critical logs are never lost or delayed. Bypass and SyncFallback writes are counted in [AsyncStats.Written] (or [AsyncStats.Errors] on failure).
The record is deep-copied before being buffered to prevent use-after-return bugs with stack-allocated attributes.
The original context is preserved and forwarded to the inner handler in the background worker, maintaining context values (e.g., trace spans).
Returns ErrHandlerClosed if the handler has been closed.
func (*AsyncHandler) Stats ¶
func (h *AsyncHandler) Stats() AsyncStats
Stats returns a snapshot of the handler's runtime statistics.
Written includes all successful writes: background worker, bypass-level synchronous writes, and SyncFallback writes. Errors includes all write failures across all paths.
func (*AsyncHandler) Unwrap ¶
func (h *AsyncHandler) Unwrap() slog.Handler
Unwrap returns the inner handler, enabling lifecycle traversal.
func (*AsyncHandler) WithAttrs ¶
func (h *AsyncHandler) WithAttrs(attrs []slog.Attr) slog.Handler
WithAttrs returns a new AsyncHandler that shares the same async infrastructure (channel, worker) but wraps a child inner handler with the given attributes.
func (*AsyncHandler) WithGroup ¶
func (h *AsyncHandler) WithGroup(name string) slog.Handler
WithGroup returns a new AsyncHandler that shares the same async infrastructure but wraps a child inner handler with the given group.
type AsyncOption ¶
type AsyncOption func(*asyncOptions)
AsyncOption configures an AsyncHandler.
func WithAsyncBypassLevel ¶
func WithAsyncBypassLevel(level slog.Level) AsyncOption
WithAsyncBypassLevel sets the level at or above which records are written synchronously, bypassing the async buffer entirely.
This ensures critical records (e.g., Error, Fatal) are written immediately even if the buffer is full or the worker is behind.
Default: slog.LevelError.
func WithBufferSize ¶
func WithBufferSize(size int) AsyncOption
WithBufferSize sets the size of the internal record channel buffer.
Larger buffers absorb more bursts but consume more memory. Default: 1024.
func WithDropPolicy ¶
func WithDropPolicy(policy DropPolicy) AsyncOption
WithDropPolicy sets the behavior when the buffer is full.
Default: DropNewest.
type AsyncStats ¶
type AsyncStats struct {
// Written is the total number of records successfully written by the
// background worker.
Written uint64
// Dropped is the total number of records dropped due to a full buffer
// when using the [DropNewest] policy.
Dropped uint64
// Errors is the total number of errors returned by the inner handler
// during background processing.
Errors uint64
// QueueLen is the current number of items waiting in the async buffer.
QueueLen int
}
AsyncStats holds runtime statistics for an AsyncHandler.
All counters are accumulated since handler creation and read atomically.
type DropPolicy ¶
type DropPolicy int
DropPolicy defines the behavior when the async buffer is full.
const ( // DropNewest drops the new record when the buffer is full. // The record is silently discarded and [AsyncHandler.DroppedCount] is incremented. DropNewest DropPolicy = iota // Block blocks the calling goroutine until space is available in the buffer. // This provides backpressure to the caller but may impact latency. Block // SyncFallback writes the record synchronously to the inner handler when // the buffer is full. This ensures no records are lost but bypasses the // async path, so the caller blocks for the duration of the write. SyncFallback )
type ModuleConfig ¶
type ModuleConfig struct {
// contains filtered or unexported fields
}
ModuleConfig holds per-component log level configuration for ModuleHandler.
Each component is identified by a string name (e.g., "networking", "storage", "consensus") and has its own *slog.LevelVar that can be changed at runtime.
Components not explicitly configured use the default level.
ModuleConfig is safe for concurrent use.
func NewModuleConfig ¶
func NewModuleConfig(defaultLevel slog.Level) *ModuleConfig
NewModuleConfig creates a ModuleConfig with the given default log level.
The default level applies to any component not explicitly configured via ModuleConfig.SetLevel.
func (*ModuleConfig) SetDefaultLevel ¶
func (c *ModuleConfig) SetDefaultLevel(level slog.Level)
SetDefaultLevel updates the default log level for components not explicitly configured.
This method is safe for concurrent use.
func (*ModuleConfig) SetLevel ¶
func (c *ModuleConfig) SetLevel(component string, level slog.Level)
SetLevel sets the log level for a specific component.
If the component already has a configured level, it is updated in place (all loggers using this component will see the change immediately).
If the component has not been configured before, a new *slog.LevelVar is created.
This method is safe for concurrent use.
type ModuleHandler ¶
type ModuleHandler struct {
// contains filtered or unexported fields
}
ModuleHandler filters log records based on per-component log level configuration.
The component name is resolved from the "component" attribute, which can be set via [logger.Component]. Resolution checks record attributes first, then falls back to pre-applied attributes from slog.Handler.WithAttrs.
This allows code like:
log := slog.New(handler).With(logger.Component("networking"))
log.Debug("low-level detail") // filtered based on "networking" level config
ModuleHandler implements slog.Handler and follows the immutable clone pattern for slog.Handler.WithAttrs and slog.Handler.WithGroup.
func NewModuleHandler ¶
func NewModuleHandler(inner slog.Handler, config *ModuleConfig) *ModuleHandler
NewModuleHandler creates a ModuleHandler that wraps the given inner handler with per-component log level filtering.
The ModuleConfig is shared across all clones created by [WithAttrs] and [WithGroup], enabling runtime level changes to take effect immediately.
func (*ModuleHandler) Enabled ¶
Enabled reports whether this handler would log a record at the given level.
When a component name has been resolved via [WithAttrs] (e.g., from log.With(logger.Component("database"))), Enabled uses the cached component to perform an efficient per-module level check. This avoids creating a record that will be filtered in Handle.
When no component is cached, Enabled uses the most permissive level across all configured modules (via [ModuleConfig.minLevel]). This ensures that log records where the component is only provided as a log-call attribute (e.g., log.Debug("msg", "component", "database")) can still reach Handle for proper component-level filtering.
For best performance, attach the component via .With(logger.Component(...)) so that Enabled can perform an exact module-level check.
func (*ModuleHandler) Handle ¶
Handle resolves the component name from the record's attributes and applies per-component level filtering. If the record passes, it is forwarded to the inner handler.
func (*ModuleHandler) Unwrap ¶
func (h *ModuleHandler) Unwrap() slog.Handler
Unwrap returns the inner handler, enabling lifecycle traversal.
func (*ModuleHandler) WithAttrs ¶
func (h *ModuleHandler) WithAttrs(attrs []slog.Attr) slog.Handler
WithAttrs returns a new ModuleHandler where the inner handler has been cloned with the given attributes.
If a "component" attribute is found (either in the new attrs or in the accumulated preAttrs), its value is cached in the clone for efficient Enabled() checks. This makes log.With(logger.Component("database")) the recommended pattern for module filtering.
func (*ModuleHandler) WithGroup ¶
func (h *ModuleHandler) WithGroup(name string) slog.Handler
WithGroup returns a new ModuleHandler where the inner handler has been cloned with the given group name.
The cached component is preserved across group boundaries.
type MultiHandler ¶
type MultiHandler struct {
// contains filtered or unexported fields
}
MultiHandler fans out log records to multiple slog.Handler implementations simultaneously.
Each record is dispatched to every child handler whose slog.Handler.Enabled method returns true for the record's level. Errors from individual handlers are aggregated using errors.Join.
MultiHandler follows the immutable clone pattern: MultiHandler.WithAttrs and MultiHandler.WithGroup return new instances wrapping cloned children, ensuring concurrency safety.
func NewMultiHandler ¶
func NewMultiHandler(handlers ...slog.Handler) *MultiHandler
NewMultiHandler creates a MultiHandler that fans out to the given handlers.
At least one handler should be provided. If zero handlers are given, the resulting handler will silently discard all records.
func (*MultiHandler) CloseContext ¶
func (m *MultiHandler) CloseContext(ctx context.Context) error
CloseContext closes all child handlers that implement lifecycle interfaces.
Each child is checked for [ContextCloser] first, then [Closer]. Errors from individual children are aggregated using errors.Join. A failure in one child does not prevent other children from being closed.
func (*MultiHandler) Enabled ¶
Enabled reports whether ANY child handler is enabled for the given level.
This uses OR semantics: if at least one child handler would accept a record at this level, Enabled returns true. This ensures no records are dropped prematurely — individual handlers perform their own level checks in Handle.
func (*MultiHandler) FlushContext ¶
func (m *MultiHandler) FlushContext(ctx context.Context) error
FlushContext flushes all child handlers that implement lifecycle interfaces.
Each child is checked for [ContextFlusher] first, then [Flusher]. Errors from individual children are aggregated using errors.Join. A failure in one child does not prevent other children from being flushed.
func (*MultiHandler) Handle ¶
Handle dispatches the record to every child handler that is enabled for the record's level.
Errors from individual handlers are collected and returned as a single error using errors.Join. A failure in one handler does not prevent dispatch to other handlers.
func (*MultiHandler) WithAttrs ¶
func (m *MultiHandler) WithAttrs(attrs []slog.Attr) slog.Handler
WithAttrs returns a new MultiHandler where each child handler has been cloned with the given attributes.
The original MultiHandler and its children are not modified.
func (*MultiHandler) WithGroup ¶
func (m *MultiHandler) WithGroup(name string) slog.Handler
WithGroup returns a new MultiHandler where each child handler has been cloned with the given group name.
The original MultiHandler and its children are not modified.
type RedactOption ¶
type RedactOption func(*redactOptions)
RedactOption configures a RedactionHandler.
func WithRedactFunc ¶
WithRedactFunc sets a custom function for redacting attribute values.
The function receives the current group path, the attribute key, and its value. It should return the (possibly modified) value. To redact, return slog.StringValue("[REDACTED]").
The function is called after key-based and pattern-based checks, so it acts as an additional layer of redaction.
func WithRedactKeys ¶
func WithRedactKeys(keys ...string) RedactOption
WithRedactKeys specifies attribute keys whose values should be replaced with "[REDACTED]".
Keys can be simple names ("password", "token") or dotted paths for nested groups ("auth.token", "db.password", "request.headers.authorization").
Matching is case-sensitive and exact.
func WithRedactPatterns ¶
func WithRedactPatterns(patterns ...string) RedactOption
WithRedactPatterns specifies regular expression patterns for key matching.
Any attribute whose full key path (e.g., "auth.token") matches any pattern will have its value replaced with "[REDACTED]".
Patterns are compiled once at handler construction time. Invalid patterns cause a panic.
type RedactionHandler ¶
type RedactionHandler struct {
// contains filtered or unexported fields
}
RedactionHandler inspects and redacts sensitive attributes from log records before passing them to the inner handler.
It supports three complementary redaction strategies:
- Key-based: exact key names or dotted group paths (e.g., "password", "auth.token")
- Pattern-based: regular expression matching against full key paths
- Function-based: custom logic for context-dependent redaction
RedactionHandler correctly handles nested slog.Group attributes by recursively inspecting group values and tracking the current group path.
RedactionHandler implements slog.Handler and follows the immutable clone pattern for slog.Handler.WithAttrs and slog.Handler.WithGroup.
func NewRedactionHandler ¶
func NewRedactionHandler(inner slog.Handler, opts ...RedactOption) *RedactionHandler
NewRedactionHandler creates a RedactionHandler that wraps the given inner handler with the specified redaction configuration.
Panics if any pattern in WithRedactPatterns is not a valid regular expression.
func (*RedactionHandler) Enabled ¶
Enabled reports whether the inner handler is enabled for the given level.
func (*RedactionHandler) Handle ¶
Handle creates a new slog.Record with redacted attributes and passes it to the inner handler.
All attributes on the record are inspected and potentially redacted. Group attributes are recursively inspected with proper path tracking.
func (*RedactionHandler) Unwrap ¶
func (h *RedactionHandler) Unwrap() slog.Handler
Unwrap returns the inner handler, enabling lifecycle traversal.
func (*RedactionHandler) WithAttrs ¶
func (h *RedactionHandler) WithAttrs(attrs []slog.Attr) slog.Handler
WithAttrs returns a new RedactionHandler where the inner handler has been cloned with the given attributes (after redacting them).
The original handler is not modified.
func (*RedactionHandler) WithGroup ¶
func (h *RedactionHandler) WithGroup(name string) slog.Handler
WithGroup returns a new RedactionHandler with the given group name appended to the current group path. The inner handler is also cloned with the group.
The original handler is not modified.
type SampleOption ¶
type SampleOption func(*sampleOptions)
SampleOption configures a SamplingHandler.
func WithSampleByLevel ¶
func WithSampleByLevel(rates map[slog.Level]float64) SampleOption
WithSampleByLevel sets per-level sampling rates.
Levels not present in the map use the default rate (set via WithSampleRate, default 1.0). The bypass level (default Error) always keeps all records regardless of the rate specified here.
func WithSampleBypassLevel ¶
func WithSampleBypassLevel(level slog.Level) SampleOption
WithSampleBypassLevel sets the level at or above which sampling is bypassed and all records are kept.
Default: slog.LevelError. Set to a very high value to disable bypass.
func WithSampleRate ¶
func WithSampleRate(rate float64) SampleOption
WithSampleRate sets the global sampling rate applied to all levels (unless overridden by WithSampleByLevel).
Rate must be between 0.0 (drop all) and 1.0 (keep all). Values outside this range are clamped.
type SampleStats ¶
type SampleStats struct {
// Passed is the total number of records that passed the sampling check
// and were forwarded to the inner handler.
Passed uint64
// Dropped is the total number of records that were sampled out and
// silently discarded.
Dropped uint64
}
SampleStats holds runtime statistics for a SamplingHandler.
All counters are accumulated since handler creation and read atomically.
type SamplingHandler ¶
type SamplingHandler struct {
// contains filtered or unexported fields
}
SamplingHandler applies probabilistic or per-level sampling to log records, allowing high-volume logging in production without overwhelming storage or processing systems.
Records at or above the bypass level (default: slog.LevelError) are never sampled — they always pass through. This ensures critical logs are never lost.
SamplingHandler implements slog.Handler and follows the immutable clone pattern for slog.Handler.WithAttrs and slog.Handler.WithGroup.
The sampling decision uses math/rand/v2, which is safe for concurrent use without additional synchronization.
func NewSamplingHandler ¶
func NewSamplingHandler(inner slog.Handler, opts ...SampleOption) *SamplingHandler
NewSamplingHandler creates a SamplingHandler that wraps the given inner handler with the specified sampling configuration.
Defaults: rate=1.0 (keep all), bypassLevel=slog.LevelError.
func (*SamplingHandler) Enabled ¶
Enabled reports whether the inner handler is enabled for the given level.
SamplingHandler does not filter in Enabled — the sampling decision happens in Handle to avoid losing records before they can be evaluated. If the inner handler would not log at this level, Enabled returns false immediately.
func (*SamplingHandler) Handle ¶
Handle applies the sampling decision and, if the record passes, delegates to the inner handler.
Records at or above the bypass level always pass through. Other records are sampled based on their level-specific rate (if configured) or the default rate.
func (*SamplingHandler) SetRate ¶
func (h *SamplingHandler) SetRate(rate float64)
SetRate updates the default sampling rate at runtime.
This is safe for concurrent use. The new rate takes effect on the next sampling decision. Rate is clamped to [0.0, 1.0].
func (*SamplingHandler) Stats ¶
func (h *SamplingHandler) Stats() SampleStats
Stats returns a snapshot of the handler's runtime statistics.
func (*SamplingHandler) Unwrap ¶
func (h *SamplingHandler) Unwrap() slog.Handler
Unwrap returns the inner handler, enabling lifecycle traversal.
func (*SamplingHandler) WithAttrs ¶
func (h *SamplingHandler) WithAttrs(attrs []slog.Attr) slog.Handler
WithAttrs returns a new SamplingHandler where the inner handler has been cloned with the given attributes.
The sampling configuration is shared (read-only) across all clones.
func (*SamplingHandler) WithGroup ¶
func (h *SamplingHandler) WithGroup(name string) slog.Handler
WithGroup returns a new SamplingHandler where the inner handler has been cloned with the given group name.