# LogLayer for Go: Comprehensive LLM Reference > 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`. This is the comprehensive reference. For the concise index see [llms.txt](https://go.loglayer.dev/llms.txt). ## Installation ```sh go get go.loglayer.dev/v2 ``` LogLayer is multi-module: most transports, plugins, and integrations ship as their own Go modules. Install only what you import. ```sh # Renderer transports go get go.loglayer.dev/transports/structured/v2 go get go.loglayer.dev/transports/console/v2 go get go.loglayer.dev/transports/pretty/v2 go get go.loglayer.dev/transports/cli/v2 go get go.loglayer.dev/transports/testing/v2 go get go.loglayer.dev/transports/blank/v2 # Cloud (managed log services) go get go.loglayer.dev/transports/axiom/v2 go get go.loglayer.dev/transports/betterstack/v2 go get go.loglayer.dev/transports/datadog/v2 go get go.loglayer.dev/transports/gcplogging/v2 go get go.loglayer.dev/transports/sentry/v2 # Other transports go get go.loglayer.dev/transports/http/v2 go get go.loglayer.dev/transports/lumberjack/v2 go get go.loglayer.dev/transports/otellog/v2 # Wrappers around third-party loggers go get go.loglayer.dev/transports/charmlog/v2 go get go.loglayer.dev/transports/logrus/v2 go get go.loglayer.dev/transports/phuslu/v2 go get go.loglayer.dev/transports/slog/v2 go get go.loglayer.dev/transports/zap/v2 go get go.loglayer.dev/transports/zerolog/v2 # Plugins go get go.loglayer.dev/plugins/redact/v2 go get go.loglayer.dev/plugins/sampling/v2 go get go.loglayer.dev/plugins/fmtlog/v2 go get go.loglayer.dev/plugins/datadogtrace/v2 go get go.loglayer.dev/plugins/oteltrace/v2 # Integrations go get go.loglayer.dev/integrations/loghttp/v2 go get go.loglayer.dev/integrations/sloghandler/v2 ``` The full module list is in [`monorel.toml`](https://github.com/loglayer/loglayer-go/blob/main/monorel.toml). ## 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{}), MetadataFieldName: "metadata", }) log.Info("hello world") log.WithMetadata(loglayer.M{"userId": 42}).Info("user logged in") log.WithError(errors.New("failed")).Error("operation aborted") } ``` `loglayer.New` panics on misconfiguration (no transport set; reports `loglayer.ErrNoTransport`). For explicit error handling, use `loglayer.Build(Config) (*LogLayer, error)`. ## Core Concepts LogLayer separates structured data into three categories with distinct scopes: | Category | Method | Scope | Purpose | |----------|---------------|----------------------------|----------------------------------------| | Fields | WithFields | Persistent across all logs | Request IDs, user info, session data | | Metadata | WithMetadata | Single log entry only | Per-event details: durations, counts | | Errors | WithError | Single log entry only | An error value, serialized for output | Each category can be muted, renamed, or nested under a configurable field. ## Log Levels Seven levels, integer-typed, in priority order: - `loglayer.LogLevelTrace` (5) - `loglayer.LogLevelDebug` (10) - `loglayer.LogLevelInfo` (20) - `loglayer.LogLevelWarn` (30) - `loglayer.LogLevelError` (40) - `loglayer.LogLevelFatal` (50): dispatches then `os.Exit(1)` unless `DisableFatalExit` is set - `loglayer.LogLevelPanic` (60): dispatches then panics with the joined message string (recoverable; matches zerolog/zap/logrus) ```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") log.Panic("unrecoverable") // Multiple values join with a space log.Info("user", 123, "logged in") ``` ## Metadata (per-message structured data) ```go // Map metadata flattens to root keys log.WithMetadata(loglayer.Metadata{"userId": "123", "action": "login"}).Info("user logged in") // loglayer.M is a shorter alias log.WithMetadata(loglayer.M{"durationMs": 42}).Info("served") // Struct metadata nests under "metadata" by default type Event struct { OrderID string `json:"orderId"` Path string `json:"path"` } log.WithMetadata(Event{OrderID: "o-1", Path: "/checkout"}).Info("event") ``` ### Metadata-Only Logging ```go // Just metadata, no message text log.MetadataOnly(loglayer.M{"queueDepth": 17}) // Override the level (default is Info) log.MetadataOnly(loglayer.M{"queueDepth": 17}, loglayer.MetadataOnlyOpts{ LogLevel: loglayer.LogLevelWarn, }) ``` ### Nested Metadata Field `loglayer.Config.MetadataFieldName` (a single core knob) nests both map and non-map metadata under one configurable key uniformly across every transport. When unset, each transport keeps its default policy: renderers flatten map metadata at root, wrappers flatten map metadata as individual attributes and nest non-map values under a hardcoded `"metadata"` key. ```go loglayer.New(loglayer.Config{ Transport: otellog.New(otellog.Config{Name: "checkout-api"}), MetadataFieldName: "user", }) ``` ### Muting Metadata ```go // Construction-time loglayer.New(loglayer.Config{ Transport: someTransport, MuteMetadata: true, }) // Runtime log = log.MuteMetadata() log = log.UnmuteMetadata() ``` ## Lazy Evaluation Adapted from [LogTape's lazy evaluation](https://logtape.org/manual/lazy). Wrap a `Fields` value with `loglayer.Lazy(fn)` so it runs only at dispatch time, after the level filter, on every emission. Recognized only as the direct value of a `Fields` (or `RawLogEntry.Fields`) key. A `*LazyValue` nested inside another value (a map, slice, struct field) is not resolved. ```go log = log.WithFields(loglayer.Fields{ "service": "api", "heap_kb": loglayer.Lazy(func() any { var m runtime.MemStats; runtime.ReadMemStats(&m) return m.HeapAlloc / 1024 }), }) log.Info("starting work") // heap_kb captured here log.Info("done") // captured fresh again ``` If the callback panics, the recovered placeholder `loglayer.LazyEvalError` (`"[LazyEvalError]"`) is substituted into the entry. The rest of the entry still sends so other fields aren't lost. Callbacks may be invoked concurrently across goroutines if the same `*LogLayer` is shared; they must be thread-safe. Plugin call-time hooks (`OnFieldsCalled`) see the raw `*LazyValue` wrapper; dispatch-time hooks (`OnBeforeDataOut`, `OnBeforeMessageOut`, `TransformLogLevel`) see the resolved value. ## Multi-line Messages - loglayer.Multiline(lines ...any) returns a *MultilineMessage that terminal-renderer transports (cli, pretty, console) interpret as authored "\n" boundaries. JSON sinks and wrapper transports flatten via Stringer/MarshalJSON. Each authored line is still sanitized individually. The wrapper is honored only as a positional message argument; values placed inside Fields or Metadata still collapse to one line in terminal renderers. https://go.loglayer.dev/logging-api/multiline ## Log Sanitization - The cli, pretty, and console transports strip control bytes (CR / LF / ANSI ESC / bidi controls / zero-width chars) from user-controlled message strings before writing, to defeat log forging, terminal-escape smuggling, and Trojan Source attacks. Untrusted input cannot become multi-line by accident; the loglayer.Multiline wrapper is the developer's explicit opt-in. - structured and every wrapper transport (zerolog, zap, slog, logrus, charmlog, phuslu, sentry, otellog, gcplogging, http, datadog, testing) do NOT call sanitize.Message; their JSON encoders escape control bytes automatically. - Helpers: utils/sanitize.Message(string) string strips per-rune; transport.AssembleMessage(messages, sanitize) handles per-line sanitize with Multiline-aware joining for terminal transports. https://go.loglayer.dev/log-sanitization ## Fields (persistent data across all log entries) Fields ride on every emission from the logger they're set on. `WithFields` returns a new `*LogLayer`; always assign the result. ```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 log = log.WithoutFields("requestId") // Drop all fields log = log.WithoutFields() ``` ### Nested Fields Field ```go log := loglayer.New(loglayer.Config{ Transport: structured.New(structured.Config{}), FieldsKey: "context", // empty default = merge at root }) log = log.WithFields(loglayer.F{"requestId": "abc"}) log.Info("hi") // {"level":"info","msg":"hi","context":{"requestId":"abc"}} ``` ### Muting Fields ```go // Construction-time loglayer.New(loglayer.Config{Transport: t, MuteFields: true}) // Runtime log = log.MuteFields() log = log.UnmuteFields() ``` ## Error Handling ### Error with a Message ```go log.WithError(errors.New("connection failed")).Error("DB error") ``` ### Error-Only Logging ```go log.ErrorOnly(errors.New("connection failed")) // Override the level / copy err.Error() as the message for this call log.ErrorOnly(err, loglayer.ErrorOnlyOpts{ LogLevel: loglayer.LogLevelWarn, CopyMsg: loglayer.CopyMsgEnabled, }) ``` ### Error Configuration ```go loglayer.New(loglayer.Config{ Transport: t, ErrorFieldName: "err", // default CopyMsgOnOnlyError: true, // ErrorOnly() copies err.Error() as the message }) ``` ### Error Serialization (recommended) Default serializer emits `{"message": err.Error()}`. To unwrap error chains and surface every cause as a `causes` array: ```go loglayer.New(loglayer.Config{ Transport: t, ErrorSerializer: loglayer.UnwrappingErrorSerializer, }) ``` Walks `errors.Unwrap` and `errors.Join`'s `Unwrap() []error`. Bounded against cyclic Unwrap. Custom serializer (e.g. with [`rotisserie/eris`](https://github.com/rotisserie/eris) for stack traces): ```go loglayer.New(loglayer.Config{ Transport: t, ErrorSerializer: func(err error) map[string]any { return map[string]any{ "message": err.Error(), "kind": classify(err), } }, }) ``` Returning `nil` drops the error field entirely. Returning an empty map adds an empty err object. ### Combining Errors with Other Data ```go log.WithError(errors.New("timeout")). WithMetadata(loglayer.M{"query": "SELECT *", "durationMs": 1500}). Error("query failed") ``` ## Configuration ### Full Configuration Example ```go log := loglayer.New(loglayer.Config{ // Required: one of these (mutually exclusive) Transport: someTransport, Transports: []loglayer.Transport{t1, t2}, Prefix: "[auth]", Disabled: false, // Errors ErrorSerializer: loglayer.UnwrappingErrorSerializer, ErrorFieldName: "err", CopyMsgOnOnlyError: false, // Field/metadata layout FieldsKey: "", // empty = merge at root MuteFields: false, MuteMetadata: false, // Fatal behavior DisableFatalExit: false, // false = Fatal calls os.Exit(1) TransportCloseTimeout: 5 * time.Second, // close-on-fatal cap // 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{ /* ... */ }, ActiveGroups: []string{"database", "auth"}, Ungrouped: loglayer.UngroupedRouting{Mode: loglayer.UngroupedToAll}, }, // Plugins applied at construction (in slice order) Plugins: []loglayer.Plugin{ /* ... */ }, // Opt-in transport-panic recovery (default nil = panic propagates per Go convention) OnTransportPanic: func(err *loglayer.RecoveredPanicError) { /* report */ }, }) // With explicit error handling log, err := loglayer.Build(loglayer.Config{ /* ... */ }) ``` ## Message Prefixing ```go log := loglayer.New(loglayer.Config{ Transport: someTransport, Prefix: "[MyApp]", }) log.Info("started") // "[MyApp] started" // Or dynamically (returns a new logger) prefixed := log.WithPrefix("[Auth]") prefixed.Info("login") // "[Auth] login" ``` ## Child Loggers ```go parent := loglayer.New(loglayer.Config{Transport: t}) parent = parent.WithFields(loglayer.F{"service": "api"}) // Independent clone (mutations don't bleed back to parent) child := parent.Child() child = child.WithFields(loglayer.F{"handler": "users"}) child.Info("request received") // emits with service AND handler ``` `Child()` shallow-copies fields, level state, transports, plugins, and group routing. Group config is shared by reference (runtime changes propagate); persistent group tags from `WithGroup` are copied. ## Log Level Control Three independent tiers; a log must pass all that apply: 1. **LogLayer (global)**: `SetLevel`, `EnableLevel`, `DisableLevel`, `EnableLogging`, `DisableLogging`. Checked first, before any processing. 2. **Group**: `Routing.Groups[name].Level`. Only applies to grouped logs. 3. **Transport**: `transport.BaseConfig{Level: ...}` per transport, checked at dispatch time. ```go log.SetLevel(loglayer.LogLevelWarn) // raise threshold; only Warn+ logs log.EnableLevel(loglayer.LogLevelDebug) log.DisableLevel(loglayer.LogLevelDebug) log.EnableLogging() log.DisableLogging() log.IsLevelEnabled(loglayer.LogLevelDebug) ``` Lock-free internally; safe to call concurrently with emission. Mirrors `zap.AtomicLevel`. ## Raw Logging Bypass the builder when forwarding from another logging system: ```go log.Raw(loglayer.RawLogEntry{ LogLevel: loglayer.LogLevelWarn, Messages: []any{"upstream timeout"}, Metadata: loglayer.M{"retries": 3}, Fields: loglayer.F{"requestId": "abc"}, Err: err, Ctx: ctx, Groups: []string{"database"}, Source: pcSource, // pre-captured *Source; skip runtime capture }) ``` ## Multiple Transports ```go log := loglayer.New(loglayer.Config{ Transports: []loglayer.Transport{ structured.New(structured.Config{BaseConfig: transport.BaseConfig{ID: "console"}}), datadog.New(datadog.Config{ APIKey: os.Getenv("DD_API_KEY"), BaseConfig: transport.BaseConfig{ID: "datadog"}, }), }, }) // Runtime mutation (atomic, lock-free reads) log.AddTransport(extra) log.RemoveTransport("console") log.SetTransports(t1, t2) log.GetLoggerInstance("zerolog") // *zerolog.Logger if that ID is wrapped ``` Closing transports: any transport that implements `io.Closer` is closed when removed via `RemoveTransport` or replaced via `SetTransports`. `Config.TransportCloseTimeout` (default 5s) caps the wait so a wedged endpoint can't block the mutator. ## Groups (route logs to specific transports) ### Configuration ```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}, }, ActiveGroups: []string{"database", "auth"}, // optional filter Ungrouped: loglayer.UngroupedRouting{Mode: loglayer.UngroupedToAll}, }, }) ``` ### Group Options ```go type LogGroup struct { Transports []string // transport IDs this group routes to (required) Level LogLevel // optional minimum level for this group (zero = no filter) Disabled bool // suppress this group's routing } ``` ### Per-Log Tagging ```go log.WithGroup("database").Error("connection lost") log.WithGroup("database", "auth").Error("auth DB failure") ``` ### Persistent Tagging (Child Loggers) ```go dbLog := log.WithGroup("database") dbLog.Error("pool exhausted") // every emission tagged "database" ``` ### Group Level Filtering A group's `Level` filters out entries below that level for that group's transports. Other transports (via other groups, or ungrouped routing) are unaffected. ### Ungrouped Logs ```go Ungrouped: loglayer.UngroupedRouting{ Mode: loglayer.UngroupedToAll, // default: every transport gets ungrouped logs // Mode: loglayer.UngroupedToNone, // drop ungrouped logs entirely // Mode: loglayer.UngroupedToTransports, Transports: []string{"console"}, } ``` ### Active Groups Filter ```go log.SetActiveGroups("database", "auth") // only these groups active log.ClearActiveGroups() // all defined groups active ``` ### Environment Variable ```go loglayer.New(loglayer.Config{ Routing: loglayer.RoutingConfig{ ActiveGroups: loglayer.ActiveGroupsFromEnv("LOGLAYER_GROUPS"), // ... }, }) // LOGLAYER_GROUPS=database,auth ``` ### Runtime Management ```go log.AddGroup("metrics", loglayer.LogGroup{Transports: []string{"console"}}) log.RemoveGroup("metrics") log.EnableGroup("auth") log.DisableGroup("auth") log.SetGroupLevel("database", loglayer.LogLevelDebug) log.GetGroups() ``` ## Go context.Context Distinct from "context" in TypeScript LogLayer (which is what Go calls "fields"). 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 via *LogBuilder log.WithContext(ctx).Info("request received") // Persistent binding (returns a new *LogLayer; per-call still overrides) 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. Plugins like `oteltrace` and `datadogtrace` read trace IDs from `params.Ctx`. ## Source / Caller Info Capture file/line/function of every emission via `runtime.Caller`. 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"}, }) log.Info("hi") // {"level":"info","msg":"hi","source":{"function":"main.foo","file":"foo.go","line":42}} ``` `*loglayer.Source` has `json` tags matching slog convention plus `String()` and `slog.LogValuer` so non-JSON transports render readably. `loglayer.SourceFromPC(pc)` builds a Source from a captured PC. The `integrations/sloghandler` forwards `slog.Record.PC` automatically; no `Source.Enabled` needed on the loglayer side. ## Stdlib log Bridge Plumb anything that wants an `io.Writer` or `*log.Logger` (e.g. `http.Server.ErrorLog`, gorm's logger, etc.) through loglayer: ```go // io.Writer that emits at the given level w := log.Writer(loglayer.LogLevelError) // *log.Logger that emits at the given level stdlogger := log.NewLogLogger(loglayer.LogLevelError) srv := &http.Server{ErrorLog: stdlogger} ``` ## Transport Configuration ```go type BaseConfig struct { ID string Disabled bool Level LogLevel // zero = no per-transport level filter } ``` Pattern across all transports: ```go import "go.loglayer.dev/v2/transport" structured.New(structured.Config{ BaseConfig: transport.BaseConfig{ID: "main", Level: loglayer.LogLevelInfo}, Writer: os.Stdout, }) ``` ## Renderer Transports ### Console Transport Plain text, logfmt-style key/value pairs after the message. Writes to stdout (info/debug) or stderr (warn+) by default. ```go import "go.loglayer.dev/transports/console/v2" console.New(console.Config{ Writer: os.Stdout, // single writer for all levels // MessageField: "msg", // emit as a structured object instead of plain text // DateField: "time", // LevelField: "level", // Stringify: false, // JSON-encode the structured object }) ``` ### Structured Transport One JSON object per entry. Recommended for production. ```go import "go.loglayer.dev/transports/structured/v2" structured.New(structured.Config{ Writer: os.Stdout, // DateFn: func() string { return time.Now().UTC().Format(time.RFC3339) }, // LevelFn: func(l loglayer.LogLevel) string { return l.String() }, // Indent: false, // MessageField, DateField, LevelField: customize key names }) ``` ### Pretty Transport Colorized terminal output. Theme-aware (Moonlight, Sunlight, Neon, Nature, Pastel) with three view modes (inline, message-only, expanded). ```go import "go.loglayer.dev/transports/pretty/v2" pretty.New(pretty.Config{ Writer: os.Stdout, NoColor: false, ViewMode: pretty.ViewModeInline, // or ViewModeMessageOnly / ViewModeExpanded Theme: pretty.MoonlightTheme, }) ``` ### CLI Transport Tuned for command-line application output rather than diagnostic logging. Short cargo/eslint-style level prefixes (`warning:`, `error:`, `fatal:`), stdout for info/debug and stderr for warn+, TTY-detected ANSI color, no timestamps. Renders the `WithPrefix` value in dim grey, separate from the level color. Fields/metadata dropped by default; opt in via `Config.ShowFields`. ```go import "go.loglayer.dev/transports/cli/v2" cli.New(cli.Config{ // Writer: os.Stdout, // override (defaults: stdout for info/debug, stderr for warn+) // Color: cli.ColorAuto, // ColorNever / ColorAlways // ShowFields: false, // append fields/metadata after the message // TableColumnOrder: []string{"package"}, // pin leading columns for slice-of-map metadata tables; rest sort lex }) ``` For slice-of-map metadata (`[]loglayer.Metadata`, `[]map[string]any`, `[]MyStruct`), the cli transport renders a tabwriter-aligned table after the message. Columns sort lexicographically by default; `TableColumnOrder` pins specific leading columns (additive: pinned keys first in listed order, remaining keys lex-sorted and appended; pinned keys absent from every row are silently skipped). ### Testing Transport In-memory capture for assertion tests. Public package is `transports/testing`; `lltest` is the conventional import alias. ```go 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 last := lib.GetLastLine() popped := lib.PopLine() ``` ### Blank Transport Delegates dispatch to a user-supplied function. For prototyping, metrics counters, message queues. ```go import "go.loglayer.dev/transports/blank/v2" blank.New(blank.Config{ BaseConfig: transport.BaseConfig{ID: "metrics"}, ShipToLogger: func(p loglayer.TransportParams) { if p.LogLevel >= loglayer.LogLevelError { metricsClient.Increment("errors", 1) } }, }) ``` ## Cloud Transports Managed log services. Async + batched; site-aware where applicable. ### Datadog Transport Datadog Logs HTTP intake. Site-aware URL, `DD-API-KEY` header, level→status mapping. Built on the HTTP transport. ```go import "go.loglayer.dev/transports/datadog/v2" tr := datadog.New(datadog.Config{ APIKey: os.Getenv("DD_API_KEY"), // required Site: datadog.SiteUS3, // or SiteUS1/EU/AP1, or use URL: ... AllowInsecureURL: false, // true to point at httptest.Server Source: "go", Service: "checkout-api", Hostname: hostname, Tags: "env:prod,team:platform", }) defer tr.Close() ``` ## Other Transports Generic shippers and on-disk sinks. ### HTTP Transport Generic batched HTTP POST to any endpoint. Pluggable Encoder. ```go import httptr "go.loglayer.dev/transports/http/v2" tr := httptr.New(httptr.Config{ URL: "https://logs.example.com/ingest", Method: "POST", // default POST Headers: map[string]string{"Authorization": "Bearer " + token}, BatchSize: 100, // default BatchInterval: 5 * time.Second, // default BufferSize: 1024, // channel capacity ShutdownTimeout: 5 * time.Second, // bounds Close; cancels in-flight requests on overflow Client: &http.Client{Timeout: 30 * time.Second}, Encoder: httptr.JSONArrayEncoder, OnError: func(err error, entries []httptr.Entry) { /* report */ }, }) defer tr.Close() // flush pending entries; bounded by ShutdownTimeout ``` ### File (Lumberjack) One JSON object per log entry written to a rotating file. Render path matches `transports/structured`. Rotation delegated to `lumberjack.v2`. The package name `lumberjack` shadows the upstream `gopkg.in/natefinch/lumberjack.v2`; alias one of them when both are imported. The shorter `transports/file` import path is reserved for a future roll-our-own implementation. ```go import "go.loglayer.dev/transports/lumberjack/v2" tr := lumberjack.New(lumberjack.Config{ Filename: "/var/log/myapp/app.log", // required MaxSize: 100, // MB before rotation; default 100 MaxBackups: 7, // 0 = keep all (subject to MaxAge) MaxAge: 30, // days; 0 = no age-based cleanup Compress: true, // gzip rotated files }) defer tr.Close() // releases file handle; post-Close logs are dropped // Force rotation (e.g. from a SIGHUP handler): _ = tr.Rotate() ``` `lumberjack.Build` returns `ErrFilenameRequired` instead of panicking when `Filename` is empty. To pretty-print to a file, plug `*lumberjack.Logger` directly into the pretty/console transport's `Writer` field; no need for the file transport. ## Wrapper Transports Wrap an existing third-party logger so loglayer's API sits on top of your established stack. ### zerolog ```go import ( zlog "github.com/rs/zerolog" llzero "go.loglayer.dev/transports/zerolog/v2" ) z := zlog.New(os.Stderr).With().Timestamp().Logger() log := loglayer.New(loglayer.Config{ Transport: llzero.New(llzero.Config{Logger: &z}), }) ``` ### zap ```go import ( "go.uber.org/zap" llzap "go.loglayer.dev/transports/zap/v2" ) z, _ := zap.NewProduction() log := loglayer.New(loglayer.Config{ Transport: llzap.New(llzap.Config{Logger: z}), }) ``` Fatal-level entries are written through a custom hook that prevents zap's default `os.Exit`, letting `Config.DisableFatalExit` control exit behavior. ### log/slog ```go import ( "log/slog" llslog "go.loglayer.dev/transports/slog/v2" ) handler := slog.NewJSONHandler(os.Stderr, nil) sl := slog.New(handler) log := loglayer.New(loglayer.Config{ Transport: llslog.New(llslog.Config{Logger: sl}), }) ``` Forwards `WithContext` to slog so handler-side trace context extraction works automatically. ### phuslu/log, logrus, charmbracelet/log Same wrapping pattern; see [transports overview](https://go.loglayer.dev/transports/) for details. ### OpenTelemetry Logs (otellog) Emits each entry as an OTel `log.Record` on a `go.opentelemetry.io/otel/log.Logger`. ```go import "go.loglayer.dev/transports/otellog/v2" tr := otellog.New(otellog.Config{ Name: "checkout-api", // instrumentation scope name Version: "1.2.3", LoggerProvider: provider, // or use the OTel global; nil = global }) ``` Forwards `WithContext` to `Logger.Emit` so SDK processors correlate logs with the active trace span automatically. ### `transports/gcplogging` Forwards each entry to a caller-supplied `*logging.Logger` from `cloud.google.com/go/logging` (Google Cloud Logging, formerly Stackdriver). ```go import ( "context" "cloud.google.com/go/logging" "go.loglayer.dev/transports/gcplogging/v2" ) ctx := context.Background() client, _ := logging.NewClient(ctx, "my-gcp-project") defer client.Close() tr := gcplogging.New(gcplogging.Config{ Logger: client.Logger("my-log"), RootEntry: logging.Entry{ Labels: map[string]string{"env": "prod"}, }, Sync: false, // default async; set true for short-lived processes }) ``` Severity mapping: Trace/Debug → `Debug`, Info → `Info`, Warn → `Warning`, Error → `Error`, Fatal → `Critical`, Panic → `Alert`. Map metadata merges at the JSON payload root; non-map metadata nests under `"metadata"`. Use `Config.EntryFn` to lift values from `params.Metadata` onto typed Entry fields (`Trace`, `SpanID`, `Labels`, `HTTPRequest`, ...). The transport implements `io.Closer`; `Close()` calls `Logger.Flush` to drain pending async entries. ### `transports/sentry` Forwards each entry to a caller-supplied `sentry.Logger` (Sentry's structured-logs API). ```go import ( "github.com/getsentry/sentry-go" sentrytransport "go.loglayer.dev/transports/sentry/v2" ) sentry.Init(sentry.ClientOptions{Dsn: "...", EnableLogs: true}) tr := sentrytransport.New(sentrytransport.Config{ Logger: sentry.NewLogger(ctx), }) ``` Map fields and metadata flatten as typed Sentry attributes; non-map metadata stringifies under the metadata key (default `"metadata"`, overridable via `loglayer.Config.MetadataFieldName`). Routes `LogLevelFatal` and `LogLevelPanic` through `LFatal()` so loglayer's core controls termination; Sentry's `Fatal()`/`Panic()` (which would `os.Exit`/`panic`) are never called by this transport. ## Creating Custom Transports Implement the four methods of `loglayer.Transport`. `BaseTransport` provides ID, level, enabled state, and helpers. ```go type Transport interface { ID() string IsEnabled() bool SendToLogger(params TransportParams) GetLoggerInstance() any } type myTransport struct { transport.BaseTransport cfg myConfig } func New(cfg myConfig) *myTransport { return &myTransport{ BaseTransport: transport.NewBaseTransport(cfg.BaseConfig), cfg: cfg, } } func (t *myTransport) GetLoggerInstance() any { return nil } func (t *myTransport) SendToLogger(p loglayer.TransportParams) { if !t.ShouldProcess(p.LogLevel) { return } // ... write the entry ... } ``` `TransportParams` carries: `LogLevel`, `Messages` (the raw message slice; the prefix is exposed separately on `Prefix` below), `Data` (assembled fields + error map; nil when both absent, use `len(Data) > 0`), `Metadata` (raw `WithMetadata` value; transport decides serialization), `Err`, `Fields` (raw persistent bag), `Ctx` (per-call `WithContext`, nil when unset), `Groups` (merged persistent + per-call `WithGroup` tags; nil when no groups apply, routing has already consumed it before this point, so the slice is exposed for wire-payload tagging only), `Schema` (resolved assembly shape: FieldsKey, MetadataFieldName, ErrorFieldName, SourceFieldName), `Prefix` (the value attached via `WithPrefix` or `Config.Prefix`, exposed verbatim so transports can render it independently from the message text; empty when no prefix was set). Transports that want a "prefix folded into Messages[0]" rendering call `transport.JoinPrefixAndMessages(p.Prefix, p.Messages)` at the top of `SendToLogger`. The `transport/transporttest` package exports `RunContract(t, ContractCase{...})`: a 14-test contract suite that verifies the wrapper-transport conventions (level filtering, struct vs map metadata, error serialization, fatal handling, etc.). ## Plugins Six lifecycle hooks. A plugin implements any subset of these interfaces: | Hook | Fires when | Used for | |---------------------|---------------------------------------------------------------------------|---------------------------| | `OnFieldsCalled` | `WithFields` is called | Validate/rewrite fields | | `OnMetadataCalled` | `WithMetadata` / `MetadataOnly` is called | Validate/rewrite metadata | | `OnBeforeDataOut` | After fields + error are assembled, before transport dispatch | Inject computed fields | | `OnBeforeMessageOut`| After message assembly, before dispatch | Format strings, redact | | `TransformLogLevel` | Decide whether to override the entry's level | Log-level escalation | | `ShouldSend` | Per-transport gate; returning false skips that transport for this emission| Sampling, group filtering | Hook panics are recovered centrally: each hook returns its no-op value on panic, and `ShouldSend` fails open. Set `Plugin.OnError` to observe recovered panics. The four dispatch-time hook param structs (`BeforeDataOutParams`, `BeforeMessageOutParams`, `TransformLogLevelParams`, `ShouldSendParams`) all carry a `Ctx context.Context` (per-call `WithContext` value, nil if unset) and a `Groups []string` (merged persistent + per-call `WithGroup` tags, nil when none apply). Routing decisions consume `Groups` before any hook fires; the slice is exposed so plugins can drive group-aware transformations. `OnFieldsCalled` and `OnMetadataCalled` fire at builder time (before the chain is finalized) so they don't see either field. ```go log.AddPlugin(loglayer.NewDataHook("tagger", func(p loglayer.BeforeDataOutParams) loglayer.Data { return loglayer.Data{"tagged": true} })) log.AddPlugin(loglayer.NewSendGate("sample-1pct", func(p loglayer.ShouldSendParams) bool { return rand.Float64() < 0.01 })) log.RemovePlugin("tagger") log.GetPlugin("sample-1pct") log.PluginCount() ``` ### Redact Plugin ```go import "go.loglayer.dev/plugins/redact/v2" log.AddPlugin(redact.New(redact.Config{ Keys: []string{"password", "apiKey", "ssn"}, Patterns: []*regexp.Regexp{regexp.MustCompile(`secret-`)}, Censor: "[REDACTED]", })) ``` Walks structs, maps, slices, and pointers via reflection at any depth; preserves the runtime type. Dependency-free. ### Sampling Plugin ```go import "go.loglayer.dev/plugins/sampling/v2" // Independent Bernoulli draw per emission log.AddPlugin(sampling.FixedRate(0.01)) // 1% kept // Per-level rates (levels not in the map are kept unconditionally) log.AddPlugin(sampling.FixedRatePerLevel(map[loglayer.LogLevel]float64{ loglayer.LogLevelDebug: 0.1, loglayer.LogLevelTrace: 0.01, })) // Burst limiting: keep the first N per rolling window, drop the rest log.AddPlugin(sampling.Burst(100, time.Second)) // Strategies compose: every gate must pass for emission ``` ### Format Strings Plugin (fmtlog) Opt the logger into `fmt.Sprintf` semantics for multi-arg messages. ```go import "go.loglayer.dev/plugins/fmtlog/v2" log.AddPlugin(fmtlog.New()) log.Info("user %d logged in from %s", 42, "1.2.3.4") // → msg: "user 42 logged in from 1.2.3.4" ``` ### Datadog APM Trace Injector Tracer-agnostic: supply a small `Extract` function for your tracer (dd-trace-go v1 or v2). ```go import ( ddtracer "github.com/DataDog/dd-trace-go/v2/ddtrace/tracer" "go.loglayer.dev/plugins/datadogtrace/v2" ) log.AddPlugin(datadogtrace.New(datadogtrace.Config{ Service: "checkout-api", Env: "production", Version: "1.2.3", Extract: func(ctx context.Context) (uint64, uint64, bool) { span, ok := ddtracer.SpanFromContext(ctx) if !ok { return 0, 0, false } sc := span.Context() return sc.TraceIDLower(), sc.SpanID(), true // v2 has 128-bit trace IDs; use lower 64 for DD log/trace correlation }, })) // Inside an HTTP handler log.WithContext(r.Context()).Info("served") // Emits dd.trace_id, dd.span_id, dd.service, dd.env, dd.version ``` ### OpenTelemetry Trace Injector Inject `trace_id`, `span_id`, optional `trace_flags`, W3C `trace_state`, and W3C baggage members. ```go import "go.loglayer.dev/plugins/oteltrace/v2" log.AddPlugin(oteltrace.New(oteltrace.Config{ TraceIDKey: "trace_id", // default SpanIDKey: "span_id", // default TraceFlagsKey: "", // optional; omit to skip TraceStateKey: "", // optional W3C trace_state BaggageKeyPrefix: "", // optional W3C baggage; e.g. "baggage." })) ``` Baggage rides independently of the span: contexts with baggage but no span still surface baggage attributes. `transports/otellog` does trace correlation natively, so this plugin is mainly for non-OTel transports. ## Integrations ### loghttp (HTTP middleware) ```go import "go.loglayer.dev/integrations/loghttp/v2" mux := http.NewServeMux() handler := loghttp.Middleware(log, loghttp.Config{})(mux) http.ListenAndServe(":8080", handler) // Inside handlers func myHandler(w http.ResponseWriter, r *http.Request) { reqLog := loghttp.FromRequest(r) // logger with requestId, method, path bound reqLog.Info("handling request") } ``` Auto-emits a "request completed" log on response with status, bytes written, duration. ### sloghandler (slog adapter) ```go import ( "log/slog" "go.loglayer.dev/integrations/sloghandler/v2" ) slog.SetDefault(slog.New(sloghandler.New(log))) // Now every slog.Info(...) call (yours and dependencies') flows through loglayer slog.Info("from somewhere deep", "userId", 42) ``` Levels above `slog.LevelError` pin to `LogLevelError` (slog can't trigger Fatal exit through this handler). `slog.Record.PC` becomes a `*loglayer.Source` automatically; no `Source.Enabled` needed. ## Testing / Mocking ### NewMock: silent logger ```go log := loglayer.NewMock() // same API, emits nothing, Fatal does not exit log.Info("invisible") ``` ### Capture entries with the testing transport ```go 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") log.WithMetadata(loglayer.M{"id": 42}).Warn("warning") lines := lib.Lines() // []lltest.LogLine // LogLine fields: Level, Messages, Data, Metadata, Ctx ``` ### Plugin testing helpers ```go import "go.loglayer.dev/plugins/plugintest/v2" // Wires plugin into a logger backed by lltest, returns the lib for assertions log, lib := plugintest.Install(t, redact.New(redact.Config{Keys: []string{"pw"}})) log.WithMetadata(loglayer.M{"pw": "secret", "user": "alice"}).Info("login") md := lib.PopLine().Metadata.(loglayer.Metadata) require.Equal(t, "[REDACTED]", md["pw"]) ``` `plugintest.AssertNoMutation` and `plugintest.AssertPanicRecovered` are available for common patterns. ## Currently out of scope - Lazy evaluation in fields/metadata (TS LogLayer has `lazy()`; not in Go yet) - Async lazy values - Mixins (TS has `Mixins`; the Go equivalent is plugin authorship) - Context Managers / Log Level Managers as separate concepts (Go uses `context.Context` + the three-tier level system directly) ## Multi-Module Versioning `go.loglayer.dev/v2` is the main module; every transport, plugin, and integration ships as its own Go module. Tags use the prefix form (`transports//v`, `plugins//v`). A breaking change in any one sub-module bumps only that sub-module's major version, so `go.loglayer.dev/v2`'s import path stays stable. Full module list: [`monorel.toml`](https://github.com/loglayer/loglayer-go/blob/main/monorel.toml). ## Documentation - [Getting Started](https://go.loglayer.dev/getting-started) - [Configuration](https://go.loglayer.dev/configuration) - [Cheat Sheet](https://go.loglayer.dev/cheatsheet) - [Basic Logging](https://go.loglayer.dev/logging-api/basic-logging) - [Adjusting Log Levels](https://go.loglayer.dev/logging-api/adjusting-log-levels) - [Fields](https://go.loglayer.dev/logging-api/fields) - [Metadata](https://go.loglayer.dev/logging-api/metadata) - [Error Handling](https://go.loglayer.dev/logging-api/error-handling) - [Go Context](https://go.loglayer.dev/logging-api/go-context) - [Child Loggers](https://go.loglayer.dev/logging-api/child-loggers) - [Groups](https://go.loglayer.dev/logging-api/groups) - [Thread Safety](https://go.loglayer.dev/logging-api/thread-safety) - [Raw Logging](https://go.loglayer.dev/logging-api/raw) - [Multi-line messages](https://go.loglayer.dev/logging-api/multiline) - [Log Sanitization](https://go.loglayer.dev/log-sanitization) - [Mocking](https://go.loglayer.dev/logging-api/mocking) - [For TypeScript Developers](https://go.loglayer.dev/for-typescript-developers) - [Transport Overview](https://go.loglayer.dev/transports/) - [Transport Configuration](https://go.loglayer.dev/transports/configuration) - [Transport Management](https://go.loglayer.dev/transports/management) - [Multiple Transports](https://go.loglayer.dev/transports/multiple-transports) - [Creating Transports](https://go.loglayer.dev/transports/creating-transports) - [Testing Transports](https://go.loglayer.dev/transports/testing-transports) - [Plugins Overview](https://go.loglayer.dev/plugins/) - [Plugin Configuration](https://go.loglayer.dev/plugins/configuration) - [Plugin Management](https://go.loglayer.dev/plugins/management) - [Creating Plugins](https://go.loglayer.dev/plugins/creating-plugins) - [Testing Plugins](https://go.loglayer.dev/plugins/testing-plugins) - [HTTP Middleware (loghttp)](https://go.loglayer.dev/integrations/loghttp) - [slog Handler](https://go.loglayer.dev/integrations/sloghandler) - [What's New](https://go.loglayer.dev/whats-new)