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
13 changes: 0 additions & 13 deletions .changeset/prefix-on-params.md

This file was deleted.

76 changes: 76 additions & 0 deletions .changeset/v2-prefix-passthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
"go.loglayer.dev": major
"transports/blank": major
"transports/charmlog": major
"transports/cli": major
"transports/console": major
"transports/datadog": major
"transports/gcplogging": major
"transports/http": major
"transports/logrus": major
"transports/lumberjack": major
"transports/otellog": major
"transports/phuslu": major
"transports/pretty": major
"transports/sentry": major
"transports/slog": major
"transports/structured": major
"transports/testing": major
"transports/zap": major
"transports/zerolog": major
"plugins/datadogtrace": major
"plugins/fmtlog": major
"plugins/oteltrace": major
"plugins/plugintest": major
"plugins/redact": major
"plugins/sampling": major
"integrations/loghttp": major
"integrations/sloghandler": major
---

**Breaking: import paths bump to `/v2`.**

The loglayer core no longer mutates `Messages[0]` to fold the `WithPrefix` value into the message text. The prefix flows through `TransportParams.Prefix` (and the matching field on every dispatch-time plugin hook param struct). Each transport decides how to render the prefix:

- Most built-in transports call `transport.JoinPrefixAndMessages(params.Prefix, params.Messages)` at the top of `SendToLogger` to preserve the v1 user-visible output exactly.
- The cli transport opts into smart rendering: the level prefix and message body keep the level color (yellow / red), while the `WithPrefix` value gets its own dim-grey color, visually separating caller-context from urgency.
- The `blank` transport intentionally passes raw v2 params through to the user-supplied `ShipToLogger` function, so advanced users can decide their own rendering.

## Migration

Every consumer must update import paths to `/v2`:

```sh
go get go.loglayer.dev/v2 \
go.loglayer.dev/transports/cli/v2 \
go.loglayer.dev/transports/zerolog/v2 \
# ... whichever sub-modules you import
```

In source files:

```diff
-import (
- "go.loglayer.dev"
- "go.loglayer.dev/transports/zerolog"
-)
+import (
+ "go.loglayer.dev/v2"
+ "go.loglayer.dev/transports/zerolog/v2"
+)
```

For most users no other changes are needed: the built-in transports preserve v1 user-visible output. Custom transports that consumed `params.Messages[0]` and assumed the prefix was already prepended must either:

1. Call `transport.JoinPrefixAndMessages(params.Prefix, params.Messages)` at the top of `SendToLogger` (legacy behavior preserved), or
2. Read `params.Prefix` directly and render it however you like (e.g. as a separate JSON field, in its own color, forwarded to the underlying logger's structured-field API).

The new `transport.JoinPrefixAndMessages` helper has fast-path early returns when the prefix is empty, when messages is empty, or when `messages[0]` isn't a string; the per-call cost for any logger that hasn't called `WithPrefix` is one string compare.

## Plugin authors

Plugin hooks (`OnBeforeDataOut`, `OnBeforeMessageOut`, `TransformLogLevel`, `ShouldSend`) gained a `Prefix string` field on their respective params structs (was added additively in v1.7.0). The field is read-only from the plugin's perspective. With v2, `params.Messages[0]` no longer carries the prefix; plugins that read messages directly should be aware that the prefix is now ONLY on `params.Prefix`.

## See also

The prior v1.7.0 release added `Prefix` to the param structs as an additive field; this release removes the auto-prepend.
4 changes: 2 additions & 2 deletions bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ package loglayer_test
import (
"testing"

"go.loglayer.dev"
"go.loglayer.dev/transport/benchtest"
"go.loglayer.dev/v2"
"go.loglayer.dev/v2/transport/benchtest"
)

type noopTransport struct{}
Expand Down
6 changes: 5 additions & 1 deletion builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,11 @@ func (b *LogBuilder) Panic(messages ...any) {
}

func (b *LogBuilder) dispatch(level LogLevel, messages []any, source *Source) {
applyPrefix(b.layer.prefix, messages)
// Prefix flows through TransportParams.Prefix; each transport
// renders it however it wants (most call the
// transport.JoinPrefixAndMessages helper to fold it into the
// first message string).
//
// Hot path: builder has no per-call groups, so pass the layer's
// assigned groups straight through. mergeGroups is out-of-line and
// would be a measurable hit per emission for the dominant case.
Expand Down
6 changes: 3 additions & 3 deletions concurrency_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import (
"sync/atomic"
"testing"

"go.loglayer.dev"
"go.loglayer.dev/internal/lltest"
"go.loglayer.dev/transport"
"go.loglayer.dev/v2"
"go.loglayer.dev/v2/internal/lltest"
"go.loglayer.dev/v2/transport"
)

func TestConcurrentEmission_SimpleMessage(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"errors"
"testing"

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

func TestBuild_NoTransport(t *testing.T) {
Expand Down
16 changes: 10 additions & 6 deletions dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ import (
// tests to verify the pre-exit flush without terminating the test runner.
var osExit = os.Exit

// formatLog applies the prefix to messages then hands the entry to processLog
// using the logger's persistent fields. Per-call goCtx overrides the
// logger's bound ctx (when one is provided), otherwise the bound ctx is
// passed through. source carries pre-captured call-site info from the
// emission entry point (nil if Config.Source.Enabled is off and no adapter
// formatLog hands the entry to processLog using the logger's
// persistent fields. The prefix is propagated through
// TransportParams.Prefix; transports decide how to render it (most
// call transport.JoinPrefixAndMessages to fold it into the first
// message string).
//
// Per-call goCtx overrides the logger's bound ctx (when one is
// provided), otherwise the bound ctx is passed through. source
// carries pre-captured call-site info from the emission entry
// point (nil if Config.Source.Enabled is off and no adapter
// supplied one).
func (l *LogLayer) formatLog(level LogLevel, messages []any, goCtx context.Context, metadata any, err error, source *Source, plugins *pluginSet) {
applyPrefix(l.prefix, messages)
if goCtx == nil {
goCtx = l.boundCtx
}
Expand Down
6 changes: 3 additions & 3 deletions dispatch_edge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"errors"
"testing"

"go.loglayer.dev"
"go.loglayer.dev/internal/lltest"
"go.loglayer.dev/transport"
"go.loglayer.dev/v2"
"go.loglayer.dev/v2/internal/lltest"
"go.loglayer.dev/v2/transport"
)

// dispatch_edge_test.go covers edge cases of the processLog dispatch path
Expand Down
4 changes: 2 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
// # Quickstart
//
// import (
// "go.loglayer.dev"
// "go.loglayer.dev/transports/structured"
// "go.loglayer.dev/v2"
// "go.loglayer.dev/transports/structured/v2"
// )
//
// log := loglayer.New(loglayer.Config{
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ gtag('config', '${gaMeasurementId}');`,
{ text: 'For TypeScript Developers', link: '/for-typescript-developers' },
{ text: 'Use with AI / LLMs', link: '/llms' },
{ text: "What's New", link: '/whats-new' },
{ text: 'Migrating to v2', link: '/migrating-to-v2' },
],
},
{
Expand Down
4 changes: 2 additions & 2 deletions docs/src/cheatsheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,12 @@ The "At a Glance" example shows the typical chain. Two things to know:

```go
child := log.Child() // copy of fields + level state
prefixed := log.WithPrefix("[auth]") // child with a prefix prepended
prefixed := log.WithPrefix("[auth]") // child with a Prefix value
```

Mutations on the child do not affect the parent.

The prefix is also surfaced to transports via `TransportParams.Prefix` and to all four dispatch-time plugin hooks (`BeforeDataOutParams.Prefix`, `BeforeMessageOutParams.Prefix`, `TransformLogLevelParams.Prefix`, `ShouldSendParams.Prefix`) so transports and plugins can render or react to the prefix independently from the message text. The legacy auto-prepend into `Messages[0]` is preserved unchanged for backwards compatibility; future major version will drop it.
The prefix is surfaced to transports via `TransportParams.Prefix` and to all four dispatch-time plugin hooks (`BeforeDataOutParams.Prefix`, `BeforeMessageOutParams.Prefix`, `TransformLogLevelParams.Prefix`, `ShouldSendParams.Prefix`) so transports and plugins can render or react to the prefix independently from the message text. Transports that want a "prefix folded into the message" rendering call `transport.JoinPrefixAndMessages(params.Prefix, params.Messages)`.

## Level Control

Expand Down
98 changes: 98 additions & 0 deletions docs/src/migrating-to-v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
---
title: Migrating to v2
description: "Upgrade guide for loglayer-go v2: import paths bump to /v2, the prefix is now exposed on TransportParams.Prefix instead of being folded into Messages[0]."
---

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

This page is the upgrade checklist.

## Do I have to migrate?

Not immediately. v1.x continues to work; the v1 module path (`go.loglayer.dev`) keeps resolving to its last v1 tag and the auto-prepend behavior stays intact there. Future feature work and bug fixes ship at v2 (`go.loglayer.dev/v2`), so the migration is the path forward but it's not on a deadline.

You can migrate one module at a time: a project that uses several `loglayer-go` sub-modules can have v1 imports for some and v2 for others (Go treats `go.loglayer.dev` and `go.loglayer.dev/v2` as separate modules). The catch is that fields shared between modules (e.g. `loglayer.Config` from main) won't bridge between v1 and v2; pick one main module per project.

## Why this change

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

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

In source files:

```diff
import (
- "go.loglayer.dev"
- "go.loglayer.dev/transports/zerolog"
- "go.loglayer.dev/plugins/redact"
+ "go.loglayer.dev/v2"
+ "go.loglayer.dev/transports/zerolog/v2"
+ "go.loglayer.dev/plugins/redact/v2"
)
```

The package import name (`loglayer`, `zerolog`, `redact`) does not change; only the import path does.

## Step 2: most users are done

For users of the built-in transports who don't write custom transports, nothing else changes. Every built-in transport preserves the v1 user-visible output: `log.WithPrefix("[auth]").Info("hi")` still produces `"[auth] hi"` through every renderer / wrapper / network transport, just like it did in v1.

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

## Step 3: custom transports

If you wrote a custom transport that reads `params.Messages[0]` and relied on the prefix being baked in, you have two paths:

### Path A: preserve v1 behavior (simplest)

Call `transport.JoinPrefixAndMessages` at the top of `SendToLogger`:

```go
import "go.loglayer.dev/v2/transport"

func (t *Transport) SendToLogger(p loglayer.TransportParams) {
if !t.ShouldProcess(p.LogLevel) {
return
}
p.Messages = transport.JoinPrefixAndMessages(p.Prefix, p.Messages)
// ... your existing rendering logic, unchanged
}
```

The helper has fast-path early returns when the prefix is empty, when messages is empty, or when `messages[0]` isn't a string. Per-call cost on a no-prefix logger is one string compare.

### Path B: smart rendering

Read `params.Prefix` directly and render it however suits your transport:

- A renderer transport could color the prefix differently from the message body (see `transports/cli` for an example).
- A structured / JSON transport could emit the prefix as a separate top-level field instead of embedding it in `msg`.
- A wrapper transport could forward the prefix to the underlying logger's structured-field API (`zerolog.Event.Str("prefix", p.Prefix)`, `zap.Field`, etc.).

## Step 4: custom plugins

The dispatch-time plugin hook param structs (`BeforeDataOutParams`, `BeforeMessageOutParams`, `TransformLogLevelParams`, `ShouldSendParams`) gained a `Prefix string` field in v1.7.0; that part is unchanged in v2. The only difference: in v1, `params.Messages[0]` carried the prefix folded in; in v2 it doesn't. Plugins that read the message string directly should be aware.

The prefix is read-only from the plugin's perspective; hooks that return modified data / messages / level / send-decision can act on the prefix value but don't propagate a modified prefix back to downstream hooks.

## See also

- The full [release notes for v2](/whats-new) cover every package's bump and any other v2-only changes.
- [`creating-transports.md`](/transports/creating-transports#reading-params-prefix) documents the `params.Prefix` contract for transport authors.
- [`creating-plugins.md`](/plugins/creating-plugins#reading-params-prefix) documents it for plugin authors.
4 changes: 3 additions & 1 deletion docs/src/plugins/creating-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,9 @@ If multiple plugins implement `SendGate`, the entry goes only when **every** plu

Every dispatch-time hook param struct (`BeforeDataOutParams`, `BeforeMessageOutParams`, `TransformLogLevelParams`, `ShouldSendParams`) carries the value attached via `WithPrefix` on the emitting logger (or set on `Config.Prefix` at construction) as `params.Prefix`. Empty string when no prefix was set.

The prefix is intentionally read-only from the plugin's perspective: hooks that return modified data / messages / level / send-decision can act on the prefix value, but they don't propagate a modified prefix back to downstream hooks. Plugins that want to mutate the user-visible prefix today have to do it via `OnBeforeMessageOut` (rewriting `Messages[0]`); a future major version may expose the prefix as a writable signal once the legacy auto-prepend is removed.
The prefix is intentionally read-only from the plugin's perspective: hooks that return modified data / messages / level / send-decision can act on the prefix value, but they don't propagate a modified prefix back to downstream hooks. Plugins that want to mutate the user-visible prefix have to do it via `OnBeforeMessageOut` (rewriting `Messages[0]`).

`params.Messages[0]` does NOT carry the prefix; `params.Prefix` is the only signal. Plugins reading the message string can ignore the prefix, or read it from `params.Prefix` directly when needed.

Use cases:

Expand Down
4 changes: 2 additions & 2 deletions docs/src/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@ func (t *myTransport) SendToLogger(p loglayer.TransportParams) {
}
```

`TransportParams` carries: `LogLevel`, `Messages` (with the prefix prepended into `Messages[0]` for backwards compat; see `Prefix` below for the unmangled value), `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; the legacy auto-prepend into `Messages[0]` is also performed for backwards compatibility, and will be removed in a future major version).
`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.).

Expand Down Expand Up @@ -1099,7 +1099,7 @@ require.Equal(t, "[REDACTED]", md["pw"])

`plugintest.AssertNoMutation` and `plugintest.AssertPanicRecovered` are available for common patterns.

## What Out of Scope (v1)
## Currently out of scope

- Lazy evaluation in fields/metadata (TS LogLayer has `lazy()`; not in Go yet)
- Async lazy values
Expand Down
Loading
Loading