Index

Debugging Real Systems

A Correlation Identifier Is a Boundary Contract

We had logs everywhere and still could not follow one shipment through the system. It had failed somewhere after payment and before its documents were ready, but the application, queue worker, and remote adapter did not share an identifier.

The investigation started with timestamps, account details, and values which looked related. Payment was plausible, as were several remote calls nearby. Some trails took hours to disprove because they belonged to different requests which happened at roughly the same time. We did not need more log lines. We needed a chain of custody.

One investigation key has to survive every boundary which can separate the work: HTTP ingress, application context, outbound calls, queued messages, workers, and the response returned to the caller. Treat propagation as an optional logging detail at any one of those boundaries and the execution splits into unrelated stories again.

Assignment and propagation are different contracts

The first article in this series established how to assign a distinct identifier to each request execution. That solves only the first transition:

incoming request -> active correlation identifier

Propagation asks a different question:

which later records and executions still belong to that request?

A better generator cannot solve propagation. A perfectly unique value which never leaves middleware memory is as useless to a queue worker as an identifier which was never created.

The DBG-09 fixture models the expected path:

HTTP request
  -> request context
  -> request log
  -> outbound request
  -> queue envelope
  -> worker context
  -> worker log
  -> HTTP response

Every arrow is a carrier. The fixture fails if the queue envelope drops the identifier even though the request and both logs remain valid on their own. In that case the logger did not lose context; the queue boundary never transported it. Naming the actual broken boundary saves us from fixing the wrong thing.

Write a carrier map before adding another log

Correlation becomes reviewable when the application declares the carrier at each boundary:

BoundaryAccept fromValidateCarry withExpose to
HTTP ingresscaller header, when trustedformat and sizerequest contextinternal handlers
structured logactive contextrequired field presentevent recordlog index
outbound HTTPactive contextdestination policyrequest headernext service
queue dispatchactive contextserializable valuemessage metadataworker
queue executionmessage metadataexpected formatworker contexthandlers and logs
HTTP responseactive contextnon-empty valueresponse headercaller and support

This table is more useful than a ticket saying “add tracing” because it exposes ownership. If a worker log has no identifier, there are only a few places to inspect: dispatch capture, message serialization, worker hydration, or log enrichment.

It also keeps the language honest. A custom correlation header connects records; it does not magically produce a distributed trace. Tracing also models parent-child execution and sampling, and the W3C Trace Context specification standardizes traceparent and tracestate for that purpose.

The implementation this article draws from uses a validated correlation UUID. That is enough for its search contract and should not be dressed up as proof of spans, parentage, or end-to-end trace export.

Preserve valid context; replace untrusted context

Blindly trusting an incoming identifier lets a caller choose log cardinality, inject awkward values into search tools, or merge unrelated work under one key. Blindly replacing every identifier breaks a chain established by an upstream component.

The HTTP rule can remain small:

valid supported identifier received -> preserve it
missing or invalid identifier        -> generate a new one

In that application, the HTTP boundary follows the rule for UUID correlation identifiers. It preserves a valid caller value, replaces an invalid value, stores the active identifier in request context, and returns it in the response.

Returning the value matters. Support can ask for one opaque reference instead of an exact timestamp, route, account, payload fragment, and timezone. A proxy can also verify that the application preserved the identifier it sent.

Validation is not authentication. A syntactically valid caller value may be safe to search while remaining unsafe for authorization, tenant lookup, deduplication, or billing. A correlation identifier links execution; it grants nothing.

Context should remove manual logging arguments

Passing the identifier into every log call invites omission:

Log::info('remote request started', [
    'correlation_id' => $correlationId,
]);

Someone will eventually forget the field. Context makes the identifier part of the execution instead of another thing every author has to remember:

Context::add('correlation_id', $correlationId);

Log::info('remote request started');
Log::info('remote response received');

Laravel’s context documentation states that context added in middleware is appended to log entries. That removes repetitive call-site plumbing, not the need to verify the logging pipeline. A formatter, processor, transport, or index mapping can still discard or rename the field.

The useful test is therefore not “the context API was called.” It is:

given one active identifier
when a representative structured event is emitted
then the stored event is searchable by that identifier

The test has to reach the storage boundary we actually operate. A unit test of the context facade proves very little about whether the field arrived in a remote log index.

A queue is a new execution, not a continuation of memory

An HTTP request and a queue worker may run in different processes, on different machines, hours apart. Process-local context does not cross that gap through wishful thinking. The dispatcher has to put the relevant value into the message and the worker has to restore it before application code and logging begin.

Laravel captures context when a job is dispatched and restores it while the job executes . That is a useful default, but I would still test the contract when jobs cross framework versions, custom serializers, other languages, or non-Laravel consumers.

The queue record should keep two identities separate:

correlation_id:    the investigation chain
job_execution_id: this worker attempt

Retries should preserve the first and change the second. Otherwise five worker attempts collapse into one apparent execution. The previous article decides whether those attempts should happen; this one makes sure an operator can tell them apart without losing the shared request.

The same distinction applies to business identity:

operation_id:     the durable business request
correlation_id:   the related investigation
execution_id:     this particular attempt

One identifier cannot safely do all three jobs. An operation may be retried by another request tomorrow, one request may create several operations, and one queued operation may execute several times. Collapsing those lifetimes makes a convenient field and a useless investigation model.

Outbound calls need an explicit trust boundary

Sending the identifier everywhere is not automatically correct. Internal services may share the contract, while an external provider may ignore the header, reject it, reflect it, retain it under another policy, or expose it to another support organisation.

The carrier map therefore needs a destination rule:

owned service with shared contract -> propagate supported context
external provider                  -> propagate only when agreed and useful
unknown destination                -> do not leak internal context by default

For interoperable distributed tracing, use the standard header format and processing model rather than inventing a chain of proprietary headers. For a small internal correlation scheme, document the supported header, validation, replacement behavior, and whether intermediaries preserve it.

Either way, customer data, account names, and email addresses do not belong in the identifier. An opaque value is easier to propagate safely and does not become stale when business data changes.

Correlation narrows a search; it does not explain a failure

With a shared identifier, the earlier partial-booking case becomes one ordered set of records instead of several timestamp guesses:

request accepted
payment completed
remote booking completed
persistence failed
worker or cleanup decision recorded
response completed

The chain eliminates unrelated requests and shows which boundaries were crossed. It does not explain why persistence failed, prove that every missing record is an error, or decide what cleanup should do. Correlation gets us to the right story; it does not write the ending.

That limit matters because correlation can create false confidence. Ten records with the same identifier are not necessarily complete. Sampling, buffering, exporter failure, conditional logging, and a crash before emission can all leave gaps. An absent record proves only that the queried system did not return it.

I use correlation to reduce the candidate set, then state, ordering, controlled reproduction, and before-and-after evidence to explain the mechanism.

Test the break, not just the happy path

The DBG-09 fixture uses one synthetic correlation value and deterministic boundary records. Its happy path proves that the request log, outbound header, queue metadata, worker log, and response header all retain the same value.

Two counterexamples make the contract sharper:

queue carrier removed
  -> worker starts without the request correlation
  -> verification rejects the broken chain

untrusted incoming value
  -> ingress replaces it with an application-owned value
  -> no record retains the rejected input

A production suite should also exercise the boundaries which exist there:

  • a valid upstream identifier is preserved;
  • an invalid or oversized value is replaced;
  • log storage can retrieve a representative event by identifier;
  • a queued job restores correlation before its first application log;
  • each retry receives a distinct execution identifier;
  • approved outbound clients propagate the supported format;
  • unapproved destinations receive no internal correlation header; and
  • the final response exposes the active value even on an error path.

The last condition is easy to miss when response middleware runs only after a successful handler. Error responses are exactly where callers tend to need the identifier.

The fixture does not run Laravel, a queue, an HTTP server, or a log backend. The repository evidence stops at HTTP preservation, replacement, context storage, and response exposure. My account supplies the operational pain and use of central search; framework documentation supplies the queue and log mechanics.

Missing correlation turned some investigations into weeks of waiting for another occurrence, followed by timestamp stitching and false trails. Adding more messages after the fact helped only if the failure happened again and we happened to log the right fields this time.

The durable repair is not “log more.” List every boundary which can separate the work and give each one a carrier, validation rule, and test. A correlation identifier becomes useful when losing it is a contract failure, not an ordinary omission we discover during the next incident.