Introduction
When systems grow beyond a single process, debugging and observability become hard. A single user action may touch multiple services, background workers, databases and third-party APIs. Without a unified correlation model, finding the root cause of a failure becomes a scatter-gun exercise: you search logs in Service A, then Service B, then the queue, hoping timestamps line up.
A Unified Logging Correlation Model solves this by propagating a shared correlation context — typically a Trace ID — across every hop of a request. When logs, traces and metrics all include that same Trace ID, you can reconstruct the entire transaction across your distributed system.
This article gives a production-ready blueprint: design principles, propagation techniques (HTTP, messaging, DB calls), OpenTelemetry integration, sampling, performance trade-offs, security and concrete .NET and Angular code examples. Diagrams use block-style layout for clarity.
Goals and Non-Goals
Goals
Generate a stable Trace ID for each external request or scheduled job.
Propagate the Trace ID through all service boundaries: HTTP, gRPC, message buses, background workers.
Attach the Trace ID to logs, traces, and metrics.
Support distributed tracing (spans) with OpenTelemetry.
Preserve minimal overhead and ensure safe sampling.
Allow ad-hoc correlation from logs and dashboards.
Non-Goals
Replacing business-level correlation IDs (orderId, invoiceId). Use both: Trace ID for technical flow, business ID for domain tracing.
Forcing 100% tracing of every low-value event—use sampling and tail-based strategies.
Core Concepts
Trace ID: global identifier for the whole transaction (128-bit recommended).
Span: a unit of work inside the trace (HTTP handler, DB call). Spans have start/end timestamps and parent-child relationships.
Baggage: small key-value pairs that travel with the trace across services (use sparingly).
Correlation Context: a structured map containing Trace ID, Span ID, user id, tenant id, and optional business IDs.
Propagator: code that injects / extracts trace and baggage into transport headers.
High-Level Flow (Block Diagram)
┌──────────┐ HTTP Req ┌────────────┐ MQ Msg ┌────────────┐
│ Browser │ ─────────────> │ API Gateway│ ───────────> │ Worker │
│ (Angular)│ │ (.NET) │ │ (.NET) │
└────┬─────┘ └────┬───────┘ └────┬───────┘
│ │ │
│ TraceID injected │ TraceID forwarded │ TraceID extracted
│ (client) │ (HTTP headers) │ (message attributes)
▼ ▼ ▼
Logs include traceId Logs include traceId Logs include traceId
Metrics tagged Traces & spans created Spans continue, baggage preserved
Trace ID Format and Generation
Use 128-bit Trace IDs (hex string) for uniqueness and compatibility with tracing backends (Jaeger, Zipkin, OTLP). Example:
4bf92f3577b34da6a3ce929d0e0e4736.Generate Trace ID in the first ingress point: API Gateway, Load Balancer, or front-end when initiating long flows. If an incoming request already carries a Trace ID, honor it (preserve continuity).
Trace ID must be URL-safe (hex) and reasonably short to avoid header bloat.
Prefer secure, collision-resistant RNG (e.g.,
Guidwith v4 or cryptographically random 16 bytes).
Header Names and Standards
Follow W3C Trace Context standard (
traceparent,tracestate) for maximum compatibility. Example header:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tracestate: vendor1=opaque,other=val
For older systems, you may keep a
X-Request-IdorX-Correlation-Idbut prefertraceparentfor tracing spans andtrace-idfor logs if needed. Keep header names consistent across services.
Propagation Patterns
HTTP (Inbound / Outbound)
Extract: At the beginning of an HTTP request, extract
traceparentand baggage headers. If missing, create a new Trace ID and root span.Inject: When making outbound HTTP calls, inject the current
traceparentandtracestateinto outgoing request headers.
Messaging (Kafka / RabbitMQ / SQS)
Inject as message attributes/headers: place
traceparentandbaggageas headers on the message.Consume: Consumer extracts trace headers, continues the trace by creating a child span, and processes. Always include them in any logs from the consumer.
Background Jobs and Batch Processing
When scheduling a job from a request, include the originating trace ID (or start a new trace and link them); include business correlation IDs in the job payload.
Database Calls and Non-networked Resources
Add trace context to logs and metrics around DB calls. You don’t put traceparent into DB queries, but you create a child span for the DB operation. Attach
traceIdas a field in any long-term audit logs or job records.
OpenTelemetry Integration (Recommended)
OpenTelemetry gives a vendor-neutral SDK for traces, metrics, and logs. Core steps:
Instrument your services with OpenTelemetry SDK.
Use W3C Trace Context propagator (default in OT).
Export traces via OTLP to collectors (Jaeger, Zipkin, Tempo, DataDog, New Relic).
Correlate logs by including
trace_idandspan_idin structured log records.
Example .NET setup
// Program.cs
builder.Services.AddOpenTelemetryTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSqlClientInstrumentation()
.SetSampler(new ParentBasedSampler(new TraceIdRatioBasedSampler(0.1))) // example
.AddOtlpExporter(opts =>
{
opts.Endpoint = new Uri("http://otel-collector:4317");
});
});
Angular front-end can inject Trace IDs for long-running flows (optional). Use OpenTelemetry JS for browser instrumentation.
Correlating Logs, Traces and Metrics
Structured logging: Include
trace_id,span_id,user_id,tenant_id, andbusiness_id(if available) as structured fields in every log. Example JSON log:
{
"timestamp":"2025-11-21T10:00:00Z",
"level":"Error",
"message":"Payment failed",
"trace_id":"4bf92f3577b34da6a3ce929d0e0e4736",
"span_id":"00f067aa0ba902b7",
"user_id":"u-123",
"order_id":"ORD-987",
"exception":"TimeoutException"
}
Logging framework mapping: Configure Serilog / NLog / Log4Net to read the current
Activity(System.Diagnostics) and push trace fields automatically.
.NET Serilog example:
Log.Logger = new LoggerConfiguration()
.Enrich.FromLogContext()
.Enrich.WithProperty("service", "orders-api")
.WriteTo.Console()
.CreateLogger();
app.Use(async (ctx, next) =>
{
using var activity = MyTracing.StartIncomingActivity(ctx);
LogContext.PushProperty("trace_id", activity.TraceId.ToHexString());
await next();
});
Metrics tagging: Tag important metrics with
trace_idwhen useful (but avoid high-cardinality tags in long-term metric storage).
Sampling Strategy
Tracing every single request at 100% is expensive. Use sampling:

Join the conversation! Your thoughts help the community grow.