In the monolith, there was one request, one process and one log. Since this module, taking part in a tender goes through the gateway, the Kladde, the Tenders service, the outbox, Service Bus and Sales. When it hangs, nobody asks "what does the log say?" anymore, but "which log?". This lesson joins the parts back into one picture: a trace per operation across all services, metrics per service and per flow, logs that belong to their trace. You know the tools from K3 (3.4); what is new is that they have to fit together across process boundaries.
One trace across all services
A trace consists of spans, called Activity in .NET. Every span has an ID of its own and knows the trace ID and
its parent span. For a service to know which trace a request belongs to, the context travels along. The standard for that is
W3C Trace Context: the traceparent header with version, trace ID, ID of the parent span and flags, such as
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01, plus optionally tracestate.
Who passes it on depends on the path:
- HTTP:
HttpClientwritestraceparentby itself as soon as an activity is running, and ASP.NET Core reads it on the other side and continues the trace. Since .NET 9,HttpClientfills in its spans by itself following OpenTelemetry's conventions. - gRPC: runs in .NET over
HttpClientand HTTP/2, so the same applies. - YARP: forwards the header and writes a span of its own for the forwarding. The gateway from lesson 4.12.5 is thus a link in the trace.
- Service Bus: the SDK writes the context into the message as the property
Diagnostic-Idwhen sending, and the receiver continues the trace when processing. For OpenTelemetry, according to Microsoft's documentation, this support is still experimental and has to be turned on.
A trace always breaks where no context travels along. That rarely happens within a call, but regularly in work that happens later:
a background service, a reminder, a relay. The request that triggered the work is long over by then, and
Activity.Current is empty.