--- title: "Log correlation" description: "Put trace_id, meta.id and job on every log line a handler writes, with a short example for Go, Python, Node, Java, .NET and PHP." source: https://babelqueue.com/docs/spec/1.x/log-correlation/ updated: 2026-10-31T00:00:00.000Z --- # Log correlation Spans and metrics tell you *that* something failed; the log line tells you *why*. To get from one to the other, every log line a handler writes should carry the same three envelope fields. They are already on the message, so no new wire field is involved. | Log field | Envelope field | What it lets you do | | :--- | :--- | :--- | | `trace_id` | `trace_id` | find every line from every hop of one causal chain, across services and languages (GR-4) | | `message_id` | `meta.id` | find the lines for one specific message, including its retries | | `job` | `job` (the URN) | filter by message type | The field names are a recommendation, not part of the contract. `message_id` is used rather than `id` or `message` because several logging libraries reserve `message` / `msg` for the log text. **How this lines up with traces.** For a UUID `trace_id`, the OpenTelemetry TraceID is the same 16 bytes, so it is the UUID's 32 hex digits without hyphens ([the mapping](/docs/spec/1.x/observability/#trace_id--otel-traceid--a-deterministic-bijection)). A `trace_id` that is not a UUID is hashed for the TraceID but logged as it arrived, so search logs on the raw value and use the SDK's `traceIdOf` / `TraceIDOf` helper when you need the hashed TraceID. See [a `trace_id` that is not a UUID](/docs/spec/1.x/observability/#a-trace_id-that-is-not-a-uuid). `meta.id` is a different thing from `trace_id`: one message versus one chain ([`id` vs `trace_id`](/docs/spec/1.x/envelope/#id-vs-trace_id--do-not-conflate)). Keep the payload out of the context. `data` can carry personal data (see [GDPR field encryption](/docs/spec/1.x/gdpr-field-encryption/)), so log the three identifiers above and the specific values you need, not the whole `data` object. Each example wraps a handler, so every line the handler logs carries the context. The logging libraries (`slog`, `logging`, pino, SLF4J, `Microsoft.Extensions.Logging`, Monolog) are the ones you already use; only the BabelQueue envelope accessors come from the SDK. ## Go: `slog` The handler receives a `babelqueue.Envelope`; `env.TraceID`, `env.Meta.ID` and `env.URN()` are the three fields. ```go import ( "context" "log/slog" babelqueue "github.com/babelqueue/babelqueue-go" ) func withLogContext(next babelqueue.Handler) babelqueue.Handler { return func(ctx context.Context, env babelqueue.Envelope) error { log := slog.With("trace_id", env.TraceID, "message_id", env.Meta.ID, "job", env.URN()) log.InfoContext(ctx, "handling message") return next(ctx, env) } } app.Handle("urn:babel:orders:created", withLogContext(handleOrderCreated)) ``` Pass `log` on (or store it in `ctx`) if the code below the handler logs too. ## Python: `LoggerAdapter` A handler that takes a third argument receives the full envelope (`data, meta, envelope`); `meta` is the `meta` block. ```python import logging logger = logging.getLogger("orders") def on_order_created(data, meta, envelope): log = logging.LoggerAdapter(logger, { "trace_id": envelope["trace_id"], "message_id": meta.get("id"), "job": envelope["job"], }) log.info("handling order %s", data["order_id"]) app.register("urn:babel:orders:created", on_order_created) ``` The adapter's `extra` values become attributes on the record, so a formatter can print them: `%(trace_id)s %(message_id)s %(job)s`. ## Node: pino child logger `Envelope` is exported by `@babelqueue/core`: `trace_id`, `meta.id` and `job` are plain properties. ```ts import pino from "pino"; import type { Envelope } from "@babelqueue/core"; const logger = pino(); async function onOrderCreated(env: Envelope): Promise { const log = logger.child({ trace_id: env.trace_id, message_id: env.meta.id, job: env.job, }); log.info("handling order"); } ``` ## Java: SLF4J `MDC` `Envelope` is a record: `traceId()`, `meta().id()` and `job()`. `MDC.putCloseable` removes each key again when the block ends, so the context does not leak to the next message on the thread. ```java import com.babelqueue.idempotency.Handler; import org.slf4j.MDC; static Handler withLogContext(Handler next) { return envelope -> { try (MDC.MDCCloseable t = MDC.putCloseable("trace_id", envelope.traceId()); MDC.MDCCloseable m = MDC.putCloseable("message_id", envelope.meta().id()); MDC.MDCCloseable j = MDC.putCloseable("job", envelope.job())) { next.handle(envelope); } }; } ``` Add `%X{trace_id} %X{message_id} %X{job}` to your log pattern (Logback and Log4j2 both read the MDC). ## .NET: `ILogger` scope `Handler` is `Task Handler(Envelope envelope)`; the properties are `TraceId`, `Meta?.Id` and `Job`. ```csharp using BabelQueue; using Microsoft.Extensions.Logging; static Handler WithLogScope(ILogger logger, Handler next) => async envelope => { using IDisposable? scope = logger.BeginScope(new Dictionary { ["trace_id"] = envelope.TraceId, ["message_id"] = envelope.Meta?.Id, ["job"] = envelope.Job, }); await next(envelope); }; ``` Scopes show up in the output only if the logger provider is configured to include them (for the console provider, `IncludeScopes = true`). ## PHP: Monolog processor The `php-sdk` hands a handler a `ConsumedMessage`: `getTraceId()`, `getUrn()` and `getMeta()` (the `meta` block as an array). This processor uses the Monolog 3 `LogRecord` API. ```php use BabelQueue\Contracts\ConsumedMessage; use Monolog\LogRecord; use Monolog\Processor\ProcessorInterface; final class BabelQueueContext implements ProcessorInterface { public function __construct(private readonly ConsumedMessage $message) { } public function __invoke(LogRecord $record): LogRecord { return $record->with(extra: $record->extra + [ 'trace_id' => $this->message->getTraceId(), 'message_id' => $this->message->getMeta()['id'] ?? null, 'job' => $this->message->getUrn(), ]); } } $handler = function (ConsumedMessage $message) use ($logger): void { $logger->pushProcessor(new BabelQueueContext($message)); try { $logger->info('handling order'); } finally { $logger->popProcessor(); } }; ``` ## Per-SDK pages Each SDK's *Reliability & governance* page links here from its OpenTelemetry section: [Go](/docs/babelqueue-go/1.x/reliability-and-governance/#opentelemetry-spans-metrics-and-traceparent), [Python](/docs/babelqueue-python/1.x/reliability-and-governance/#opentelemetry-spans-metrics-and-traceparent), [Node](/docs/babelqueue-node/1.x/reliability-and-governance/#opentelemetry-spans-metrics-and-traceparent), [Java](/docs/babelqueue-java/1.x/reliability-and-governance/#opentelemetry-spans-metrics-and-traceparent), [.NET](/docs/babelqueue-dotnet/1.x/reliability-and-governance/#opentelemetry-spans-metrics-and-traceparent), [PHP](/docs/php-sdk/1.x/reliability-and-governance/#opentelemetry-spans-metrics-and-traceparent). Back to [Observability](/docs/spec/1.x/observability/).