Documentation
¶
Overview ¶
Package loglayer is a transport-agnostic structured logger with a fluent builder API. The core defines the LogLayer type, the Transport and Plugin interfaces, and the dispatch pipeline. Concrete transports (zap, zerolog, slog, charmlog, OTel, etc.) ship as separately-versioned sub-modules under go.loglayer.dev/transports/<name>/v2.
Full docs: https://go.loglayer.dev
Quickstart ¶
import (
"go.loglayer.dev/v2"
"go.loglayer.dev/transports/structured/v2"
)
log := loglayer.New(loglayer.Config{
Transport: structured.New(structured.Config{}),
})
log.WithFields(loglayer.Fields{"requestId": "abc"}).
WithMetadata(loglayer.Metadata{"durationMs": 42}).
Info("served")
Three data shapes ¶
LogLayer separates persistent from per-call data on purpose. Pick the one whose lifetime matches the data:
- Fields (map[string]any): persistent on the logger. Set once via WithFields and it appears on every subsequent log entry. Use for request IDs, user IDs, and anything request-scoped.
- Metadata (any): single log call only. Use for per-event payloads such as durations, counters, or structs. Maps merge at the entry root; other values nest under Config.MetadataFieldName.
- Context (context.Context): single log call only. Transports that understand context (OTel, slog) read trace IDs and deadlines from it; others ignore it.
Choosing a constructor ¶
- New panics on misconfiguration. Use at program start when failure means the binary cannot run.
- Build returns (*LogLayer, error). Use when the config is loaded at runtime (env vars, config file) and the caller wants to handle ErrNoTransport, ErrTransportAndTransports, or ErrUngroupedTransportsWithoutMode via errors.Is.
- NewMock returns a silent logger that accepts every call and emits nothing. Use in tests that inject a *LogLayer.
Concurrency ¶
Every method on *LogLayer is safe to call from any goroutine, including concurrently with emission. Fluent chain methods (WithFields, WithoutFields, Child, WithPrefix, WithGroup, WithContext) return a new logger and never mutate the receiver. Level, transport, plugin, and group mutators are atomic and intended for live runtime toggling. See the per-method GoDoc for the exact class.
Authoring transports and plugins ¶
Transport and plugin implementations live in their own sub-modules. The transport/ package exports BaseTransport, BaseConfig, and shared helpers (JoinMessages, MetadataAsMap, MergeFieldsAndMetadata) for authors. See https://go.loglayer.dev for the authoring guides.
Example ¶
Construct a logger and chain persistent fields and per-call metadata onto a single log entry. The exampleTransport defined in this file emits a deterministic JSON line; production code uses one of the transports under go.loglayer.dev/transports/<name>.
log := loglayer.New(loglayer.Config{
Transport: exampleTransport{},
DisableFatalExit: true,
})
log.WithFields(loglayer.Fields{"requestId": "abc"}).
WithMetadata(loglayer.Metadata{"durationMs": 42}).
Info("served")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"served","durationMs":42,"requestId":"abc"}
Index ¶
- Constants
- Variables
- func ActiveGroupsFromEnv(name string) []string
- func NewContext(parent context.Context, log *LogLayer) context.Context
- func UnwrappingErrorSerializer(err error) map[string]any
- type BeforeDataOutParams
- type BeforeMessageOutParams
- type Config
- type CopyMsgPolicy
- type Data
- type DataHook
- type ErrorOnlyOpts
- type ErrorReporter
- type ErrorSerializer
- type F
- type Fields
- type FieldsHook
- type LazyValue
- type LevelHook
- type LogBuilder
- func (b *LogBuilder) Debug(messages ...any)
- func (b *LogBuilder) Error(messages ...any)
- func (b *LogBuilder) Fatal(messages ...any)
- func (b *LogBuilder) Info(messages ...any)
- func (b *LogBuilder) Panic(messages ...any)
- func (b *LogBuilder) Trace(messages ...any)
- func (b *LogBuilder) Warn(messages ...any)
- func (b *LogBuilder) WithContext(ctx context.Context) *LogBuilder
- func (b *LogBuilder) WithError(err error) *LogBuilder
- func (b *LogBuilder) WithGroup(groups ...string) *LogBuilder
- func (b *LogBuilder) WithMetadata(v any) *LogBuilder
- type LogGroup
- type LogLayer
- func (l *LogLayer) AddGroup(name string, group LogGroup) *LogLayer
- func (l *LogLayer) AddPlugin(plugins ...Plugin) *LogLayer
- func (l *LogLayer) AddTransport(transports ...Transport) *LogLayer
- func (l *LogLayer) Child() *LogLayer
- func (l *LogLayer) ClearActiveGroups() *LogLayer
- func (l *LogLayer) Debug(messages ...any)
- func (l *LogLayer) DisableGroup(name string) *LogLayer
- func (l *LogLayer) DisableLevel(level LogLevel) *LogLayer
- func (l *LogLayer) DisableLogging() *LogLayer
- func (l *LogLayer) EnableGroup(name string) *LogLayer
- func (l *LogLayer) EnableLevel(level LogLevel) *LogLayer
- func (l *LogLayer) EnableLogging() *LogLayer
- func (l *LogLayer) Error(messages ...any)
- func (l *LogLayer) ErrorOnly(err error, opts ...ErrorOnlyOpts)
- func (l *LogLayer) Fatal(messages ...any)
- func (l *LogLayer) GetFields() Fields
- func (l *LogLayer) GetGroups() map[string]LogGroup
- func (l *LogLayer) GetLoggerInstance(id string) any
- func (l *LogLayer) GetPlugin(id string) (Plugin, bool)
- func (l *LogLayer) Info(messages ...any)
- func (l *LogLayer) IsLevelEnabled(level LogLevel) bool
- func (l *LogLayer) MetadataOnly(v any, opts ...MetadataOnlyOpts)
- func (l *LogLayer) MuteFields() *LogLayer
- func (l *LogLayer) MuteMetadata() *LogLayer
- func (l *LogLayer) NewLogLogger(level LogLevel) *log.Logger
- func (l *LogLayer) Panic(messages ...any)
- func (l *LogLayer) PluginCount() int
- func (l *LogLayer) Raw(entry RawLogEntry)
- func (l *LogLayer) RemoveGroup(name string) bool
- func (l *LogLayer) RemovePlugin(id string) bool
- func (l *LogLayer) RemoveTransport(id string) bool
- func (l *LogLayer) SetActiveGroups(first string, more ...string) *LogLayer
- func (l *LogLayer) SetGroupLevel(name string, level LogLevel) *LogLayer
- func (l *LogLayer) SetLevel(level LogLevel) *LogLayer
- func (l *LogLayer) SetTransports(transports ...Transport) *LogLayer
- func (l *LogLayer) Trace(messages ...any)
- func (l *LogLayer) UnmuteFields() *LogLayer
- func (l *LogLayer) UnmuteMetadata() *LogLayer
- func (l *LogLayer) Warn(messages ...any)
- func (l *LogLayer) WithContext(ctx context.Context) *LogLayer
- func (l *LogLayer) WithError(err error) *LogBuilder
- func (l *LogLayer) WithFields(f Fields) *LogLayer
- func (l *LogLayer) WithGroup(groups ...string) *LogLayer
- func (l *LogLayer) WithMetadata(v any) *LogBuilder
- func (l *LogLayer) WithPrefix(prefix string) *LogLayer
- func (l *LogLayer) WithoutFields(keys ...string) *LogLayer
- func (l *LogLayer) Writer(level LogLevel) io.Writer
- type LogLevel
- type M
- type MessageHook
- type Metadata
- type MetadataHook
- type MetadataOnlyOpts
- type MultilineMessage
- type Plugin
- func NewDataHook(id string, fn func(BeforeDataOutParams) Data) Plugin
- func NewFieldsHook(id string, fn func(Fields) Fields) Plugin
- func NewLevelHook(id string, fn func(TransformLogLevelParams) (LogLevel, bool)) Plugin
- func NewMessageHook(id string, fn func(BeforeMessageOutParams) []any) Plugin
- func NewMetadataHook(id string, fn func(any) any) Plugin
- func NewPlugin(id string) Plugin
- func NewSendGate(id string, fn func(ShouldSendParams) bool) Plugin
- func WithErrorReporter(p Plugin, onError func(error)) Plugin
- type PluginPanicDetails
- type RawLogEntry
- type RecoveredPanicError
- type RoutingConfig
- type Schema
- type SendGate
- type ShouldSendParams
- type Source
- type SourceConfig
- type TransformLogLevelParams
- type Transport
- type TransportParams
- type UngroupedMode
- type UngroupedRouting
Examples ¶
- Package
- Build
- ErrorSerializer
- FromContext
- Lazy
- LogLayer.AddTransport
- LogLayer.Child
- LogLayer.ErrorOnly
- LogLayer.MetadataOnly
- LogLayer.Raw
- LogLayer.SetLevel
- LogLayer.WithContext
- LogLayer.WithError
- LogLayer.WithFields
- LogLayer.WithGroup
- LogLayer.WithMetadata
- LogLayer.WithPrefix
- LogLayer.WithoutFields
- Multiline
- New
- NewDataHook
- NewFieldsHook
- NewLevelHook
- NewMessageHook
- NewMetadataHook
- NewMock
- NewSendGate
Constants ¶
const ( // PanicKindPlugin marks a panic recovered from a plugin hook. // ID is the plugin's ID; Plugin carries the hook method name. PanicKindPlugin = "plugin" // PanicKindTransport marks a panic recovered from a transport's // SendToLogger. ID is the transport ID; Plugin is nil. PanicKindTransport = "transport" )
PanicKind values for [RecoveredPanicError.Kind].
const LazyEvalError = "[LazyEvalError]"
LazyEvalError is the placeholder substituted into a log entry when a Lazy callback panics.
Variables ¶
var ErrNoTransport = errors.New("loglayer: at least one transport must be provided")
ErrNoTransport is returned by Build (and panicked by New) when a Config is constructed with neither Transport nor Transports set.
var ErrTransportAndTransports = errors.New("loglayer: set Transport or Transports, not both")
ErrTransportAndTransports is returned by Build (and panicked by New) when a Config sets both Transport and Transports. Use one or the other to avoid silently dropping entries.
var ErrUngroupedTransportsWithoutMode = errors.New("loglayer: UngroupedRouting.Transports set without UngroupedToTransports mode")
ErrUngroupedTransportsWithoutMode is returned by Build (and panicked by New) when Config.UngroupedRouting.Transports is non-empty but UngroupedRouting.Mode is left at its zero value (UngroupedToAll). Either set Mode to UngroupedToTransports to use the allowlist, or clear Transports.
Functions ¶
func ActiveGroupsFromEnv ¶
ActiveGroupsFromEnv reads a comma-separated list of group names from the named environment variable and returns it as a slice suitable for Config.ActiveGroups or SetActiveGroups. Empty / unset returns nil.
Whitespace around commas is trimmed; empty entries are skipped.
Use it explicitly at startup (we don't read environment variables on your behalf):
loglayer.New(loglayer.Config{
Transport: ...,
Groups: ...,
ActiveGroups: loglayer.ActiveGroupsFromEnv("LOGLAYER_GROUPS"),
})
func NewContext ¶
NewContext returns a copy of parent in which the given LogLayer is attached. Use this in middleware (HTTP, gRPC, etc.) to make a request-scoped logger available to downstream code without threading it through every function signature. Recover the logger with FromContext.
If log is nil, parent is returned unchanged.
func UnwrappingErrorSerializer ¶
UnwrappingErrorSerializer is an opt-in ErrorSerializer that walks the error chain and surfaces every wrapped cause as structured data.
The default serializer (a flat `{"message": err.Error()}`) is the right choice when you only need the rendered string. Pick this one when you want to preserve the structure of `fmt.Errorf("...: %w", err)` chains or `errors.Join(...)` lists in the log output:
log := loglayer.New(loglayer.Config{
Transport: structured.New(structured.Config{}),
ErrorSerializer: loglayer.UnwrappingErrorSerializer,
})
log.WithError(fmt.Errorf("op failed: %w", io.EOF)).Error("oops")
// {"err": {"message": "op failed: EOF", "causes": [{"message": "EOF"}]}}
Behavior:
- The top-level message is `err.Error()` verbatim.
- For a single-chain error (`errors.Unwrap(err)` returns one), each unwrap step appends one `{"message": ...}` object to `causes`. The walk stops at the first nil unwrap.
- For an `errors.Join` value (`Unwrap() []error`), each member becomes one `{"message": ...}` object in `causes`. Members are not recursively walked, so nested Joined+wrapped errors flatten to one level. If a Join member has its own chain you can recurse by writing your own serializer that calls this one per member.
- `causes` is omitted when there are no wrapped errors below the top frame, keeping the JSON shape identical to the default serializer for unwrapped errors.
Returns nil for a nil error (which the dispatch path treats as "no err key in the output", matching the default serializer's contract).
Types ¶
type BeforeDataOutParams ¶
type BeforeDataOutParams struct {
LogLevel LogLevel
// Data is the assembled fields + error map. May be nil if the entry
// has no fields and no error.
Data Data
// Fields is the logger's persistent fields, as the core sees them
// (after OnFieldsCalled has already run at registration time).
Fields Fields
// Metadata is the value the user passed to WithMetadata, after any
// OnMetadataCalled mutations.
Metadata any
// Err is the error attached via WithError, or nil.
Err error
// Ctx is the per-call context.Context attached via WithContext, or nil.
Ctx context.Context
// Groups mirrors [TransportParams.Groups].
Groups []string
// Schema mirrors [TransportParams.Schema].
Schema Schema
// Prefix mirrors [TransportParams.Prefix]: the value attached
// via WithPrefix on the emitting logger (or set on Config.Prefix
// at construction). Empty when no prefix was set. Read-only for
// hooks; the framework propagates this value unchanged through
// the dispatch path.
Prefix string
}
BeforeDataOutParams is the input to [DataHook.OnBeforeDataOut].
type BeforeMessageOutParams ¶
type BeforeMessageOutParams struct {
LogLevel LogLevel
Messages []any
// Ctx is the per-call context.Context attached via WithContext, or nil.
Ctx context.Context
// Groups mirrors [TransportParams.Groups].
Groups []string
// Schema mirrors [TransportParams.Schema].
Schema Schema
// Prefix mirrors [TransportParams.Prefix]: the value attached
// via WithPrefix on the emitting logger (or set on Config.Prefix
// at construction). Empty when no prefix was set. Read-only for
// hooks; the framework propagates this value unchanged through
// the dispatch path.
Prefix string
}
BeforeMessageOutParams is the input to [MessageHook.OnBeforeMessageOut].
type Config ¶
type Config struct {
// Transport is a convenience for the single-transport case. Mutually
// exclusive with Transports; setting both panics with
// ErrTransportAndTransports (or returns it from Build).
Transport Transport
// Transports is a slice for the multi-transport case. Mutually exclusive
// with Transport.
Transports []Transport
// Plugins are added to the logger at construction time, in slice order.
// Equivalent to calling AddPlugin for each entry after construction;
// either form is fine.
Plugins []Plugin
// Prefix is exposed verbatim on TransportParams.Prefix and on
// every dispatch-time plugin hook param struct. Transports
// decide how to render it: most call
// transport.JoinPrefixAndMessages to fold it into the first
// message string; cli renders it in dim grey separate from
// the level color. Equivalent to calling WithPrefix on the
// freshly-constructed logger.
Prefix string
// Disabled suppresses all log output when true. Defaults to false
// (logging on). Equivalent to calling DisableLogging() after construction.
Disabled bool
// ErrorSerializer customizes how errors are serialized into the log data.
ErrorSerializer ErrorSerializer
// ErrorFieldName is the key used for the serialized error in log data. Defaults to "err".
ErrorFieldName string
// CopyMsgOnOnlyError copies err.Error() as the log message when calling ErrorOnly.
CopyMsgOnOnlyError bool
// FieldsKey nests all persistent fields under this key. If empty, fields are merged at root.
FieldsKey string
// MetadataFieldName nests the per-call metadata value under this key in
// the assembled output. If empty, transports use their default placement
// policy (renderer transports flatten map metadata at root; wrapper
// transports flatten map metadata to attributes and nest non-map metadata
// under a transport-specific default key, typically "metadata").
//
// When non-empty, the entry's metadata (whether a map, struct, scalar,
// or slice) is nested under this single key uniformly, and transports
// honor that placement.
MetadataFieldName string
// MuteFields disables inclusion of persistent fields in log output.
MuteFields bool
// MuteMetadata disables inclusion of metadata in log output.
MuteMetadata bool
// DisableFatalExit prevents the library from calling os.Exit(1) after a
// Fatal-level log is dispatched. Defaults to false (Fatal exits, matching
// the Go convention used by log.Fatal, zerolog, zap, logrus, and others).
//
// Set to true in tests, library code that shouldn't terminate the host
// process, or any context where os.Exit would be inappropriate.
//
// Note: a few logger-wrapper transports (notably phuslu) may still trigger
// their underlying library's exit before this option takes effect. See
// each wrapper's docs for details.
DisableFatalExit bool
// TransportCloseTimeout caps how long the framework waits for an
// io.Closer transport to drain on removal (RemoveTransport,
// SetTransports, AddTransport-by-replace) or pre-Fatal flush, so a
// wedged endpoint can't hang the process or mutator goroutine.
// Defaults to 5 seconds when zero or negative.
TransportCloseTimeout time.Duration
// OnTransportPanic is called when a transport's SendToLogger panics.
// The dispatch loop recovers the panic so a buggy transport can't
// crash the host application from inside a log call, then continues
// to the next transport.
//
// The argument is a [*RecoveredPanicError] with Kind = PanicKindTransport
// and Hook = the panicking transport's ID. This matches the shape
// passed to plugin [ErrorReporter.OnError] callbacks so a single
// observability handler can absorb panics from either source.
//
// Default (nil): no recover wrap; a panicking transport propagates
// up through the emission call (matching the convention used by
// zerolog/zap/log/slog). Set this to plumb panics into your own
// observability (a metrics counter, an error tracker, a separate
// logger). A panic from inside this callback is itself recovered
// (and dropped) so a buggy reporter can't take down the dispatch
// loop.
OnTransportPanic func(err *RecoveredPanicError)
// Source configures call-site capture (file/line/function) per emission.
// Off by default; see [SourceConfig] for the cost.
Source SourceConfig
// Routing configures group-based dispatch (named routing rules,
// active-groups filter, behavior for ungrouped entries). The zero
// value disables group routing entirely (every transport receives
// every entry).
Routing RoutingConfig
}
Config is the initialization configuration for a LogLayer instance.
type CopyMsgPolicy ¶
type CopyMsgPolicy uint8
CopyMsgPolicy controls per-call whether ErrorOnly copies err.Error() into the log message. The zero value (CopyMsgDefault) defers to Config.CopyMsgOnOnlyError.
const ( // CopyMsgDefault uses Config.CopyMsgOnOnlyError. Zero value. CopyMsgDefault CopyMsgPolicy = iota // CopyMsgEnabled forces err.Error() to be copied as the log message // for this call, regardless of Config.CopyMsgOnOnlyError. CopyMsgEnabled // CopyMsgDisabled forces no message copy for this call, regardless of // Config.CopyMsgOnOnlyError. CopyMsgDisabled )
type Data ¶
Data is the assembled object sent to transports containing the persistent fields and the serialized error.
type DataHook ¶
type DataHook interface {
OnBeforeDataOut(BeforeDataOutParams) Data
}
DataHook fires per-emission, after the assembled data map (fields + serialized error) is built but before the entry reaches transports. Return the data to merge in; nil leaves the assembled data unchanged. The returned map is shallow-merged: keys present overwrite existing values; missing keys are left alone.
type ErrorOnlyOpts ¶
type ErrorOnlyOpts struct {
// LogLevel overrides the default error level. Defaults to LogLevelError.
LogLevel LogLevel
// CopyMsg overrides Config.CopyMsgOnOnlyError for this call. Zero
// value (CopyMsgDefault) keeps the config default.
CopyMsg CopyMsgPolicy
}
ErrorOnlyOpts are optional settings for the ErrorOnly method.
type ErrorReporter ¶
type ErrorReporter interface {
OnError(err error)
}
ErrorReporter is implemented by plugins that want to observe recovered panics in their own hooks. The framework recovers every hook panic so a buggy plugin can't tear down the calling goroutine; OnError lets the plugin observe the recovery (log it, increment a counter, etc.).
If a plugin doesn't implement ErrorReporter, the framework writes a one-line description of the recovered panic to os.Stderr so it isn't silent.
The error passed is a *RecoveredPanicError; the panicked hook is named in the error message and accessible via Hook / Value.
type ErrorSerializer ¶
ErrorSerializer converts an error into a structured map for the log output. If not set, the default serializer uses {"message": err.Error()}.
Returning nil drops the error field entirely (the entry is emitted with no err key). Returning an empty map adds an empty err object.
Example ¶
ErrorSerializer customizes how errors attached via WithError are rendered in the assembled Data. The default emits {"message": err.Error()}; override to add type, stack, or wrapped-cause fields.
log := loglayer.New(loglayer.Config{
Transport: exampleTransport{},
DisableFatalExit: true,
ErrorSerializer: func(err error) map[string]any {
return map[string]any{
"message": err.Error(),
"type": fmt.Sprintf("%T", err),
}
},
})
log.WithError(&queryErr{msg: "connection refused"}).Error("query failed")
Output: {"level":"error","time":"2026-04-26T12:00:00Z","msg":"query failed","err":{"message":"connection refused","type":"*loglayer_test.queryErr"}}
type F ¶
type F = Fields
F is a short alias for Fields for terser call sites: log.WithFields(loglayer.F{"reqId": "abc"}).Info("done").
type Fields ¶
Fields is persistent key/value data included with every log entry from a logger instance. Set via WithFields; surfaced to transports via TransportParams.Fields. Distinct from Metadata (per-call) and Data (assembled output) at the type level so the compiler catches misuse.
type FieldsHook ¶
FieldsHook fires when *LogLayer.WithFields is called. It receives the fields about to be merged onto the derived logger and returns the fields to merge instead. Return nil to drop the WithFields call (the receiver's existing fields are preserved either way). Multiple plugins chain: each receives the previous plugin's output.
type LazyValue ¶
type LazyValue struct {
// contains filtered or unexported fields
}
LazyValue wraps a callback evaluated at log dispatch time. Construct with Lazy and store as a value in Fields. See the Lazy Evaluation docs for placement and timing.
func Lazy ¶
Lazy wraps fn so it is invoked only at log emit time, and only when the level is enabled. Re-evaluated on every emission from a logger holding it; child loggers inherit the wrapper, not a resolved value.
Example ¶
Lazy defers a value until log emit time, and only when the level is enabled. Use it for fields whose computation is too expensive to pay on every call site.
log := exampleLogger().WithFields(loglayer.Fields{
"computed": loglayer.Lazy(func() any { return 42 }),
})
log.Info("done")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"done","computed":42}
type LevelHook ¶
type LevelHook interface {
TransformLogLevel(TransformLogLevelParams) (LogLevel, bool)
}
LevelHook fires per-emission, after DataHook and MessageHook but before per-transport dispatch. Return (level, true) to override the entry's level; (_, false) to leave it unchanged. If multiple plugins return ok=true, the last one wins.
type LogBuilder ¶
type LogBuilder struct {
// contains filtered or unexported fields
}
LogBuilder accumulates per-log metadata, error, and context.Context before dispatching to a log level method. Obtain one via LogLayer.WithMetadata, LogLayer.WithError, or LogLayer.WithContext.
LogBuilders are intended to be single-use and stack-allocated. Build, chain, and terminate inline:
log.WithContext(ctx).WithMetadata(meta).WithError(err).Error("failed")
Holding a *LogBuilder past its terminal call works but discards the stack-allocation benefit.
func (*LogBuilder) Debug ¶
func (b *LogBuilder) Debug(messages ...any)
Debug dispatches the accumulated entry at the debug level.
func (*LogBuilder) Error ¶
func (b *LogBuilder) Error(messages ...any)
Error dispatches the accumulated entry at the error level.
func (*LogBuilder) Fatal ¶
func (b *LogBuilder) Fatal(messages ...any)
Fatal dispatches the accumulated entry at the fatal level. Calls os.Exit(1) after dispatch unless Config.DisableFatalExit is set.
func (*LogBuilder) Info ¶
func (b *LogBuilder) Info(messages ...any)
Info dispatches the accumulated entry at the info level.
func (*LogBuilder) Panic ¶
func (b *LogBuilder) Panic(messages ...any)
Panic dispatches the accumulated entry at the panic level then panics with the joined message string. The panic is recoverable; see LogLayer.Panic for the contract.
func (*LogBuilder) Trace ¶
func (b *LogBuilder) Trace(messages ...any)
Trace dispatches the accumulated entry at the trace level.
func (*LogBuilder) Warn ¶
func (b *LogBuilder) Warn(messages ...any)
Warn dispatches the accumulated entry at the warn level.
func (*LogBuilder) WithContext ¶
func (b *LogBuilder) WithContext(ctx context.Context) *LogBuilder
WithContext attaches a context.Context to this single log entry, overriding any context bound to the parent logger via (*LogLayer).WithContext for this emission only.
For the persistent variant (bind once, every subsequent emission carries the ctx), use (*LogLayer).WithContext instead.
Passing nil clears any per-call ctx previously set on this builder. On a fresh builder it has no observable effect (the layer's bound ctx, if any, still applies on dispatch).
func (*LogBuilder) WithError ¶
func (b *LogBuilder) WithError(err error) *LogBuilder
WithError attaches an error to the log entry.
func (*LogBuilder) WithGroup ¶
func (b *LogBuilder) WithGroup(groups ...string) *LogBuilder
WithGroup tags this single log entry with one or more group names. Routing rules in Config.Groups decide which transports receive the entry. Tags are merged with any persistent groups assigned via (*LogLayer).WithGroup.
Calling this multiple times accumulates groups (deduplicated).
func (*LogBuilder) WithMetadata ¶
func (b *LogBuilder) WithMetadata(v any) *LogBuilder
WithMetadata attaches metadata to the log entry. Accepts any value: a struct, a map, or any other type. Serialization is handled by the transport. Calling this multiple times replaces the previous value.
OnMetadataCalled plugin hooks run here. A hook returning nil drops the metadata entirely for this entry.
type LogGroup ¶
type LogGroup struct {
// Transports lists the IDs of transports this group routes to.
// Required for the group to do anything.
Transports []string
// Level is the minimum log level for this group. Entries below this
// level are dropped for this group's transports. Zero value means "no
// per-group filter: all levels pass" (the levelIndex check rejects 0
// as an unknown level).
Level LogLevel
// Disabled suppresses this group's routing when true. Entries tagged
// only with disabled groups are dropped. Entries tagged with both a
// disabled and an enabled group still route through the enabled one.
//
// (Contrast with an undefined group name in the tag list: if every
// tag refers to an undefined group, the entry falls back to
// UngroupedRouting. Disabled is "explicitly off"; undefined is
// "treated as no tag.")
Disabled bool
}
LogGroup is a named routing rule.
type LogLayer ¶
type LogLayer struct {
// contains filtered or unexported fields
}
LogLayer is the central logger. It assembles log entries from fields, metadata, and error data, then dispatches them to one or more transports.
func Build ¶
Build creates a new LogLayer from the given Config, returning an error instead of panicking if the configuration is invalid (e.g. no transport).
Use New for the more concise idiom when misconfiguration is a programmer error (the typical case for application setup).
Example ¶
Build returns an error instead of panicking on misconfiguration.
_, err := loglayer.Build(loglayer.Config{}) // no transport
if errors.Is(err, loglayer.ErrNoTransport) {
os.Stdout.WriteString("missing transport\n")
}
Output: missing transport
func FromContext ¶
FromContext returns the LogLayer attached to ctx by NewContext, or nil if no logger was attached. Pair with MustFromContext if your code expects the logger to always be present (e.g. handlers behind middleware that always sets one).
Example ¶
FromContext retrieves a *LogLayer attached upstream by NewContext. Pair the two in middleware so handlers don't have to thread the logger through every call signature.
log := exampleLogger().WithFields(loglayer.Fields{"requestId": "abc"})
ctx := loglayer.NewContext(context.Background(), log)
handler := func(ctx context.Context) {
loglayer.FromContext(ctx).Info("handling")
}
handler(ctx)
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"handling","requestId":"abc"}
func MustFromContext ¶
MustFromContext is like FromContext but panics if no logger is attached. Use it in handler code where middleware is guaranteed to have called NewContext; the panic surfaces a misconfiguration immediately rather than silently dropping logs.
func New ¶
New creates a new LogLayer from the given Config.
Panics if no transport is provided. For applications that prefer explicit error handling on misconfiguration, use Build instead.
Example ¶
log := loglayer.New(loglayer.Config{
Transport: exampleTransport{},
})
log.Info("hello")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"hello"}
func NewMock ¶
func NewMock() *LogLayer
NewMock returns a *LogLayer that silently discards every entry. Use it in tests when you need to pass a logger to code under test but don't care about its output.
The returned value is the same concrete *LogLayer type as a production logger, so it drops into anywhere the real one fits. All methods behave normally — context, metadata, child loggers, level changes — they just produce no output.
DisableFatalExit is enabled so log.Fatal(...) in code under test does not terminate the test process.
To assert on what was logged, use the transports/testing transport instead.
Example ¶
NewMock returns a silent logger for tests; calls accept everything but emit nothing.
log := loglayer.NewMock()
log.WithFields(loglayer.Fields{"requestId": "abc"}).Info("silent")
os.Stdout.WriteString("test ran\n")
Output: test ran
func (*LogLayer) AddGroup ¶
AddGroup registers (or replaces) a named group. If a group with the same name already exists it is replaced, matching the AddTransport / AddPlugin convention. Returns the receiver for chaining.
Safe to call from any goroutine.
func (*LogLayer) AddPlugin ¶
AddPlugin registers one or more plugins. Plugins whose ID() returns the empty string get an auto-generated identifier; supply your own ID if you plan to call RemovePlugin / GetPlugin or replace the plugin later. If a plugin's ID matches an already-registered plugin, the existing one is replaced.
Safe to call from any goroutine: the plugin set is published atomically. Concurrent mutators on the same logger serialize via an internal mutex.
func (*LogLayer) AddTransport ¶
AddTransport appends one or more transports. If a transport with the same ID already exists it is closed (if it implements io.Closer) and replaced.
Safe to call concurrently with log emission: the new transport set is published atomically. Concurrent mutators on the same logger serialize via an internal mutex.
Example ¶
AddTransport registers a transport on a running logger. Subsequent emissions fan out to every registered transport, in registration order. A new transport whose ID matches an existing one replaces (and closes) the prior instance.
log := loglayer.New(loglayer.Config{
Transport: tagTransport{id: "primary"},
DisableFatalExit: true,
})
log.Info("before")
log.AddTransport(tagTransport{id: "audit"})
log.Info("after")
Output: [primary] before [primary] after [audit] after
func (*LogLayer) Child ¶
Child creates a new LogLayer that inherits the current config, fields (shallow copy), level state, transports, plugins, and group routing. Changes to the child do not affect the parent.
Example ¶
Child returns an independent clone. Mutations on the child don't bleed back to the parent.
parent := exampleLogger().WithFields(loglayer.Fields{"who": "parent"})
child := parent.WithFields(loglayer.Fields{"who": "child"})
child.Info("hi")
parent.Info("hi")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"hi","who":"child"} {"level":"info","time":"2026-04-26T12:00:00Z","msg":"hi","who":"parent"}
func (*LogLayer) ClearActiveGroups ¶
ClearActiveGroups removes the active-groups filter, returning the logger to "all defined groups are active."
func (*LogLayer) DisableGroup ¶
DisableGroup suppresses a group without removing it. Entries tagged only with disabled groups are dropped (the explicit-off semantics; undefined-group tags fall back to UngroupedRouting). No-op when the group is not registered.
func (*LogLayer) DisableLevel ¶
DisableLevel turns off a specific log level without affecting others. Unknown levels are silently ignored.
Safe to call concurrently with log emission: mutates an atomic bitmap.
func (*LogLayer) DisableLogging ¶
DisableLogging suppresses all log output regardless of individual level state.
Safe to call concurrently with log emission: mutates an atomic bitmap.
func (*LogLayer) EnableGroup ¶
EnableGroup re-enables a previously disabled group. No-op when the group is not registered.
func (*LogLayer) EnableLevel ¶
EnableLevel turns on a specific log level without affecting others. Unknown levels are silently ignored.
Safe to call concurrently with log emission: mutates an atomic bitmap.
func (*LogLayer) EnableLogging ¶
EnableLogging re-enables all logging after DisableLogging.
Safe to call concurrently with log emission: mutates an atomic bitmap.
func (*LogLayer) ErrorOnly ¶
func (l *LogLayer) ErrorOnly(err error, opts ...ErrorOnlyOpts)
ErrorOnly logs an error without a message. The log level defaults to error.
Example ¶
ErrorOnly logs just an error. Override the level via opts.
log := exampleLogger()
log.ErrorOnly(errors.New("disk full"))
Output: {"level":"error","time":"2026-04-26T12:00:00Z","msg":"","err":{"message":"disk full"}}
func (*LogLayer) Fatal ¶
Fatal logs at the fatal level. Calls os.Exit(1) after dispatch unless Config.DisableFatalExit is set.
func (*LogLayer) GetGroups ¶
GetGroups returns a snapshot of the current group configuration. Mutating the returned map (or its LogGroup entries) does not affect the logger's state.
func (*LogLayer) GetLoggerInstance ¶
GetLoggerInstance returns the underlying logger instance for the transport with the given ID, or nil if not found.
func (*LogLayer) GetPlugin ¶
GetPlugin returns the registered plugin with the given ID, or (nil, false) if no plugin with that ID is registered.
func (*LogLayer) IsLevelEnabled ¶
IsLevelEnabled reports whether the given level will produce output.
func (*LogLayer) MetadataOnly ¶
func (l *LogLayer) MetadataOnly(v any, opts ...MetadataOnlyOpts)
MetadataOnly logs metadata without a message. The log level defaults to info. Accepts any value: a struct, a map, or any other type.
OnMetadataCalled plugin hooks run here, same as WithMetadata. If a plugin returns nil (the documented nil-drop signal), the entire entry is suppressed: there's no message and no metadata, so there's nothing to log. Plugin authors should be aware that returning nil from OnMetadataCalled silences MetadataOnly callers entirely. Same applies when MuteMetadata is set on the logger.
Example ¶
MetadataOnly logs just the metadata, with no message text. Useful for metric-style entries.
log := exampleLogger()
log.MetadataOnly(loglayer.Metadata{"queueDepth": 17})
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"","queueDepth":17}
func (*LogLayer) MuteFields ¶
MuteFields disables persistent fields from appearing in log output.
Safe to call concurrently with log emission: backed by atomic.Bool.
func (*LogLayer) MuteMetadata ¶
MuteMetadata disables metadata from appearing in log output.
Safe to call concurrently with log emission: backed by atomic.Bool.
func (*LogLayer) NewLogLogger ¶
NewLogLogger returns a *log.Logger that emits through this LogLayer at the given level. Use it for stdlib-log-shaped consumers that won't accept an arbitrary writer:
srv := &http.Server{
ErrorLog: log.NewLogLogger(loglayer.LogLevelError),
}
gorm, database/sql tracing, and most third-party libraries that take a *log.Logger plug in the same way. The returned logger has empty prefix and zero flags so the rendered entry doesn't get a duplicate timestamp or level prefix from the stdlib side; loglayer adds those itself.
Mirrors slog.NewLogLogger.
func (*LogLayer) Panic ¶
Panic logs at the panic level then panics with the joined message string. Unlike Fatal, the panic is recoverable, so async transports are not pre-flushed (closing them would break callers that recover and keep emitting). To suppress the panic in tests, recover in the calling goroutine.
func (*LogLayer) PluginCount ¶
PluginCount returns the number of plugins currently registered.
func (*LogLayer) Raw ¶
func (l *LogLayer) Raw(entry RawLogEntry)
Raw dispatches a fully specified log entry, bypassing the builder API. All normal assembly and transport dispatch still applies.
entry.Source takes precedence over runtime capture: if it's non-nil it's passed through as-is (the slog handler uses this to forward source from slog.Record.PC). Otherwise, when Config.Source.Enabled is true, source is captured at the Raw call site.
Example ¶
Raw bypasses the builder and dispatches a fully-specified entry. Useful when forwarding from another logging system.
log := exampleLogger()
log.Raw(loglayer.RawLogEntry{
LogLevel: loglayer.LogLevelWarn,
Messages: []any{"upstream timeout"},
Metadata: loglayer.Metadata{"retries": 3},
})
Output: {"level":"warn","time":"2026-04-26T12:00:00Z","msg":"upstream timeout","retries":3}
func (*LogLayer) RemoveGroup ¶
RemoveGroup deletes a group by name. Returns true if the group was present.
Safe to call from any goroutine.
func (*LogLayer) RemovePlugin ¶
RemovePlugin removes the plugin with the given ID. Returns true if a plugin was removed, false if no plugin with that ID was registered.
Safe to call from any goroutine.
func (*LogLayer) RemoveTransport ¶
RemoveTransport removes the transport with the given ID. The removed transport is closed if it implements io.Closer (HTTP/Datadog drain pending entries before returning), capped by Config.TransportCloseTimeout so a wedged endpoint can't hang the mutator goroutine. Returns true if found and removed, false otherwise.
Safe to call concurrently with log emission.
func (*LogLayer) SetActiveGroups ¶
SetActiveGroups restricts routing to the named groups. Entries tagged with groups not in the list are dropped (or fall back to UngroupedRouting if every tagged group is excluded).
At least one group name is required. Use ClearActiveGroups to remove the filter entirely; an empty filter is intentionally not expressible here because variadic-empty calls (e.g. SetActiveGroups(slice...) with an empty slice) would silently suppress all grouped routing.
func (*LogLayer) SetGroupLevel ¶
SetGroupLevel updates the minimum level for a group. No-op when the group is not registered.
func (*LogLayer) SetLevel ¶
SetLevel enables all levels at or above level and disables those below it.
Safe to call concurrently with log emission: mutates an atomic bitmap.
Example ¶
SetLevel raises the threshold so entries below the given level are dropped. Mutates the logger in place; the return value is the same instance and exists only for chaining.
log := exampleLogger()
log.SetLevel(loglayer.LogLevelWarn)
log.Info("dropped")
log.Warn("emitted")
Output: {"level":"warn","time":"2026-04-26T12:00:00Z","msg":"emitted"}
func (*LogLayer) SetTransports ¶
SetTransports replaces all existing transports. Any previous transport not present in the new set (matched by ID) is closed if it implements io.Closer, capped by Config.TransportCloseTimeout.
Safe to call concurrently with log emission.
func (*LogLayer) Trace ¶
Trace logs at the trace level. Trace sits below Debug; use it for extremely fine-grained diagnostic output that you'd typically want disabled in production.
func (*LogLayer) UnmuteFields ¶
UnmuteFields re-enables persistent fields in log output.
Safe to call concurrently with log emission: backed by atomic.Bool.
func (*LogLayer) UnmuteMetadata ¶
UnmuteMetadata re-enables metadata in log output.
Safe to call concurrently with log emission: backed by atomic.Bool.
func (*LogLayer) WithContext ¶
WithContext returns a derived logger that automatically attaches the given context.Context to every emission. Transports receive it via TransportParams.Ctx; plugins receive it on dispatch-time hook params.
Per-call (*LogBuilder).WithContext still overrides for one emission.
The receiver is unchanged (returns a new logger; assign the result). Passing nil returns a clone with no bound context, which clears any context the receiver had previously bound.
Example ¶
WithContext attaches a context.Context to one log call. Transports can read trace IDs, deadlines, and other request-scoped values from it.
log := exampleLogger()
ctx := context.Background()
log.WithContext(ctx).Info("request received")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"request received"}
func (*LogLayer) WithError ¶
func (l *LogLayer) WithError(err error) *LogBuilder
WithError returns a LogBuilder with the given error attached.
Example ¶
WithError attaches an error to one log entry. The default serializer emits {"message": err.Error()}.
log := exampleLogger()
log.WithError(errors.New("connection refused")).Error("query failed")
Output: {"level":"error","time":"2026-04-26T12:00:00Z","msg":"query failed","err":{"message":"connection refused"}}
func (*LogLayer) WithFields ¶
WithFields returns a new logger with the given key/value pairs merged into the persistent fields bag. The receiver is unchanged.
This matches the convention used by zerolog, zap, slog, and logrus: the derive operation produces a fresh logger so concurrent goroutines (HTTP handlers, workers) can each carry their own per-request fields without racing on shared state. Assign the result:
log = log.WithFields(loglayer.Fields{"requestId": "abc"})
Discarding the return value is a no-op. The compiler does not catch this.
Example ¶
WithFields returns a new logger with persistent key/value pairs that appear on every subsequent log entry. Always assign the result.
log := exampleLogger()
log = log.WithFields(loglayer.Fields{"requestId": "abc-123"})
log.Info("processing")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"processing","requestId":"abc-123"}
func (*LogLayer) WithGroup ¶
WithGroup returns a child logger tagged with the given groups. Every log emitted from the returned logger is dispatched only to the transports allowed by these groups (per the routing rules in shouldRoute).
The receiver is unchanged. Tags are additive across chained calls:
dbAuth := log.WithGroup("database").WithGroup("auth")
// dbAuth's entries route through both groups' transports.
Returns a child even when groups is empty (so callers can chain safely).
Example ¶
WithGroup tags entries so they only route to the transports listed for that group in Config.Routing. Tagged entries reach the audit transport; the general transport is skipped.
audit := exampleTransport{id: "audit"}
general := exampleTransport{id: "general"}
log := loglayer.New(loglayer.Config{
Transports: []loglayer.Transport{audit, general},
DisableFatalExit: true,
Routing: loglayer.RoutingConfig{
Groups: map[string]loglayer.LogGroup{
"audit": {Transports: []string{"audit"}},
},
},
})
log.WithGroup("audit").Info("user signed in")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"user signed in"}
func (*LogLayer) WithMetadata ¶
func (l *LogLayer) WithMetadata(v any) *LogBuilder
WithMetadata returns a LogBuilder with the given metadata attached. Accepts any value: a struct, a map, or any other type. Serialization is handled by the transport.
Example ¶
WithMetadata accepts any value for one log entry only. Maps merge at root.
log := exampleLogger()
log.WithMetadata(loglayer.Metadata{"durationMs": 42}).Info("served")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"served","durationMs":42}
func (*LogLayer) WithPrefix ¶
WithPrefix creates a child logger with the given prefix prepended to every message. The receiver is unchanged.
Example ¶
WithPrefix returns a new logger with a string prepended to every message.
log := exampleLogger().WithPrefix("[auth]")
log.Info("login attempt")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"[auth] login attempt"}
func (*LogLayer) WithoutFields ¶
WithoutFields returns a new logger with the given keys removed from the persistent fields bag. With no arguments, all fields are cleared. The receiver is unchanged. Paired with WithFields; assign the result.
Example ¶
WithoutFields removes specific keys (or all keys when called with no args).
log := exampleLogger()
log = log.WithFields(loglayer.Fields{"keep": "yes", "drop": "no"})
log = log.WithoutFields("drop")
log.Info("partial")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"partial","keep":"yes"}
func (*LogLayer) Writer ¶
Writer returns an io.Writer that emits one log entry per Write call at the given level. Each Write becomes a single dispatch through the loglayer pipeline (plugins, fan-out, group routing, level state).
Trailing newlines are stripped (callers like the stdlib log package always append "\n", and the rendered entry adds its own delimiter). Empty writes are suppressed so a flush of an already-empty buffer does not produce a blank line.
Use it to plumb code that writes to an io.Writer (third-party libraries' loggers, raw byte streams, anything that calls Write) through your loglayer pipeline:
w := log.Writer(loglayer.LogLevelInfo) fmt.Fprintln(w, "hello")
type LogLevel ¶
type LogLevel int
LogLevel represents the severity of a log entry. Higher numeric values indicate higher severity.
Values are non-uniformly spaced (Trace=5, Debug=10, Info=20, ...) so a future intermediate level (e.g. Notice between Info and Warn) can land without colliding. Panic sits above Fatal because panic is the most severe class of emission this library supports.
The set is fixed: [levelIndex], LogLevel.String, and ParseLogLevel are each switches over the seven built-ins. Replacing them with a registry lookup would unlock user-registered custom levels without changing the public API; the design is deliberately deferred until there's a concrete need (collision policy, ordering rules, mutability semantics) worth resolving.
func ParseLogLevel ¶
ParseLogLevel converts a string level name to a LogLevel. Returns LogLevelInfo and false if the name is not recognized.
type M ¶
type M = Metadata
M is a short alias for Metadata for terser call sites: log.WithMetadata(loglayer.M{"duration": 150}).Info("served").
type MessageHook ¶
type MessageHook interface {
OnBeforeMessageOut(BeforeMessageOutParams) []any
}
MessageHook fires per-emission, after DataHook and before LevelHook. Return a replacement messages slice; nil leaves the messages unchanged.
type Metadata ¶
Metadata is the most common shape passed to WithMetadata: a string-keyed map of arbitrary values. WithMetadata accepts any value (struct, scalar, slice, anything), but when the data is an ad-hoc bag this named type keeps call sites short.
type MetadataHook ¶
MetadataHook fires from WithMetadata and MetadataOnly. It receives the metadata value the user passed (which may be a map, struct, scalar, or nil) and returns the metadata to use. Return nil to drop the metadata entirely for this entry.
type MetadataOnlyOpts ¶
type MetadataOnlyOpts struct {
// LogLevel overrides the default info level. Defaults to LogLevelInfo.
LogLevel LogLevel
}
MetadataOnlyOpts are optional settings for the MetadataOnly method.
type MultilineMessage ¶ added in v2.1.0
type MultilineMessage struct {
// contains filtered or unexported fields
}
MultilineMessage wraps a sequence of authored lines so terminal transports render them on separate rows. Construct with Multiline.
Token of trust: the wrapper signals that the developer authored the line boundaries, so terminal renderers permit \n between elements while still sanitizing ANSI / control bytes within each line.
func Multiline ¶ added in v2.1.0
func Multiline(lines ...any) *MultilineMessage
Multiline wraps lines so terminal transports render them on separate rows.
Construction-time normalization, applied uniformly so every transport sees the same Lines() shape:
- Non-string args convert via fmt.Sprintf("%v", v).
- *MultilineMessage args flatten: their Lines() append into the outer's slice.
- Every resulting string is split on "\n", and each piece becomes one entry of Lines(). After this step, no Lines() entry contains an embedded "\n".
The split rule means Multiline("a\nb") and Multiline("a","b") are interchangeable. CRLF input (e.g. "a\r\nb") splits to ["a\r","b"] and the trailing "\r" is stripped by per-line sanitization in terminal transports, yielding the same rendered output as Multiline("a","b").
Example ¶
Multiline shows how to author a multi-line message that survives terminal-renderer sanitization. Multi-line content authored with Multiline preserves "\n" boundaries between authored elements while terminal transports still sanitize ANSI / control bytes inside each line.
log := loglayer.New(loglayer.Config{
Transport: exampleTransport{},
DisableFatalExit: true,
})
log.Info(loglayer.Multiline("Header:", " port: 8080", " host: ::1"))
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"Header:\n port: 8080\n host: ::1"}
func (*MultilineMessage) Lines ¶ added in v2.1.0
func (m *MultilineMessage) Lines() []string
Lines returns the authored line list. Transport authors call this when rendering each line independently. No entry contains "\n".
The returned slice aliases the wrapper's internal storage; do not mutate it. Treat the result as read-only.
func (*MultilineMessage) MarshalJSON ¶ added in v2.1.0
func (m *MultilineMessage) MarshalJSON() ([]byte, error)
MarshalJSON returns the "\n"-joined string as a JSON string. Provided so a wrapper that accidentally lands inside Fields or Metadata serializes as a string rather than {} (no exported fields). Terminal transports still sanitize metadata values to a single line in v1; this just prevents silent data loss in JSON sinks.
func (*MultilineMessage) String ¶ added in v2.1.0
func (m *MultilineMessage) String() string
String joins the lines with "\n". Used by the fmt.Stringer fallback path in transports that don't special-case the type (JSON sinks and every wrapper transport).
type Plugin ¶
type Plugin interface {
ID() string
}
Plugin is the base contract every plugin satisfies. A plugin participates in zero or more lifecycle hooks by also implementing one or more of the hook interfaces below (FieldsHook, MetadataHook, DataHook, MessageHook, LevelHook, SendGate).
Plugins are added via *LogLayer.AddPlugin and identified by ID. Adding a plugin with an ID that's already registered replaces the previous one (matches the AddTransport convention). When ID returns the empty string, the framework assigns an auto-generated identifier at registration time; supply your own when you intend to call RemovePlugin / GetPlugin or replace the plugin later.
Hook ordering: plugins run in the order they were added.
func NewDataHook ¶
func NewDataHook(id string, fn func(BeforeDataOutParams) Data) Plugin
NewDataHook returns a Plugin that implements DataHook only.
Example ¶
NewDataHook fires once per emission, after fields and serialized error are merged into the assembled Data. Returned keys merge into that map; missing keys are left alone.
tagHost := loglayer.NewDataHook("tag-host", func(p loglayer.BeforeDataOutParams) loglayer.Data {
return loglayer.Data{"host": "web-01"}
})
log := loglayer.New(loglayer.Config{
Transport: exampleTransport{},
DisableFatalExit: true,
Plugins: []loglayer.Plugin{tagHost},
})
log.Info("served")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"served","host":"web-01"}
func NewFieldsHook ¶
NewFieldsHook returns a Plugin that implements FieldsHook only.
Example ¶
NewFieldsHook installs a callback that runs whenever WithFields is called. Use it for cross-cutting transformations: redaction, default fields, or normalization.
addApp := loglayer.NewFieldsHook("add-app", func(in loglayer.Fields) loglayer.Fields {
in["app"] = "billing"
return in
})
log := loglayer.New(loglayer.Config{
Transport: exampleTransport{},
DisableFatalExit: true,
Plugins: []loglayer.Plugin{addApp},
})
log.WithFields(loglayer.Fields{"requestId": "abc"}).Info("served")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"served","app":"billing","requestId":"abc"}
func NewLevelHook ¶
func NewLevelHook(id string, fn func(TransformLogLevelParams) (LogLevel, bool)) Plugin
NewLevelHook returns a Plugin that implements LevelHook only.
Example ¶
NewLevelHook can override the level of an entry just before per-transport dispatch.
bumpRetry := loglayer.NewLevelHook("retry-bump", func(p loglayer.TransformLogLevelParams) (loglayer.LogLevel, bool) {
if len(p.Messages) == 0 {
return p.LogLevel, false
}
s, ok := p.Messages[0].(string)
if !ok || !strings.HasPrefix(s, "RETRY") {
return p.LogLevel, false
}
return loglayer.LogLevelWarn, true
})
log := loglayer.New(loglayer.Config{
Transport: exampleTransport{},
DisableFatalExit: true,
Plugins: []loglayer.Plugin{bumpRetry},
})
log.Info("RETRY connection")
Output: {"level":"warn","time":"2026-04-26T12:00:00Z","msg":"RETRY connection"}
func NewMessageHook ¶
func NewMessageHook(id string, fn func(BeforeMessageOutParams) []any) Plugin
NewMessageHook returns a Plugin that implements MessageHook only.
Example ¶
NewMessageHook fires once per emission and rewrites the messages slice.
prefix := loglayer.NewMessageHook("prefix", func(p loglayer.BeforeMessageOutParams) []any {
return append([]any{"[svc]"}, p.Messages...)
})
log := loglayer.New(loglayer.Config{
Transport: exampleTransport{},
DisableFatalExit: true,
Plugins: []loglayer.Plugin{prefix},
})
log.Info("served")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"[svc] served"}
func NewMetadataHook ¶
NewMetadataHook returns a Plugin that implements MetadataHook only.
Example ¶
NewMetadataHook installs a callback that runs whenever WithMetadata or MetadataOnly is called.
addEnv := loglayer.NewMetadataHook("add-env", func(in any) any {
md, ok := in.(loglayer.Metadata)
if !ok {
return in
}
out := make(loglayer.Metadata, len(md)+1)
maps.Copy(out, md)
out["env"] = "prod"
return out
})
log := loglayer.New(loglayer.Config{
Transport: exampleTransport{},
DisableFatalExit: true,
Plugins: []loglayer.Plugin{addEnv},
})
log.WithMetadata(loglayer.Metadata{"durationMs": 42}).Info("served")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"served","durationMs":42,"env":"prod"}
func NewPlugin ¶
NewPlugin returns a Plugin that implements no hook interfaces. Useful for tests that exercise registration/replacement/removal semantics without needing actual hook behavior.
func NewSendGate ¶
func NewSendGate(id string, fn func(ShouldSendParams) bool) Plugin
NewSendGate returns a Plugin that implements SendGate only.
Example ¶
NewSendGate gates per-(entry, transport) dispatch. Return false to drop the entry for that transport; other transports are unaffected. When multiple gates are installed, the entry sends only when every one returns true.
noPings := loglayer.NewSendGate("no-pings", func(p loglayer.ShouldSendParams) bool {
for _, m := range p.Messages {
if s, ok := m.(string); ok && strings.Contains(s, "ping") {
return false
}
}
return true
})
log := loglayer.New(loglayer.Config{
Transport: exampleTransport{},
DisableFatalExit: true,
Plugins: []loglayer.Plugin{noPings},
})
log.Info("ping")
log.Info("served")
Output: {"level":"info","time":"2026-04-26T12:00:00Z","msg":"served"}
func WithErrorReporter ¶
WithErrorReporter wraps p with an ErrorReporter backed by onError. Hook dispatch goes to p exactly as if it were registered directly; the framework recognises the wrapper internally and resolves hook interfaces against p, not the wrapper. The wrapper only contributes the ErrorReporter behavior, so panics in p's hooks reach onError instead of the default stderr path.
Returns p unchanged when onError is nil.
type PluginPanicDetails ¶
type PluginPanicDetails struct {
// Hook is the hook method that panicked, e.g. "OnBeforeDataOut".
Hook string
}
PluginPanicDetails carries plugin-specific information attached to a RecoveredPanicError. Non-nil iff Kind == PanicKindPlugin.
type RawLogEntry ¶
type RawLogEntry struct {
LogLevel LogLevel
Messages []any
// Metadata is per-entry data. Accepts any value: structs, maps, or any other type.
// Serialization is handled by the transport.
Metadata any
Err error
// Fields overrides the logger's persistent fields for this entry. If nil
// the logger's current fields are used.
Fields Fields
// Ctx is an optional Go context.Context for the entry; surfaced to transports
// via TransportParams.Ctx. Use it to carry trace IDs, deadlines, or anything
// else a transport may extract.
Ctx context.Context
// Groups overrides the logger's assigned group tags for routing. Nil
// uses the logger's groups (set via WithGroup).
Groups []string
// Source overrides the captured source info for this entry. Set this
// from adapters that already have source info (e.g. the slog handler
// extracts it from slog.Record.PC). Nil falls back to runtime capture
// when Config.Source.Enabled is true; otherwise no source info is recorded.
Source *Source
}
RawLogEntry is a fully specified log entry used with the Raw method.
type RecoveredPanicError ¶
type RecoveredPanicError struct {
Kind string
ID string
Plugin *PluginPanicDetails
Value any
// contains filtered or unexported fields
}
RecoveredPanicError is the error type produced by the framework's centralized panic recovery (plugin hooks via [ErrorReporter.OnError], and transport SendToLogger via [Config.OnTransportPanic]).
Kind identifies the category (PanicKindPlugin or PanicKindTransport). ID is the panicking component's identifier — the plugin ID for plugins, the transport ID for transports. Plugin carries plugin-specific details (the hook method name) and is non-nil iff Kind == PanicKindPlugin; for transport panics it is nil so the absence of a hook-method dimension is a typed condition rather than an empty-string convention.
Value is the value originally passed to panic(). When Value satisfies the error interface, errors.Unwrap reaches it (and errors.Is / errors.As work transparently); when it doesn't, read Value directly to inspect the concrete type.
func (*RecoveredPanicError) Error ¶
func (e *RecoveredPanicError) Error() string
func (*RecoveredPanicError) Unwrap ¶
func (e *RecoveredPanicError) Unwrap() error
type RoutingConfig ¶
type RoutingConfig struct {
// Groups defines named routing rules. Each group lists the transport
// IDs it routes to, an optional minimum level, and an optional
// disabled flag. Tag entries with a group via WithGroup to limit
// dispatch to that group's transports.
Groups map[string]LogGroup
// ActiveGroups, when non-empty, restricts routing to only these
// groups. Logs tagged with groups not in this list are dropped (or
// fall back to Ungrouped if none of the entry's groups are active).
// Nil/empty means "no filter: all defined groups are active".
ActiveGroups []string
// Ungrouped controls what happens to entries with no group tag
// when Groups is configured. Zero value (Mode: UngroupedToAll)
// preserves the no-routing behavior of every transport receiving
// every ungrouped entry.
Ungrouped UngroupedRouting
}
RoutingConfig configures Config.Routing: named groups + active-groups filter + behavior for ungrouped entries. The zero value disables group routing entirely (every transport receives every entry).
type Schema ¶
type Schema struct {
// FieldsKey is non-empty when the persistent fields are nested under
// this key inside Data. When empty, fields are merged at root.
FieldsKey string
// MetadataFieldName is non-empty when the entry's metadata should
// nest under this key uniformly (both map and non-map values).
// When empty, each transport applies its default placement policy.
MetadataFieldName string
// ErrorFieldName is the key under which the serialized error map
// lives in Data. Always populated; defaults to "err".
ErrorFieldName string
// SourceFieldName is the key under which call-site source info lives
// in Data when [Config.Source.Enabled] is true. Always populated;
// defaults to "source".
SourceFieldName string
}
Schema is the resolved assembly shape for an entry. The core publishes it to transports and dispatch-time plugin hooks so they can navigate [TransportParams.Data] precisely (e.g. find the error map at Schema.ErrorFieldName) and decide their own metadata placement.
All four fields are populated from the matching keys on Config: FieldsKey, MetadataFieldName, ErrorFieldName, Source.FieldName.
type SendGate ¶
type SendGate interface {
ShouldSend(ShouldSendParams) bool
}
SendGate gates per-transport dispatch. Called once per (entry, transport) pair. Return false to skip dispatching that entry to that transport; the other transports are unaffected. If multiple plugins implement SendGate, the entry is sent only when every one returns true.
type ShouldSendParams ¶
type ShouldSendParams struct {
// TransportID is the ID of the transport this dispatch would target.
// Use it to selectively gate per-transport (e.g. send debug to console
// but not to the shipping transport).
TransportID string
LogLevel LogLevel
Messages []any
Data Data
Fields Fields
Metadata any
Err error
// Ctx is the per-call context.Context attached via WithContext, or nil.
Ctx context.Context
// Groups mirrors [TransportParams.Groups].
Groups []string
// Schema mirrors [TransportParams.Schema].
Schema Schema
// Prefix mirrors [TransportParams.Prefix]: the value attached
// via WithPrefix on the emitting logger (or set on Config.Prefix
// at construction). Empty when no prefix was set. Read-only for
// hooks; the framework propagates this value unchanged through
// the dispatch path.
Prefix string
}
ShouldSendParams is the input to [SendGate.ShouldSend].
type Source ¶
type Source struct {
Function string `json:"function,omitempty"`
File string `json:"file,omitempty"`
Line int `json:"line,omitempty"`
}
Source identifies the call site that produced a log entry. Surfaced under Config.Source.FieldName (default "source") when Config.Source.Enabled is true, or when an adapter (e.g. the slog handler) supplies it explicitly via RawLogEntry.Source. Field names match the slog convention so structured output is interchangeable.
func SourceFromPC ¶
SourceFromPC builds a Source from a captured program counter. Adapters that already have a PC (slog.Record.PC, custom callers using runtime.Callers) can call this rather than re-walking the stack. Returns nil for a zero PC or an unresolvable frame.
type SourceConfig ¶
type SourceConfig struct {
// Enabled captures the call site (file/line/function) of every log
// emission via runtime.Caller and includes it in the assembled Data
// under FieldName. Off by default; opt in for production-debuggable
// output. Costs about 620 ns and 5 extra allocations per emission
// on amd64 (see Benchmarks). Paid only when this is true; the
// dispatch path is untouched otherwise.
//
// Adapters that already have source information (notably the slog
// handler, which extracts it from slog.Record.PC) can supply it via
// RawLogEntry.Source without setting Enabled.
Enabled bool
// FieldName is the key under which the captured Source is rendered
// in the assembled Data. Defaults to "source" to match the slog
// convention.
FieldName string
}
SourceConfig configures Config.Source: capture and render the call site of every emission. Paired so the boolean and the output key live next to each other rather than two unrelated top-level fields.
type TransformLogLevelParams ¶
type TransformLogLevelParams struct {
LogLevel LogLevel
Data Data
Messages []any
Fields Fields
Metadata any
Err error
// Ctx is the per-call context.Context attached via WithContext, or nil.
Ctx context.Context
// Groups mirrors [TransportParams.Groups].
Groups []string
// Schema mirrors [TransportParams.Schema].
Schema Schema
// Prefix mirrors [TransportParams.Prefix]: the value attached
// via WithPrefix on the emitting logger (or set on Config.Prefix
// at construction). Empty when no prefix was set. Read-only for
// hooks; the framework propagates this value unchanged through
// the dispatch path.
Prefix string
}
TransformLogLevelParams is the input to [LevelHook.TransformLogLevel].
type Transport ¶
type Transport interface {
// ID returns the unique identifier for this transport.
ID() string
// IsEnabled returns whether the transport is currently active.
IsEnabled() bool
// SendToLogger receives a fully assembled log entry and dispatches it.
// Implementations should perform their own level filtering if needed.
SendToLogger(params TransportParams)
// GetLoggerInstance returns the underlying logger instance, if any.
// Returns nil for transports that have no underlying library.
GetLoggerInstance() any
}
Transport is the interface that all LogLayer transports must implement.
type TransportParams ¶
type TransportParams struct {
LogLevel LogLevel
Messages []any
// Data holds the assembled persistent fields and error. Nil when no
// fields are set and no error is attached. Use len(Data) > 0 to test.
Data Data
// Metadata is the raw value passed to WithMetadata. May be a struct, map,
// or any other type. Nil if WithMetadata was not called or metadata is muted.
Metadata any
Err error
// Fields is the logger's persistent key/value bag, raw (not yet folded into Data).
Fields Fields
// Ctx is the per-call context.Context attached via WithContext, if any.
// Transports can use it to extract trace IDs, span context, deadlines, etc.
// Nil when no Go context was attached.
Ctx context.Context
// Groups holds the entry's group tags (persistent WithGroup first,
// per-call WithGroup appended, deduped). Nil when no groups apply.
// Routing has already consumed the slice before this point; it's
// exposed so transports can include it in the wire payload.
Groups []string
// Schema is the resolved assembly shape (FieldsKey, MetadataFieldName,
// ErrorFieldName, SourceFieldName). Use it to navigate Data and to
// decide metadata placement.
Schema Schema
// Prefix is the value attached via WithPrefix on the emitting
// logger (or set on Config.Prefix at construction), exposed
// verbatim so transports can render it independently from the
// message (e.g. tinted differently, emitted as a structured
// field, rendered in its own column).
//
// Empty string when no prefix was set.
//
// Transports that want a "prefix folded into Messages[0]"
// rendering call [transport.JoinPrefixAndMessages] at the top
// of SendToLogger; the helper has fast-path early returns for
// the no-prefix case, so the per-call cost on a logger that
// hasn't called WithPrefix is one string compare.
Prefix string
}
TransportParams is the fully assembled log entry passed to each transport.
Data contains the assembled fields and error. Metadata carries the raw value passed to WithMetadata; the transport is responsible for serializing it in whatever way suits the underlying library.
type UngroupedMode ¶
type UngroupedMode uint8
UngroupedMode is the routing strategy for entries that have no group tag.
const ( // UngroupedToAll routes ungrouped entries to every transport. Default. UngroupedToAll UngroupedMode = iota // UngroupedToNone drops ungrouped entries entirely. UngroupedToNone // UngroupedToTransports routes ungrouped entries only to the transport // IDs listed in UngroupedRouting.Transports. UngroupedToTransports )
type UngroupedRouting ¶
type UngroupedRouting struct {
// Mode selects the routing strategy. Zero value is UngroupedToAll.
Mode UngroupedMode
// Transports is the allowlist used when Mode == UngroupedToTransports.
// Ignored for the other modes.
Transports []string
}
UngroupedRouting controls how entries with no group tag are dispatched when group routing is configured.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
custom-transport
command
Implementing a custom Transport (renderer / "flatten" policy).
|
Implementing a custom Transport (renderer / "flatten" policy). |
|
custom-transport-attribute
command
Implementing a custom Transport (attribute-forwarding / wrapper policy).
|
Implementing a custom Transport (attribute-forwarding / wrapper policy). |
|
internal
|
|
|
lltest
Package lltest provides a TestTransport and TestLoggingLibrary for use in the main module's own tests.
|
Package lltest provides a TestTransport and TestLoggingLibrary for use in the main module's own tests. |
|
Package transport defines the Transport interface and BaseTransport helper used by all LogLayer transport implementations.
|
Package transport defines the Transport interface and BaseTransport helper used by all LogLayer transport implementations. |
|
benchtest
Package benchtest provides shared benchmark helpers for LogLayer transport and framework benchmarks.
|
Package benchtest provides shared benchmark helpers for LogLayer transport and framework benchmarks. |
|
transporttest
Package transporttest provides helpers and a contract test suite for LogLayer transport implementations.
|
Package transporttest provides helpers and a contract test suite for LogLayer transport implementations. |
|
utils
|
|
|
idgen
Package idgen produces opaque, near-unique identifiers for plugins, transports, and any other LogLayer machinery that needs a stable handle when the caller didn't supply one.
|
Package idgen produces opaque, near-unique identifiers for plugins, transports, and any other LogLayer machinery that needs a stable handle when the caller didn't supply one. |
|
maputil
Package maputil provides value-conversion and deep-clone helpers used by LogLayer transports and plugins.
|
Package maputil provides value-conversion and deep-clone helpers used by LogLayer transports and plugins. |
|
sanitize
Package sanitize cleans user-controlled strings before they're written to a terminal or log line, so untrusted input can't forge log lines (CR / LF), smuggle ANSI escape sequences (ESC), spoof text direction (U+202E "Trojan Source" bidi overrides), or hide content (zero-width joiners and other Unicode formatting characters).
|
Package sanitize cleans user-controlled strings before they're written to a terminal or log line, so untrusted input can't forge log lines (CR / LF), smuggle ANSI escape sequences (ESC), spoof text direction (U+202E "Trojan Source" bidi overrides), or hide content (zero-width joiners and other Unicode formatting characters). |