In the series of these posts I will look into how we can implement an APM solution for .NET applications. Application Performance Management (APM) helps to monitor and diagnose application performance. There are numerous libraries and tools out there to solve this given problem. In this series of posts I will focus on implementing APM for .NET applications (.NET Framework 461 and above, .NET Core and NET5) with OpenTelemetry.
In the following posts I will look into the following topics, starting with the first one in this post:
W3C correlation Id specification
Creating and recording spans with ActivitySource
Using OpenTelemetry and Jaeger
W3C correlation Id specification
The CorrelationId is defined in the Trace Context W3C specification. In this post I explain the structure of the defined headers and introduce how the ASP.NET Core implementation uses these headers.
The W3C specification for trace context defines two headers:
tracestate allows to pass vendor specific information. It complements the
traceparent header. According to the specification vendors may pass custom state in this header. Each vendor may concatenate custom key-value pairs to the current value. An example value of the
As such it is possible that the
tracestate cannot be or only partially can be parsed. The current ASP.NET Core implementation does not add custom logic to the
tracestate. On each incoming request, it reads the header value as an opaque
string, and sets it on the
Activity spanning the current http request. Respectively outgoing http requests will attach the
tracestate value with the help of the diagnostics delegating handler.
Out of the two headers the
traceparent one could be called as the primary header, while the
tracestate complements it with vendor specific information. The
traceparent consists of four parts:
An example value of the
The version is a byte value, at the time of the writing this post, the value is set to
00. The specification defines that future version changes will be additive to the
traceparent and specifies how a
traceparent value should be parsed in a forward compatible manner.
The traceId is a 16 byte array representing the set of traces. Typically a user request has one unique traceId, which is used on all subsequent traces. New traceIds are generated for operations without a traceId or when the tracing is restarted. The traceId is serialized as a 32HEXDIGLC, which means a 32 char long string which consists of digits and [a-f] lowercase hex characters. The traceId may be parsed as a Guid in C#.
The parentId is an 8 byte array. In most tracing systems this is known as a
spanId. In .NET these Ids are available as a composite Id and as separate Ids on the current Activity.
The trace flags is the 4th section of the
traceparent's value. The caller may add suggestions for the current trace context to the callee with the help of these flags. The value of flags shall be parsed as a bit field. Currently there is a single flag defined, while the other flags are reserved. The sampled flag when set, means that the caller may have recorded the trace data. As an example, a service may decide to sample a trace context only when the call site has denoted that the give trace is recorded. OpenTelemetry ships with four sampler implementations. The
ParentBasedSampler type takes a sample if parent Activity or any linked Activity is recorded uses this flag.
Using W3C TraceContext with ASP.NET Core and OpenTelemetry
To use the W3C Id format in a .NET application the DefaultIdFormat static property may be set during application startup:
System.Diagnostics.Activity.DefaultIdFormat = System.Diagnostics.ActivityIdFormat.W3C;
With the above setting ASP.NET Core tries to parse incoming request headers and create a new
Activity with the parsed values. An
Activity is a specially crafted type for the purpose of capturing metrics. It has a static Current property and a (non-static) Parent property. When a new Activity is created the parent-child relationship is maintained by the Activity itself. An Activity exposes and maintains properties for all aspects of a trace context. It has TraceId, TraceStateString, Recorded, SpanId, etc. properties. An Activity also measures a given operation: when started the current timestamp is recorded, when stopped a Duration is calculated. Using an Activity we may measure custom operations in our source code, and integrate it straight with the BCL provided metrics. By default ASP.NET Core creates an activity for all incoming requests. When
tracestate headers are provided, the Activitiy is created with a null parent, but the TraceId and SpanId are captured from the incoming request. An Activity and child activities preserve the operation context throughout the context of a request, but it updates and mutates the current span Id. Activities use
AsyncLocal<T> to propagate the current context and hierarchy of parent-child activities across threads. Outgoing http requests also create new Activity, which at this point should be a child of a given hierarchy. The W3C
tracestate headers are enriched with the Id-s of this Activity.
There are two major ways to manually create an
New up an instance using the constructor of the
Activityclass, and pass a name for the current operation.
ActivitySourcetype to create a new Activity.
In this post I looked into the W3C tracecontext standard. Defined the four parts of the
traceperent header value. I also introduced
Activity type to show how .NET leverages this type to trace operations.In the next post I will dive into creating and recording Activities with either of the above methods, as the creation of an Activity affects how it may be recorded, and how OpenTelemetry records it. This is a key decisions to make during the implemention of APM.