Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// 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>.
// sub-modules under go.loglayer.dev/transports/<name>/v2.
//
// Full docs: https://go.loglayer.dev
//
Expand Down
2 changes: 1 addition & 1 deletion docs/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type Config struct {
Transport Transport // single transport (mutually exclusive with Transports)
Transports []Transport // multiple transports (mutually exclusive with Transport)
Plugins []Plugin // plugins to register at construction time
Prefix string // prepended to first string message
Prefix string // surfaced to transports as TransportParams.Prefix
Disabled bool // suppress all output (default: false)
ErrorSerializer ErrorSerializer // customize error rendering
ErrorFieldName string // key for serialized error (default: "err")
Expand Down
2 changes: 2 additions & 0 deletions docs/src/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ Send logs directly to any HTTP endpoint without a third-party logging library, w
Built-in mocks make testing painless:

```go
import lltest "go.loglayer.dev/transports/testing/v2"

// Silent mock for tests that don't care about output
log := loglayer.NewMock()

Expand Down
17 changes: 10 additions & 7 deletions docs/src/migrating-to-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: "Upgrade guide for loglayer-go v2: import paths bump to /v2, the pr

# Migrating to v2

`loglayer-go` v2 ships one breaking change: **the loglayer core no longer mutates `Messages[0]` to fold the `WithPrefix` value into the message text.** The prefix now flows through `TransportParams.Prefix` and each transport decides how to render it.
`loglayer-go` v2 ships two breaking changes: **every import path bumps to `/v2`**, and **the loglayer core no longer mutates `Messages[0]` to fold the `WithPrefix` value into the message text.** The prefix now flows through `TransportParams.Prefix` and each transport decides how to render it.

This page is the upgrade checklist.

Expand All @@ -19,16 +19,18 @@ You can migrate one module at a time: a project that uses several `loglayer-go`

`v1.x` folded the prefix into `Messages[0]` from the core so transports that didn't know about prefixes got the right behavior for free. The downside: transports that DID want to render the prefix differently (separate color, separate JSON field, structured forwarding to underlying loggers) couldn't, because by the time they saw the message it was already mangled. Pulling the prefix into a first-class field unblocks every smarter rendering, at the cost of a one-time import-path migration.

The new contract also keeps the core out of the business of mutating caller-owned input: in v1, the prefix-prepend silently rewrote the user's `Messages` slice before any transport saw it; in v2, the core passes the slice through untouched and exposes the prefix on its own field.

## Step 1: bump every import path to `/v2`

The main module and every sub-module are now versioned at `v2`. Update your `go.mod` requires and your source-file imports.

```sh
go get go.loglayer.dev/v2 \
go.loglayer.dev/transports/cli/v2 \
go.loglayer.dev/transports/zerolog/v2 \
go.loglayer.dev/plugins/redact/v2
# ... whichever sub-modules you import
# Run for each sub-module you import
go get go.loglayer.dev/v2
go get go.loglayer.dev/transports/cli/v2
go get go.loglayer.dev/transports/zerolog/v2
go get go.loglayer.dev/plugins/redact/v2
```

In source files:
Expand All @@ -53,7 +55,8 @@ For users of the built-in transports who don't write custom transports, nothing
The exceptions to "nothing else changes":

- The **cli transport** opts into smart prefix rendering: the user prefix renders in dim grey while the level prefix and message body keep the level color. If you were using cli with `WithPrefix`, the rendered output is now visually layered. See the [cli transport doc](/transports/cli) for an example.
- The **blank transport** intentionally passes raw v2 params through to your `ShipToLogger` function. The prefix is on `params.Prefix`, not in `Messages[0]`; if you were extracting the prefix from `Messages[0]`, switch to reading `params.Prefix`.
- The **blank transport** hands `params` straight to your `ShipToLogger` function. If your callback was reading the prefix out of `Messages[0]`, read `params.Prefix` instead.
- If you assert on `testing.LogLine` in tests, the unmangled prefix is also available on `LogLine.Prefix` (new field in v2). Existing assertions on `Messages[0]` keep working because the testing transport calls `JoinPrefixAndMessages` internally.

## Step 3: custom transports

Expand Down
65 changes: 40 additions & 25 deletions docs/src/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ LogLayer is multi-module: most transports, plugins, and integrations ship as the
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

Expand Down Expand Up @@ -97,8 +98,8 @@ Seven levels, integer-typed, in priority order:
- `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)
- `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")
Expand Down Expand Up @@ -189,7 +190,7 @@ Callbacks may be invoked concurrently across goroutines if the same `*LogLayer`

## 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.
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{
Expand Down Expand Up @@ -366,7 +367,7 @@ prefixed.Info("login") // "[Auth] login"
parent := loglayer.New(loglayer.Config{Transport: t})
parent = parent.WithFields(loglayer.F{"service": "api"})

// Independent clone mutations don't bleed back to parent
// 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
Expand All @@ -378,9 +379,9 @@ child.Info("request received") // emits with service AND handler

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.
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
Expand All @@ -391,7 +392,7 @@ log.DisableLogging()
log.IsLevelEnabled(loglayer.LogLevelDebug)
```

Lock-free internally safe to call concurrently with emission. Mirrors `zap.AtomicLevel`.
Lock-free internally; safe to call concurrently with emission. Mirrors `zap.AtomicLevel`.

## Raw Logging

Expand Down Expand Up @@ -485,7 +486,7 @@ A group's `Level` filters out entries below that level for that group's transpor

```go
Ungrouped: loglayer.UngroupedRouting{
Mode: loglayer.UngroupedToAll, // default every transport gets ungrouped logs
Mode: loglayer.UngroupedToAll, // default: every transport gets ungrouped logs
// Mode: loglayer.UngroupedToNone, // drop ungrouped logs entirely
// Mode: loglayer.UngroupedToTransports, Transports: []string{"console"},
}
Expand Down Expand Up @@ -550,7 +551,7 @@ 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.
`*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

Expand Down Expand Up @@ -636,9 +637,23 @@ pretty.New(pretty.Config{
})
```

### Testing Transport (lltest)
### CLI Transport

In-memory capture for assertion tests.
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
})
```

### 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"
Expand All @@ -649,7 +664,7 @@ log := loglayer.New(loglayer.Config{
})

log.Info("hello", loglayer.F{"k": "v"})
lines := lib.Lines() // []lltest.LogLine assert on Level, Messages, Data, Metadata, Ctx
lines := lib.Lines() // []lltest.LogLine; assert on Level, Messages, Data, Metadata, Ctx
last := lib.GetLastLine()
popped := lib.PopLine()
```
Expand Down Expand Up @@ -721,7 +736,7 @@ defer tr.Close() // flush pending entries

### 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 rolled-our-own implementation.
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"
Expand All @@ -741,7 +756,7 @@ _ = 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.
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

Expand Down Expand Up @@ -898,7 +913,7 @@ func (t *myTransport) SendToLogger(p loglayer.TransportParams) {

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

Expand All @@ -913,7 +928,7 @@ Six lifecycle hooks. A plugin implements any subset of these interfaces:
| `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.
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.

Expand Down Expand Up @@ -962,7 +977,7 @@ log.AddPlugin(sampling.FixedRatePerLevel(map[loglayer.LogLevel]float64{
// 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
// Strategies compose: every gate must pass for emission
```

### Format Strings Plugin (fmtlog)
Expand All @@ -979,7 +994,7 @@ log.Info("user %d logged in from %s", 42, "1.2.3.4")

### Datadog APM Trace Injector

Tracer-agnostic supply a small `Extract` function for your tracer (dd-trace-go v1 or v2).
Tracer-agnostic: supply a small `Extract` function for your tracer (dd-trace-go v1 or v2).

```go
import (
Expand Down Expand Up @@ -1016,13 +1031,13 @@ 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
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.
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

Expand Down Expand Up @@ -1058,18 +1073,18 @@ slog.SetDefault(slog.New(sloghandler.New(log)))
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.
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
### NewMock: silent logger

```go
log := loglayer.NewMock() // same API, emits nothing, Fatal does not exit
log.Info("invisible")
```

### Capture entries with lltest
### Capture entries with the testing transport

```go
import lltest "go.loglayer.dev/transports/testing/v2"
Expand Down Expand Up @@ -1108,7 +1123,7 @@ require.Equal(t, "[REDACTED]", md["pw"])

## 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/<name>/v<X.Y.Z>`, `plugins/<name>/v<X.Y.Z>`). A breaking change in any one sub-module bumps only that sub-module's major version `go.loglayer.dev/v2`'s import path stays stable.
`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/<name>/v<X.Y.Z>`, `plugins/<name>/v<X.Y.Z>`). 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).

Expand Down
Loading
Loading