handler

package
v0.0.0-...-8a7572f Latest Latest
Warning

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

Go to latest
Published: May 26, 2026 License: MIT Imports: 12 Imported by: 0

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

Constants

This section is empty.

Variables

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

func (h *AsyncHandler) Enabled(ctx context.Context, level slog.Level) bool

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

func (h *AsyncHandler) Handle(ctx context.Context, r slog.Record) error

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

func (h *ModuleHandler) Enabled(ctx context.Context, level slog.Level) bool

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

func (h *ModuleHandler) Handle(ctx context.Context, r slog.Record) error

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

func (m *MultiHandler) Enabled(ctx context.Context, level slog.Level) bool

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

func (m *MultiHandler) Handle(ctx context.Context, r slog.Record) error

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

func WithRedactFunc(fn func(groups []string, key string, value slog.Value) slog.Value) RedactOption

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:

  1. Key-based: exact key names or dotted group paths (e.g., "password", "auth.token")
  2. Pattern-based: regular expression matching against full key paths
  3. 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

func (h *RedactionHandler) Enabled(ctx context.Context, level slog.Level) bool

Enabled reports whether the inner handler is enabled for the given level.

func (*RedactionHandler) Handle

func (h *RedactionHandler) Handle(ctx context.Context, r slog.Record) error

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

func (h *SamplingHandler) Enabled(ctx context.Context, level slog.Level) bool

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

func (h *SamplingHandler) Handle(ctx context.Context, r slog.Record) error

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.

Jump to

Keyboard shortcuts

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