Propagating ActivityId with AsyncLocal in C#
In the C# ecosystem, the term ActivityId appears in several contexts:
- System.Diagnostics.Activity
- System.Diagnostics.CorrelationManager
- System.Diagnostics.EventSource
These are all under the System.Diagnostics
namespace. However, when working with async
and await
, each handles ActivityId
differently, which can lead to confusion.
What I Wanted to Achieve
The goal of my research was to find the best way to:
- Set an
ActivityId
(aGuid
) from a request or background thread and propagate it usingAsyncLocal
. - Use
EventSource
to emit ETW (Event Tracing for Windows) events that include theActivityId
.
The Issue
EventSource
provides a method called CurrentThreadActivityId
that sets the ActivityId
for ETW events. However, as the name suggests, it is thread-local, not async-local. This means:
- If you use
async
/await
and the execution moves to a different thread, theActivityId
does not carry over. - If you set an
ActivityId
and the thread changes, theActivityId
might appear in a different call context, which is undesirable.
While EventSource
supports async-local correlation propagation by starting a scope that generates a new ActivityId
with hierarchical relationships, this wasn't suitable for my needs. I wanted to use the same ActivityId
throughout the async-local code execution.
Other classes like Activity
(System.Diagnostics.Activity
) support async-local propagation when using Start()
/Stop()
methods, but they don't integrate with EventSource
. Similarly, Trace.CorrelationManager.ActivityId
is async-local but doesn't relate to EventSource
's ActivityId
.
For more information:
Activity Class (System.Diagnostics) | Microsoft Learn
CorrelationManager Class (System.Diagnostics) | Microsoft Learn
EventSource Activity IDs — .NET | Microsoft Learn
The Solution
An expert introduced me to an effective solution. Let me explain:
using System.Diagnostics.Tracing;
class Program
{
static void Main(string[] args)
{
HandleRequest(Guid.NewGuid());
}
static void HandleRequest(Guid activityId)
{
Console.WriteLine("Starting request with ID: " + activityId);
MyActivityTracker.SetActivityId(activityId);
DoSomeWork();
Task.Run(() => DoSomeWork());
MyActivityTracker.SetActivityId(Guid.Empty);
}
static void DoSomeWork()
{
Console.WriteLine($" ThreadId: {Thread.CurrentThread.ManagedThreadId} ActivityId: {EventSource.CurrentThreadActivityId}");
}
}
static class MyActivityTracker
{
static AsyncLocal<Guid> _activityId = new AsyncLocal<Guid>(ActivityIdChanged);
public static void SetActivityId(Guid activityId)
{
_activityId.Value = activityId;
}
private static void ActivityIdChanged(AsyncLocalValueChangedArgs<Guid> args)
{
EventSource.SetCurrentThreadActivityId(args.CurrentValue);
}
}
Explanation
This solution leverages the constructor of AsyncLocal<T>
that accepts a valueChangedHandler
delegate:
public AsyncLocal (Action<System.Threading.AsyncLocalValueChangedArgs<T>>? valueChangedHandler);
- Value Change Handler: The
ActivityIdChanged
delegate is called whenever the current value changes on any thread. - ExecutionContext: Changes include those due to the
ExecutionContext
, so if you useasync
/await
and execution moves to a different thread, the delegate is invoked. - Thread Assignment: For each thread assignment, the delegate sets the
ActivityId
to the thread from theAsyncLocal
value.
This approach ensures that the ActivityId
is consistently propagated across asynchronous calls and thread switches.
For more details:
AsyncLocal<T> Constructor (System.Threading) | Microsoft Learn
Conclusion
With the expert’s guidance, I successfully resolved the issue by using AsyncLocal
to propagate a consistent ActivityId
across asynchronous operations. This approach ensures that ETW events emitted via EventSource
include the correct ActivityId
, even when the execution context changes due to async
/await
.
However, for a more sustainable long-term solution, it might be advisable to transition from GUID-based ActivityId
s to using the W3C Trace Context or OpenTelemetry standards for distributed tracing propagation. These standards offer a more robust and interoperable framework for tracing across distributed systems, enhancing observability and simplifying integration with modern monitoring tools.