Klinki
Klinki

Reputation: 1439

Read and propagate TraceId in old ASP.NET 4.8

I'm maintaining several applications written in ASP.NET 4.8 (full framework) and ASP.NET Core.

.NET Core implements https://www.w3.org/TR/trace-context/ so I can see my logs (I use Serilog for structured logging) enriched with ParentId, SpanId and most importantly, TraceId.

Is there any way how to read and propagate TraceId also in old ASP.NET 4.8 application?

My applications are doing quite a lot of requests between each other and this would greatly improve debugging experience. Unfortunately most of the requests originate on the old ASP.NET 4.8 apps and go to newer .NET Core ones.

Ideally, I would like to get to the same state as ASP.NET Core apps are - if Request-Id comes from HTTP headers, it is used and filled into ParentId and TraceId and SpanId is generated based on that. Also, it is further propagated to other HTTP requests originating from the .NET Core app.

Thanks!

Upvotes: 3

Views: 2483

Answers (1)

Klinki
Klinki

Reputation: 1439

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;
}

Upvotes: 7

Related Questions