handler

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 4, 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.

The inner handler must be safe for concurrent use by multiple goroutines (the standard slog.Handler contract, satisfied by all standard library handlers): bypass-level and SyncFallback writes happen on caller goroutines concurrently with the background worker. AsyncHandler does not add its own serialization around the inner handler.

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. A flush that races with [CloseContext] may return ErrHandlerClosed; the close path itself drains all buffered records, so nothing is lost.

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 to ensure critical logs are never lost or delayed. Note that this can reorder output: a bypass-level record may appear in the output before lower-level records that were logged earlier but are still queued in the async buffer. 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. The trade-off is ordering: a bypassed record can appear in the output before lower-level records that were logged earlier but are still queued in the buffer. If strict output ordering matters more than immediacy, set the bypass level above your highest level so every record goes through the queue.

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.

func (*ModuleConfig) SetLevels

func (c *ModuleConfig) SetLevels(spec string) error

SetLevels parses and applies a comma-separated level specification:

"database=debug,auth=warn,*=info"

Each segment is component=level. The special component "*" sets the default level. Level names are parsed case-insensitively via slog.Level.UnmarshalText ("debug", "info", "warn", "error", including offset forms like "info+2"). Whitespace around segments is trimmed and empty segments are ignored, so trailing commas are harmless.

The whole spec is validated before anything is applied: on error, no levels are changed.

This makes runtime level control easy to wire to an environment variable, config file, or admin endpoint:

if spec := os.Getenv("LOG_LEVELS"); spec != "" {
    if err := config.SetLevels(spec); err != nil { ... }
}

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.

If config is nil, a default ModuleConfig with slog.LevelInfo is used. Note that such a config is not reachable by the caller, so per-component levels cannot be changed later; pass an explicit config to control levels at runtime.

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's middleware chain is traversed via Unwrap(), so closeable handlers wrapped by pass-through middleware (e.g., a RedactionHandler wrapping an AsyncHandler) are closed as well. At each level, ContextCloser is preferred over Closer. Errors 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's middleware chain is traversed via Unwrap(), so flushable handlers wrapped by pass-through middleware are flushed as well. At each level, ContextFlusher is preferred over Flusher. Errors 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.

Each child receives its own copy of the record so that no mutable state is shared between children. The last enabled child receives the original record, since ownership of r ends here; with a single enabled child no clone is made at all.

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]".

Matching uses substring semantics (regexp.Regexp.MatchString): the pattern "token" also redacts "token_count" and "auth.tokens". This errs on the side of over-redaction; anchor the pattern (e.g., `(^|\.)token$`) to match a key exactly.

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.

Limitations

RedactionHandler inspects attribute keys only. Sensitive data can still leak through paths it does not see:

  • The record message is never inspected. Secrets interpolated into the message (e.g., fmt.Sprintf) are logged as-is.
  • Values inside slog.Any attributes (maps, structs, slices) are not recursed into — only slog.Group attributes are. An attribute like slog.Any("creds", map[string]string{"password": "x"}) passes through unredacted unless its top-level key matches.

For values like these, prefer self-redacting types such as Redacted and SensitiveBytes from the root go-logger package, which hide their contents regardless of handler configuration.

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.

When no keys, patterns, or redact function are configured, the record is forwarded unchanged.

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