Ok so I managed to solve it on my own.
First of all, it is needed to add System.Diagnostics.DiagnosticSource nugget package.
Then, create ActivityManager class:
public static class ActivityManager
{
public static void StartActivity()
{
if (Activity.Current == null)
{
var activity = new Activity("Default Activity");
string parentIdFromHeaders = HttpContext.Current?.Request.Headers[GetRequestIdHeaderName()];
if (!string.IsNullOrEmpty(parentIdFromHeaders))
{
activity.SetParentId(parentIdFromHeaders);
}
activity.Start();
Activity.Current = activity;
// Sometimes I had issues with Activity.Current being empty even though I set it
// So just to be sure, I add it also to HttpContext Items.
HttpContext.Current?.Items.Add("Activity", activity);
}
}
public static void StopActivity()
{
GetActivity()?.Stop();
}
public static Activity GetActivity()
{
Activity activity = Activity.Current ?? (Activity)HttpContext.Current.Items["Activity"];
return activity;
}
public static string GetRequestIdHeaderName()
{
return "Request-Id";
}
public static string GetRequestId()
{
Activity activity = GetActivity();
if (activity != null)
{
string activityId = activity.Id;
return activityId;
}
// For the rare cases when something happens and activity is not set
// Try to read Request-Id first, if none, then create new GUID
return HttpContext.Current?.Request.Headers.Get(GetRequestIdHeaderName())
?? Guid.NewGuid().ToString().Replace("-", "");
}
}
and configure Global.asax.cs handler
protected void Application_Start()
{
Activity.DefaultIdFormat = ActivityIdFormat.Hierarchical;
}
protected void Application_BeginRequest()
{
ActivityManager.StartActivity();
}
protected void Application_EndRequest()
{
ActivityManager.StopActivity();
}
Now all incoming requests create new Activity and properly set its ParentId.
To add SpanId, TraceId and ParentId to logging context, I created custom log enricher:
public class TraceLogEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
Activity activity = ActivityManager.GetActivity();
if (activity != null)
{
string parentId = activity.ParentId;
string rootId = activity.RootId;
string activityId = activity.Id;
logEvent.AddPropertyIfAbsent(new LogEventProperty("SpanId", new ScalarValue(activityId)));
logEvent.AddPropertyIfAbsent(new LogEventProperty("ParentId", new ScalarValue(parentId)));
logEvent.AddPropertyIfAbsent(new LogEventProperty("TraceId", new ScalarValue(rootId))); }
}
}
and added it to Serilog configuration with .Enrich.With<TraceLogEnricher>().
Last step is to configure outgoing requests. In HttpClient use DefaultRequestHeaders. (For better convenience, this can be configured also in IHttpClientFactory configuration).
var requestId = ActivityManager.GetRequestId();
client.DefaultRequestHeaders.Add("Request-Id", requestId);
if WebServices are used, it is good idea to add Request-Id header also to web services requests. To do that, override GetWebRequest method of web service client. (These are usually generated as partial class, so override it in your own partial class file).
protected override WebRequest GetWebRequest(Uri uri)
{
var request = base.GetWebRequest(uri);
request.Headers.Add("Request-Id", ActivityManager.GetRequestId());
return request;
}