Skip to main content

Logs & Trace Correlation

TraceCorrelationProcessor stamps the current OTel span's trace_id and span_id onto every Nexus log record's extra field. Log lines emitted from inside an actor handler carry the same trace ID as the OTel trace, enabling log-trace correlation in tools like Grafana Loki + Tempo.

How to register

Add TraceCorrelationProcessor when building your logger:

logger-setup.php
use Monadial\Nexus\Logger\Formatter\JsonFormatter;
use Monadial\Nexus\Logger\Handler\ConsoleHandler;
use Monadial\Nexus\Logger\Level;
use Monadial\Nexus\Logger\NexusLogger;
use Monadial\Nexus\Observability\Logger\TraceCorrelationProcessor;

$logger = NexusLogger::create($system, 'app')
->minLevel(Level::Info)
->processor(new TraceCorrelationProcessor($observability))
->handler(new ConsoleHandler(STDOUT, new JsonFormatter()))
->build();

The processor must be registered before any handlers so the extra fields are present when the handler formats the record.

What it adds

Each log record produced inside a traced actor handler receives three extra fields:

FieldTypeExampleDescription
extra.trace_idstring (hex)4bf92f3577b34da6a3ce929d0e0e4736OTel trace ID
extra.span_idstring (hex)00f067aa0ba902b7OTel span ID for the current actor message span
extra.trace_flagsint1OTel trace flags (1 = sampled)

Behaviour when observability is disabled

When withObservability() has not been called, TraceCorrelationProcessor checks whether the current span is recording. The OTel no-op span reports isRecording() === false, so the processor skips stamping and the log record is emitted without trace fields. There is no error.

Relationship to ServerSpanMiddleware

In the HTTP layer, ServerSpanMiddleware (from nexus-observability-http) extracts W3C Trace Context headers from the incoming request and starts a server span as the active context before passing control to the route handler. TraceCorrelationProcessor reads this active span when log records are created.

Result: every log line emitted anywhere in the HTTP request lifecycle — inside the route handler, inside actor handlers, inside repositories — carries the HTTP server span's trace_id. A single trace ID links all related log lines and all OTel spans.

Grafana integration

To make trace_id clickable in Grafana Loki:

  1. Configure your Monolog JSON formatter to include the extra map in its output.
  2. In Grafana, open the Loki datasource settings and add a Derived field:
    • Name: TraceID
    • Regex: "trace_id":"([a-f0-9]{32})"
    • URL: http://tempo:3200/trace/$${__value.raw} (adjust for your Tempo URL)
  3. Grafana will render a Tempo link next to each log line that contains a trace_id.
tip

If you use structured JSON logging, output trace_id as a top-level field rather than inside extra to simplify the Grafana regex and make the field available for Loki label extraction.

Combining with PSR-14 log events

If your application dispatches PSR-14 events that result in log output (e.g., domain events emitted via EventDispatcherInterface), the processor still works correctly provided those events are dispatched from within an active span — that is, from inside an actor handler or from within the HTTP request lifecycle under ServerSpanMiddleware.