loglayer

package module
v2.0.1 Latest Latest
Warning

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

Go to latest
Published: May 3, 2026 License: MIT Imports: 15 Imported by: 0

README

LogLayer logo by Akshaya Madhavan

LogLayer for Go

Latest version Go Reference CI

loglayer-go is a unified logger that routes logs to various logging libraries, cloud providers, files, terminals, and OpenTelemetry while providing a fluent API for specifying log messages, fields, metadata, and errors.

Requires Go 1.25+ for the main module.

For full documentation, read the docs.

// Example using the Structured (JSON) transport.
// You can start out with one transport and swap to another later
// without touching application code.
import (
    "errors"

    "go.loglayer.dev/v2"
    "go.loglayer.dev/transports/structured/v2"
)

log := loglayer.New(loglayer.Config{
    Transport: structured.New(structured.Config{}),
    // Put fields under "context" and metadata under "metadata"
    // (defaults are flattened to the root).
    FieldsKey:         "context",
    MetadataFieldName: "metadata",
})

// Persistent fields that appear on every subsequent log
log = log.WithFields(loglayer.Fields{
    "path":  "/",
    "reqId": "1234",
})

log.WithPrefix("[my-app]").
    WithError(errors.New("test")).
    // Data attached to this log entry only
    WithMetadata(loglayer.Metadata{"some": "data"}).
    Info("my message")
{
  "level": "info",
  "time": "2026-04-26T12:00:00Z",
  "msg": "[my-app] my message",
  "context": {
    "path": "/",
    "reqId": "1234"
  },
  "metadata": {
    "some": "data"
  },
  "err": {
    "message": "test"
  }
}

Table of contents

Install

go get go.loglayer.dev/v2

Documentation

For detailed documentation, visit go.loglayer.dev.

TypeScript counterpart

Coming from loglayer for TypeScript? See For TypeScript Developers for the API mapping and Go-specific differences.

Contributing

This is a multi-module repo: the framework core lives at the root (go.loglayer.dev/v2); every transport, plugin, and integration ships as its own independently-versioned Go module under transports/, plugins/, and integrations/.

  • Dev-loop on-ramp (prerequisites, hooks, make targets, commits, tests, docs, releases via monorel): CONTRIBUTING.md.
  • Architectural context (multi-module split, thread-safety contract, performance log, release flow internals): AGENTS.md.

transports/blank/ is the copyable template for adding a new transport, plugin, or integration; the full recipe is in AGENTS.md → Adding a new transport, plugin, or integration.

Issues and questions

Bug reports, feature requests, and architectural questions go in GitHub Issues.

License

MIT

Made with ❤️ by Theo Gravity / Disaresta. Logo by Akshaya Madhavan.

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

Examples

Constants

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

View Source
const LazyEvalError = "[LazyEvalError]"

LazyEvalError is the placeholder substituted into a log entry when a Lazy callback panics.

Variables

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

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

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

func ActiveGroupsFromEnv(name string) []string

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

func NewContext(parent context.Context, log *LogLayer) context.Context

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

func UnwrappingErrorSerializer(err error) map[string]any

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

type Data map[string]any

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

type ErrorSerializer func(err error) map[string]any

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

type Fields map[string]any

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

type FieldsHook interface {
	OnFieldsCalled(fields Fields) Fields
}

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

func Lazy(fn func() any) *LazyValue

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

func Build(config Config) (*LogLayer, error)

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

func FromContext(ctx context.Context) *LogLayer

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

func MustFromContext(ctx context.Context) *LogLayer

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

func New(config Config) *LogLayer

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

func (l *LogLayer) AddGroup(name string, group LogGroup) *LogLayer

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

func (l *LogLayer) AddPlugin(plugins ...Plugin) *LogLayer

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

func (l *LogLayer) AddTransport(transports ...Transport) *LogLayer

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

func (l *LogLayer) Child() *LogLayer

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

func (l *LogLayer) ClearActiveGroups() *LogLayer

ClearActiveGroups removes the active-groups filter, returning the logger to "all defined groups are active."

func (*LogLayer) Debug

func (l *LogLayer) Debug(messages ...any)

Debug logs at the debug level.

func (*LogLayer) DisableGroup

func (l *LogLayer) DisableGroup(name string) *LogLayer

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

func (l *LogLayer) DisableLevel(level LogLevel) *LogLayer

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

func (l *LogLayer) DisableLogging() *LogLayer

DisableLogging suppresses all log output regardless of individual level state.

Safe to call concurrently with log emission: mutates an atomic bitmap.

func (*LogLayer) EnableGroup

func (l *LogLayer) EnableGroup(name string) *LogLayer

EnableGroup re-enables a previously disabled group. No-op when the group is not registered.

func (*LogLayer) EnableLevel

func (l *LogLayer) EnableLevel(level LogLevel) *LogLayer

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

func (l *LogLayer) EnableLogging() *LogLayer

EnableLogging re-enables all logging after DisableLogging.

Safe to call concurrently with log emission: mutates an atomic bitmap.

func (*LogLayer) Error

func (l *LogLayer) Error(messages ...any)

Error logs at the error level.

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

func (l *LogLayer) Fatal(messages ...any)

Fatal logs at the fatal level. Calls os.Exit(1) after dispatch unless Config.DisableFatalExit is set.

func (*LogLayer) GetFields

func (l *LogLayer) GetFields() Fields

GetFields returns a shallow copy of the current persistent fields.

func (*LogLayer) GetGroups

func (l *LogLayer) GetGroups() map[string]LogGroup

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

func (l *LogLayer) GetLoggerInstance(id string) any

GetLoggerInstance returns the underlying logger instance for the transport with the given ID, or nil if not found.

func (*LogLayer) GetPlugin

func (l *LogLayer) GetPlugin(id string) (Plugin, bool)

GetPlugin returns the registered plugin with the given ID, or (nil, false) if no plugin with that ID is registered.

func (*LogLayer) Info

func (l *LogLayer) Info(messages ...any)

Info logs at the info level.

func (*LogLayer) IsLevelEnabled

func (l *LogLayer) IsLevelEnabled(level LogLevel) bool

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

func (l *LogLayer) MuteFields() *LogLayer

MuteFields disables persistent fields from appearing in log output.

Safe to call concurrently with log emission: backed by atomic.Bool.

func (*LogLayer) MuteMetadata

func (l *LogLayer) MuteMetadata() *LogLayer

MuteMetadata disables metadata from appearing in log output.

Safe to call concurrently with log emission: backed by atomic.Bool.

func (*LogLayer) NewLogLogger

func (l *LogLayer) NewLogLogger(level LogLevel) *log.Logger

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

func (l *LogLayer) Panic(messages ...any)

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

func (l *LogLayer) PluginCount() int

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

func (l *LogLayer) RemoveGroup(name string) bool

RemoveGroup deletes a group by name. Returns true if the group was present.

Safe to call from any goroutine.

func (*LogLayer) RemovePlugin

func (l *LogLayer) RemovePlugin(id string) bool

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

func (l *LogLayer) RemoveTransport(id string) bool

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

func (l *LogLayer) SetActiveGroups(first string, more ...string) *LogLayer

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

func (l *LogLayer) SetGroupLevel(name string, level LogLevel) *LogLayer

SetGroupLevel updates the minimum level for a group. No-op when the group is not registered.

func (*LogLayer) SetLevel

func (l *LogLayer) SetLevel(level LogLevel) *LogLayer

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

func (l *LogLayer) SetTransports(transports ...Transport) *LogLayer

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

func (l *LogLayer) Trace(messages ...any)

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

func (l *LogLayer) UnmuteFields() *LogLayer

UnmuteFields re-enables persistent fields in log output.

Safe to call concurrently with log emission: backed by atomic.Bool.

func (*LogLayer) UnmuteMetadata

func (l *LogLayer) UnmuteMetadata() *LogLayer

UnmuteMetadata re-enables metadata in log output.

Safe to call concurrently with log emission: backed by atomic.Bool.

func (*LogLayer) Warn

func (l *LogLayer) Warn(messages ...any)

Warn logs at the warn level.

func (*LogLayer) WithContext

func (l *LogLayer) WithContext(ctx context.Context) *LogLayer

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

func (l *LogLayer) WithFields(f Fields) *LogLayer

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

func (l *LogLayer) WithGroup(groups ...string) *LogLayer

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

func (l *LogLayer) WithPrefix(prefix string) *LogLayer

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

func (l *LogLayer) WithoutFields(keys ...string) *LogLayer

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

func (l *LogLayer) Writer(level LogLevel) io.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.

const (
	LogLevelTrace LogLevel = 5
	LogLevelDebug LogLevel = 10
	LogLevelInfo  LogLevel = 20
	LogLevelWarn  LogLevel = 30
	LogLevelError LogLevel = 40
	LogLevelFatal LogLevel = 50
	LogLevelPanic LogLevel = 60
)

func ParseLogLevel

func ParseLogLevel(s string) (LogLevel, bool)

ParseLogLevel converts a string level name to a LogLevel. Returns LogLevelInfo and false if the name is not recognized.

func (LogLevel) String

func (l LogLevel) String() string

String returns the lowercase string name of a log level.

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

type Metadata map[string]any

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

type MetadataHook interface {
	OnMetadataCalled(metadata any) any
}

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

func NewFieldsHook(id string, fn func(Fields) Fields) Plugin

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

func NewMetadataHook(id string, fn func(any) any) Plugin

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

func NewPlugin(id string) Plugin

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

func WithErrorReporter(p Plugin, onError func(error)) Plugin

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

func SourceFromPC(pc uintptr) *Source

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.

func (*Source) LogValue

func (s *Source) LogValue() slog.Value

LogValue makes Source implement slog.LogValuer so a source attached to a slog logger (via the slog transport) renders as a nested {function, file, line} group rather than a stringified struct.

func (*Source) String

func (s *Source) String() string

String returns a compact "function file:line" rendering, used by console and pretty transports that fall back to %v for unknown values. Empty fields are omitted; the result never has trailing whitespace.

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.

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).

Jump to

Keyboard shortcuts

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