When one request crosses an API gateway, three services, a cache, a database, and a queue, a log line that merely says “an error occurred” is almost useless. Operators need to join events from the same flow, identify the failing step, and search with stable fields. Structured logging solves the data-shape problem; correlation IDs and trace context solve the linkage problem. They must form a shared contract rather than a collection of strings assembled differently by every service.
This guide develops a practical design for multi-service backends: selecting identifiers, defining a JSON schema, propagating context over HTTP and messaging, connecting logs to distributed traces, protecting sensitive data, controlling cardinality, setting sampling policies, and testing the complete path. The objective is searchable production evidence that remains safe, has predictable cost, and does not bind the architecture to one observability vendor.
1. Separate request IDs, correlation IDs, trace IDs, and span IDs
These names are often used interchangeably, causing middleware to record one identifier while HTTP clients forward another. A request ID usually identifies one inbound request at an edge or hop. A reverse proxy may issue a new value when it receives the request. A correlation ID is a broader business-workflow key that can connect an order request, a background job, and a later webhook. A trace ID identifies an entire distributed trace, while a span ID identifies one operation inside that trace.
| Identifier | Scope | Primary use | Trust an Internet-supplied value? |
|---|---|---|---|
| request_id | One request at an edge or hop | Fast application-log lookup and support reference | No; validate it or replace it |
| correlation_id | One business workflow | Join requests, jobs, events, and callbacks | Only after validation and trust-boundary review |
| trace_id | The distributed trace | Link logs to tracing data | Use a standards-compliant parser; never treat it as authentication |
| span_id | One operation in a trace | Open the exact span related to a log event | Let the tracing SDK manage it |
A small system may use the trace ID as its main log-search key. A workflow that lasts hours still needs a business correlation ID because it can create several independent traces. Do not use an order number, email address, or phone number as a correlation ID. Those values have guessable business meaning and create unnecessary disclosure risk. A correlation identifier should be random and contain no user information.
2. Define the log schema before selecting a library
Structured logging means more than emitting JSON. If service A uses requestId, service B uses request_id, and service C embeds all context in message, reliable queries remain difficult. Publish a minimum schema with explicit names, types, and semantics. Each language-specific library then implements that contract.
{
"timestamp": "2026-09-21T14:20:31.482Z",
"level": "error",
"service": "checkout-api",
"environment": "production",
"event": "payment_authorization_failed",
"message": "Payment authorization failed",
"request_id": "req_01K5...",
"correlation_id": "cor_01K5...",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"http_method": "POST",
"route": "/v1/orders/{id}/pay",
"status_code": 502,
"duration_ms": 384,
"error_type": "UpstreamTimeout",
"retryable": true
}
event should be a stable machine-readable code; message is human-oriented and may change. Record a normalized route instead of a URL containing IDs so cardinality does not grow without bound. Keep numbers as numbers, booleans as booleans, timestamps in UTC with an offset, and avoid deeply nested JSON. For an incompatible schema change, introduce schema_version and plan a transition for queries and dashboards.
The logger should automatically attach base fields such as service, deployment version, environment, region, and process identity. Request fields should come from an immutable context. Developers then provide only an event code, an outcome, and a few necessary domain attributes. This reduces spelling drift, prevents accidental overrides of reserved fields, and keeps code review focused on event meaning.
3. Establish context at the HTTP boundary
At the first trusted entry point, middleware extracts standards-based trace context, validates any organization-specific correlation header, and generates replacements when values are absent or invalid. Never accept an arbitrary-length string from a client. A header must not become a log-injection channel or unbounded storage cost. Constrain length, character set, and header count before placing normalized values in request context.
function onIncomingRequest(request) {
trace = tracer.extractValidatedContext(request.headers)
correlationId = validateId(request.header("x-correlation-id"))
? request.header("x-correlation-id")
: generateRandomId()
context = new RequestContext(
requestId = generateRandomId(),
correlationId = correlationId,
traceId = trace.traceId,
spanId = trace.spanId
)
return runWithContext(context, () => next(request))
}
W3C Trace Context defines traceparent to carry the version, trace ID, parent ID, and trace flags; tracestate carries vendor-specific state. Let a tracing SDK parse and create these headers instead of manually slicing strings. Trace headers support observability correlation; they do not establish identity or permission. Authorization must still depend on valid credentials.
An application may return a safe request ID in the response so a user can share it with support. It does not need to expose full trace context, internal topology, or service names. At a trust boundary, consider starting a new trace or forwarding only an approved field set according to the security policy.
4. Propagate context on outbound calls without leaking data
Shared HTTP and gRPC clients should use interceptors that automatically inject trace context and an approved correlation ID. If every developer must remember headers for every call, broken traces are inevitable. The client also needs a destination trust policy: do not send internal baggage, tenant metadata, or user identifiers to a third-party API merely because the same client stack handles both destinations.
OpenTelemetry describes baggage as key-value context that can be made available to spans, metrics, or logs across services. That convenience also creates a propagation risk. A value attached at the gateway may cross many services and leave the network. Allow only reviewed keys, cap total size, never place tokens, API keys, email addresses, or personal data in baggage, and do not copy all baggage into every log record.
- Inject
traceparentandtracestatethrough standard instrumentation. - Forward a separate correlation ID only when the workflow requires it and the destination is in the proper trust boundary.
- Apply outbound timeouts and record outcome, latency, and dependency type rather than payload contents.
- Do not log Authorization, Cookie, raw query strings, or request bodies by default.
- Record a stable operation name instead of a dynamic URL as the primary dimension.
5. Give message queues an explicit context lifecycle
Asynchronous messaging is where correlation is most often lost. A producer should inject context into message metadata rather than mixing it into the business payload unless it is part of the public contract. A consumer extracts context, starts a consumer span, and opens a logging scope for that message. In a batch, messages can have different trace parents; forcing an entire batch into one synthetic trace misrepresents causality.
producer.publish(payload, headers = {
"traceparent": currentTraceParent(),
"x-correlation-id": context.correlationId,
"message-id": messageId
})
consumer.handle(message) {
parent = tracer.extract(message.headers)
startConsumerSpan(parent, "invoice.generate", () => {
withLogContext({
correlation_id: validatedCorrelation(message.headers),
message_id: message.id,
delivery_attempt: message.deliveryAttempt
}, () => process(message))
})
}
A retry should preserve the message_id or business operation ID so operators can recognize the same work, while each processing attempt receives its own span. Logs must distinguish attempt number, outcome, and the ack, nack, or dead-letter decision. When one business step produces a new message, retain the correlation ID if it is still the same workflow but assign a new message ID. A scheduled job with no parent starts a new trace and uses a job execution ID as its operational key.
6. Match logging scope to the concurrency model
A global variable holding the request ID will mix context among concurrent requests. Use the runtime's context-local mechanism: async-local storage, a context variable, coroutine context, or a request-scoped container. When work enters a thread pool, callback, or background task, establish whether the framework propagates context or requires a wrapper. A correct message carrying another request's trace ID is more dangerous than a message with no ID.
Context must also be cleared when a request finishes. Workers that reuse a process or coroutine can leak an old ID into the next job if the scope is not closed in a finally block. A concurrency test should run many requests with different markers, deliberately interleave asynchronous waits, and prove that no record contains a marker from another flow.
Context propagation is part of correctness, not merely an observability convenience. Incorrect context can send operators toward the wrong customer, transaction, and root cause.
7. Make error events investigable without creating noise
The same exception does not need to be logged at every layer. When a repository logs, a service logs, and a controller logs, one failure becomes three or four duplicate events and distorts alerts. Select an owner for the decision. A low layer may wrap an error with technical context; the boundary records one complete event when the request fails. A layer that retries or falls back has a separate reason to record that decision.
Capture error_type, a stable error code, retryability, dependency, operation, and a useful stack trace. Do not group by a full exception message because it often contains dynamic identifiers. An expected input rejection may be warn or info depending on policy; a violated system invariant deserves error. Severity should correspond to an operational response, not merely the exception class in code.
| Situation | Suggested event | Important data | Avoid |
|---|---|---|---|
| Validation failure | request_rejected | route, rule group, status | Request body and personal data |
| Dependency timeout | dependency_call_failed | dependency, operation, duration, retryable | Dynamic URL and credentials |
| Retry | operation_retry_scheduled | attempt, delay_ms, reason | Repeated stack traces on every attempt without need |
| Dead letter | message_dead_lettered | message_id, attempts, reason_code | Raw payload |
8. Protect sensitive data where logs are created
Collector-side redaction is defense in depth, not a substitute for preventing the application from writing secrets. OWASP guidance identifies access tokens, passwords, session identifiers, connection strings, encryption keys, and sensitive personal data as values that generally should not be recorded directly. Build an allowlist of loggable fields, central filters for headers and bodies, and tests using canary strings that resemble real tokens.
User-controlled values require sanitization so carriage returns, line feeds, or delimiters cannot produce log injection. A JSON logger must still use correct escaping, and a log viewer must not render values as executable HTML. Access control, encryption, retention, and access auditing for the log store must match the sensitivity of the remaining information.
When a team needs to join activity for a subject without direct identification, it can use a narrow-scope pseudonymous ID or an HMAC with a managed key. Directly hashing email addresses or phone numbers is often reversible through dictionary enumeration. The design must align with legal policy, deletion rights, and the organization's retention schedule.
9. Control volume, cardinality, and sampling
Structured logs improve querying but can increase storage. Emitting a success event for every small function creates high cost with little operational value. Prefer events at boundaries, state transitions, retry or fallback decisions, failures, and security actions. Metrics are a better fit for request counts, error ratios, and latency distributions; traces are a better fit for detailed call trees.
Sampling must not remove every rare failure. A practical policy can retain all error and security events, sample successful requests, and increase trace retention for high latency. Coordinate trace sampling with log sampling. If a log contains a trace ID whose trace was not retained, the interface should show that state clearly instead of presenting a broken link.
- Set a log budget per service and event, and monitor bytes per second rather than only line count.
- Do not index every field; index frequently queried keys with controlled cardinality.
- Bound message, stack-trace, and array lengths.
- Rate-limit repeated events while exposing a counter for suppressed occurrences.
- Apply different retention to audit, security, application, and debug logs.
10. Design incident queries before the incident
A schema is useful only after the on-call team has tested its queries and dashboards. Start with the request ID supplied by a user, locate the gateway record, obtain the correlation or trace ID, and open the complete timeline sorted by timestamp. Then group by service, event, status, and dependency to find the transition from successful to failed work.
correlation_id = "cor_01K5..."
| sort timestamp asc
| fields timestamp, service, event, level, duration_ms, error_type, span_id
Machine clocks can differ, so log order alone does not always establish causality. Trace parent-child relationships, message metadata, and in-process sequence are stronger evidence for reconstructing a flow. Time synchronization remains necessary, but incident runbooks must acknowledge its limits.
Create saved searches for common cases: dependency timeouts, exhausted retries, dead letters, errors by deployment version, invalid correlation IDs, and lost context propagation. Alerts based on rates or service objectives are usually more actionable than an alert for every individual error line.
11. Test the logging and propagation contract
Unit tests should verify field types and prove that redaction blocks forbidden values. An integration test sends a request through at least two services, a queue, and a consumer, then confirms that the correlation ID survives, trace and span relationships are correct, and request IDs change by hop if that is the stated policy. Failure tests must show that stack traces remain useful without exposing secrets.
- Send a request with no headers: the gateway creates valid IDs and returns a safe request ID.
- Send an overlong header or a value containing a newline: the system rejects or replaces it without logging the raw value.
- Cross an outbound HTTP call: the next service receives valid trace context and the approved correlation value.
- Publish and retry a message: the message ID stays stable, the attempt increments, and each handling has an appropriate span.
- Run concurrent requests: no context crosses flows after asynchronous boundaries.
- Place a fake token in headers and bodies: it appears in neither source logs nor the central store.
- Disable the collector: the application follows its availability policy and the logger has bounded buffering or backpressure.
Finally, run an incident exercise in which the operator receives only a request ID and a symptom. Measure the time needed to find the failing service, dependency, and deployment version. If the operator must SSH into hosts or manually grep several locations, the observability contract is not complete.
Production checklist
- Documentation clearly separates request ID, correlation ID, trace ID, and span ID.
- Every service uses the same field names, types, timestamps, and event-code rules.
- Middleware and interceptors automatically extract, validate, inject, and close logging scopes.
- HTTP, gRPC, queues, and scheduled jobs have tested propagation paths.
- External headers are bounded, and trace or correlation data is never used as authentication.
- Secrets, session IDs, and PII are blocked at the application; user input is sanitized.
- Cardinality, sampling, retention, and volume budgets have owners and alerts.
- Saved searches connecting logs and traces work before production fails.
A sound logging design turns a distributed incident into a verifiable timeline. Begin with a minimum schema, automate context propagation in shared libraries, test sensitive-data leakage and concurrency, and add fields only in response to concrete operational questions. More logs do not automatically create better observability. What matters is that each event has stable structure, correct context, and enough safety to remain useful over time.




No comments yet. Be the first to share your thoughts.