# LogLayer for Go > Transport-agnostic structured logging for Go. A fluent API on top of zerolog, zap, logrus, phuslu/log, charmbracelet/log, log/slog, OpenTelemetry, or any custom transport. Module path: `go.loglayer.dev/v2`. GitHub: `github.com/loglayer/loglayer-go`. ## Installation ```sh go get go.loglayer.dev/v2 ``` Most transports and plugins ship as their own modules. Install only what you import: ```sh go get go.loglayer.dev/transports/structured/v2 go get go.loglayer.dev/transports/zerolog/v2 go get go.loglayer.dev/plugins/redact/v2 ``` ## Quick Start ```go package main import ( "go.loglayer.dev/v2" "go.loglayer.dev/transports/structured/v2" ) func main() { log := loglayer.New(loglayer.Config{ Transport: structured.New(structured.Config{}), }) log.Info("hello world") } ``` ## Log Levels Seven levels, in priority order: ```go log.Trace("detailed debugging") log.Debug("debug information") log.Info("informational message") log.Warn("warning message") log.Error("error occurred") log.Fatal("critical failure") // dispatches then calls os.Exit(1) unless DisableFatalExit log.Panic("unrecoverable") // dispatches then panics with the joined message string // Multiple values join with a space log.Info("user", 123, "logged in") ``` ## Metadata (per-message data) Data attached to a single log entry only. ```go // Map-style metadata flattens to root keys log.WithMetadata(loglayer.Metadata{"userId": "123", "action": "login"}).Info("user logged in") // loglayer.M is a shorter alias for loglayer.Metadata log.WithMetadata(loglayer.M{"durationMs": 42}).Info("served") // Struct metadata nests under the "metadata" key by default type Event struct { OrderID string `json:"orderId"` Path string `json:"path"` } log.WithMetadata(Event{OrderID: "o-1", Path: "/checkout"}).Info("event") // Just metadata, no message text log.MetadataOnly(loglayer.M{"queueDepth": 17}) ``` ## Lazy Evaluation Defer expensive `Fields` computation until dispatch time. The callback runs only when the level passes, on every emission. Recognized only as the direct value of a `Fields` (or `RawLogEntry.Fields`) key. ```go log = log.WithFields(loglayer.Fields{ "heap_kb": loglayer.Lazy(func() any { var m runtime.MemStats; runtime.ReadMemStats(&m) return m.HeapAlloc / 1024 }), }) ``` A panicking callback substitutes `loglayer.LazyEvalError` and the rest of the entry still emits. Callbacks may run concurrently across goroutines; they must be thread-safe. ## Multi-line Messages - loglayer.Multiline(lines ...any): wrap multi-line message content that survives terminal sanitize. https://go.loglayer.dev/logging-api/multiline ## Fields (persistent data across all log entries) Fields ride on every emission from the logger they're set on. Always assign the result; `WithFields` returns a new `*LogLayer`. ```go log = log.WithFields(loglayer.Fields{ "requestId": "abc-123", "service": "auth", }) log.Info("processing") // includes requestId and service log.Info("done") // still includes them // loglayer.F is a shorter alias log = log.WithFields(loglayer.F{"region": "us3"}) // Drop specific keys (or all keys with no args) log = log.WithoutFields("requestId") // Mute / unmute persistent fields without losing them log = log.MuteFields() log = log.UnmuteFields() // Nest fields under a single key (e.g. for Datadog facets) loglayer.New(loglayer.Config{ Transport: structured.New(structured.Config{}), FieldsKey: "context", }) ``` ## Error Handling ```go // Error with a message log.WithError(errors.New("connection failed")).Error("DB error") // Error only (no extra message) log.ErrorOnly(errors.New("connection failed")) // Combine error with metadata log.WithError(errors.New("timeout")). WithMetadata(loglayer.M{"query": "SELECT *", "durationMs": 1500}). Error("query failed") ``` ## Error Serialization (recommended) Default serializer emits `{"message": err.Error()}`. To unwrap error chains and surface every cause, use the built-in opt-in: ```go log := loglayer.New(loglayer.Config{ Transport: structured.New(structured.Config{}), ErrorSerializer: loglayer.UnwrappingErrorSerializer, // walks errors.Unwrap and errors.Join's Unwrap() []error }) ``` Or supply your own (e.g. with [`rotisserie/eris`](https://github.com/rotisserie/eris) for stack traces): ```go loglayer.New(loglayer.Config{ Transport: structured.New(structured.Config{}), ErrorSerializer: func(err error) map[string]any { return map[string]any{"message": err.Error(), "kind": classify(err)} }, }) ``` ## Configuration ```go log := loglayer.New(loglayer.Config{ // Required: one of these Transport: someTransport, // single transport Transports: []loglayer.Transport{t1, t2}, // multi-transport (mutually exclusive with Transport) // Optional Prefix: "[auth]", // surfaced to transports as TransportParams.Prefix Disabled: false, // master on/off ErrorSerializer: loglayer.UnwrappingErrorSerializer, ErrorFieldName: "err", // default CopyMsgOnOnlyError: false, // ErrorOnly copies err.Error() as the message FieldsKey: "", // empty = merge fields at root; set to nest under a key MuteFields: false, MuteMetadata: false, DisableFatalExit: false, // false = Fatal calls os.Exit(1), matching Go convention // Source / caller info (off by default; ~620 ns + 5 allocs per emission when on) Source: loglayer.SourceConfig{Enabled: true, FieldName: "source"}, // Group routing (zero value = no routing) Routing: loglayer.RoutingConfig{ Groups: map[string]loglayer.LogGroup{ "database": {Transports: []string{"datadog"}, Level: loglayer.LogLevelError}, "auth": {Transports: []string{"datadog", "console"}}, }, }, // Optional: observe panics from buggy transports' SendToLogger so they don't crash the host process OnTransportPanic: func(err *loglayer.RecoveredPanicError) { /* report */ }, // Plugins to apply at construction (in slice order) Plugins: []loglayer.Plugin{ /* ... */ }, }) // Or with explicit error handling instead of panic on misconfiguration log, err := loglayer.Build(loglayer.Config{ /* ... */ }) ``` ## Groups (route logs to specific transports) ```go log := loglayer.New(loglayer.Config{ Transports: []loglayer.Transport{ structured.New(structured.Config{BaseConfig: transport.BaseConfig{ID: "console"}}), datadog.New(datadog.Config{APIKey: key, BaseConfig: transport.BaseConfig{ID: "datadog"}}), }, Routing: loglayer.RoutingConfig{ Groups: map[string]loglayer.LogGroup{ "database": {Transports: []string{"datadog"}, Level: loglayer.LogLevelError}, "auth": {Transports: []string{"datadog", "console"}, Level: loglayer.LogLevelWarn}, }, Ungrouped: loglayer.UngroupedRouting{Mode: loglayer.UngroupedToAll}, // default }, }) // Per-log tagging log.WithGroup("database").Error("connection lost") log.WithGroup("database", "auth").Error("auth DB failure") // Persistent tagging (child logger) dbLog := log.WithGroup("database") dbLog.Error("pool exhausted") // Runtime management log.AddGroup("metrics", loglayer.LogGroup{Transports: []string{"console"}}) log.RemoveGroup("metrics") log.EnableGroup("auth") log.DisableGroup("auth") log.SetGroupLevel("database", loglayer.LogLevelDebug) log.SetActiveGroups("database", "auth") // only these groups active log.ClearActiveGroups() // all groups active // Filter active groups via env var (e.g. LOGLAYER_GROUPS=database,auth) loglayer.New(loglayer.Config{ Routing: loglayer.RoutingConfig{ ActiveGroups: loglayer.ActiveGroupsFromEnv("LOGLAYER_GROUPS"), // ... }, }) ``` The merged group slice for an entry is also surfaced to transports as `TransportParams.Groups` and to all four dispatch-time plugin hooks (`BeforeDataOutParams.Groups`, `BeforeMessageOutParams.Groups`, `TransformLogLevelParams.Groups`, `ShouldSendParams.Groups`). Use it to ship groups in a transport's wire payload, or to drive group-aware transformations. The `WithPrefix` value (or `Config.Prefix`) is surfaced to transports as `TransportParams.Prefix` and to the same four dispatch-time plugin hooks. Transports can render the prefix independently from the message body (e.g. tinted differently, emitted as a structured field). The core does NOT prepend the prefix into `Messages[0]`; transports that want a "prefix folded into the message" rendering call `transport.JoinPrefixAndMessages(p.Prefix, p.Messages)` at the top of `SendToLogger`. ## Child Loggers ```go parent := loglayer.New(loglayer.Config{Transport: structured.New(structured.Config{})}) parent = parent.WithFields(loglayer.F{"service": "api"}) // Independent clone (mutations don't affect the parent) child := parent.Child() child = child.WithFields(loglayer.F{"handler": "users"}) child.Info("request received") // emits with both service and handler ``` ## Message Prefixing ```go log := loglayer.New(loglayer.Config{ Transport: structured.New(structured.Config{}), Prefix: "[MyApp]", }) log.Info("started") // "[MyApp] started" // Or dynamically (returns a new logger) prefixed := log.WithPrefix("[Auth]") prefixed.Info("login") // "[Auth] login" ``` ## Log Level Control Three independent tiers; a log must pass all that apply: 1. **LogLayer (global)**: `SetLevel`, `EnableLevel`, `DisableLevel`, `EnableLogging`, `DisableLogging` 2. **Group**: `Routing.Groups[name].Level` for grouped logs only 3. **Transport**: `transport.BaseConfig{Level: ...}` per transport, checked at dispatch ```go log.SetLevel(loglayer.LogLevelWarn) // only Warn+ will log log.EnableLevel(loglayer.LogLevelDebug) log.DisableLevel(loglayer.LogLevelDebug) log.EnableLogging() log.DisableLogging() log.IsLevelEnabled(loglayer.LogLevelDebug) ``` ## Multiple Transports ```go log := loglayer.New(loglayer.Config{ Transports: []loglayer.Transport{ structured.New(structured.Config{}), datadog.New(datadog.Config{APIKey: os.Getenv("DD_API_KEY")}), }, }) // Runtime management log.AddTransport(extra) log.RemoveTransport("transport-id") log.SetTransports(t1, t2) ``` ## Go context.Context Distinct from "context" in TS LogLayer: Go's `context.Context` carries trace IDs, deadlines, request-scoped values. Use `WithContext` to attach it; transports and plugins read it via `TransportParams.Ctx`. ```go // Per-call attachment log.WithContext(ctx).Info("request received") // Persistent binding (returns a new logger) reqLog := log.WithContext(ctx) reqLog.Info("step 1") reqLog.Info("step 2") ``` For HTTP handlers, the [loghttp middleware](https://go.loglayer.dev/integrations/loghttp) auto-binds `r.Context()` to a per-request logger. ## Source / Caller Info Capture file/line/function of every emission. Off by default; ~620 ns + 5 allocs per emission when on. ```go log := loglayer.New(loglayer.Config{ Transport: structured.New(structured.Config{}), Source: loglayer.SourceConfig{Enabled: true, FieldName: "source"}, }) ``` The slog handler (`integrations/sloghandler`) forwards `slog.Record.PC` automatically, no `Source.Enabled` needed. ## Testing / Mocking ```go // Silent mock: same API, emits nothing, Fatal does not exit log := loglayer.NewMock() // Capture entries for assertion import lltest "go.loglayer.dev/transports/testing/v2" lib := &lltest.TestLoggingLibrary{} log := loglayer.New(loglayer.Config{ Transport: lltest.New(lltest.Config{Library: lib}), }) log.Info("hello", loglayer.F{"k": "v"}) lines := lib.Lines() // []lltest.LogLine; assert on Level, Messages, Data, Metadata, Ctx ``` ## Available Transports **Renderers** (format and write to an `io.Writer`): - `transports/console`: plain `fmt.Println`-style - `transports/structured`: one JSON object per entry (production) - `transports/pretty`: colorized terminal output (local dev) - `transports/cli`: tuned for command-line apps (short level prefixes, stdout/stderr routing, TTY-detected color, no timestamps, table rendering for slice-of-map metadata with `Config.TableColumnOrder` to pin leading columns) - `transports/testing`: in-memory capture for tests - `transports/blank`: user-supplied dispatch function **Cloud** (managed log services): - `transports/axiom`: ships logs to Axiom via caller-supplied `*axiom.Client` - `transports/betterstack`: ships logs to Better Stack via HTTP intake - `transports/datadog`: Datadog Logs HTTP intake - `transports/gcplogging`: wraps a caller-supplied `*logging.Logger` from `cloud.google.com/go/logging` - `transports/sentry`: wraps a caller-supplied `sentry.Logger` **Other transports**: - `transports/http`: generic batched HTTP POST - `transports/lumberjack`: one JSON object per line, rotating file via `lumberjack.v2` - `transports/otellog`: emits to an OTel `log.Logger`; forwards `WithContext` for trace correlation **Wrappers around third-party loggers**: - `transports/charmlog`, `transports/logrus`, `transports/phuslu`, `transports/slog`, `transports/zap`, `transports/zerolog` ## Available Plugins - `plugins/redact`: replace values for configured keys/patterns - `plugins/sampling`: drop a fraction of emissions (`FixedRate`, `FixedRatePerLevel`, `Burst`) - `plugins/fmtlog`: `fmt.Sprintf` semantics for multi-arg messages - `plugins/datadogtrace`: inject `dd.trace_id` / `dd.span_id` (tracer-agnostic; bring your own dd-trace-go) - `plugins/oteltrace`: inject OTel `trace_id` / `span_id` / `trace_flags` / W3C `trace_state` and baggage ## Available Integrations - `integrations/loghttp`: HTTP middleware. Per-request logger with `requestId`/`method`/`path`, auto request-completed log, request-context binding. Works with chi, gorilla, gin, echo, stdlib. - `integrations/sloghandler`: `log/slog.Handler` backed by a loglayer logger. `slog.SetDefault(slog.New(sloghandler.New(log)))` makes every `slog.Info(...)` flow through loglayer's plugin pipeline and group routing. ## Documentation - [Getting Started](https://go.loglayer.dev/getting-started): Installation and basic usage - [Configuration](https://go.loglayer.dev/configuration): All `loglayer.Config` fields - [Cheat Sheet](https://go.loglayer.dev/cheatsheet): One-page API reference - [Basic Logging](https://go.loglayer.dev/logging-api/basic-logging): Level methods, prefix, message assembly - [Fields](https://go.loglayer.dev/logging-api/fields): Persistent keyed data - [Metadata](https://go.loglayer.dev/logging-api/metadata): Per-message structured data - [Error Handling](https://go.loglayer.dev/logging-api/error-handling): Errors, serializers, unwrap chains - [Go Context](https://go.loglayer.dev/logging-api/go-context): `WithContext` for `context.Context` - [Child Loggers](https://go.loglayer.dev/logging-api/child-loggers): `Child()`, isolation semantics - [Groups](https://go.loglayer.dev/logging-api/groups): Per-transport routing - [Adjusting Log Levels](https://go.loglayer.dev/logging-api/adjusting-log-levels): Three-tier level control - [Raw Logging](https://go.loglayer.dev/logging-api/raw): `Raw(RawLogEntry)` bypasses the builder - [Multi-line messages](https://go.loglayer.dev/logging-api/multiline): `loglayer.Multiline` for authored multi-line output - [Log Sanitization](https://go.loglayer.dev/log-sanitization): What gets sanitized, where, and the transport-author decision tree - [Mocking](https://go.loglayer.dev/logging-api/mocking): `loglayer.NewMock()` and `transports/testing` - [Transport Overview](https://go.loglayer.dev/transports/): All transports - [Plugins Overview](https://go.loglayer.dev/plugins/): Plugin system and hooks - [For TypeScript Developers](https://go.loglayer.dev/for-typescript-developers): API mapping from `loglayer` (TS) to `go.loglayer.dev/v2` ## Optional - [Creating Transports](https://go.loglayer.dev/transports/creating-transports): The `Transport` interface and `BaseTransport` - [Creating Plugins](https://go.loglayer.dev/plugins/creating-plugins): Six lifecycle hooks - [Testing Transports](https://go.loglayer.dev/transports/testing-transports): The 14-test contract suite (`transport/transporttest.RunContract`) - [Testing Plugins](https://go.loglayer.dev/plugins/testing-plugins): `plugins/plugintest.Install`, assertion helpers - [Multiple Transports](https://go.loglayer.dev/transports/multiple-transports): Fan-out semantics - [Transport Configuration](https://go.loglayer.dev/transports/configuration): `BaseConfig`, transport IDs - [Transport Management](https://go.loglayer.dev/transports/management): Add / Remove / Set / GetLoggerInstance - [Plugin Management](https://go.loglayer.dev/plugins/management): Add / Remove / Get - [Plugin Configuration](https://go.loglayer.dev/plugins/configuration): Construction-time vs runtime