Activity Enrichment in ASP.NET Core 6.0

Waaaaay back in the ASP.NET Core 3.1 days, I wrote about increasing the cardinality of traces using Tags and Baggage. You could write code in your controllers, filters, or application code to be able to add custom information to traces to help find these traces more effectively:

[HttpGet]
public async Task<ActionResult<Guid>> Get(string message)
{
    var command = new SaySomething
    {
        Message = message,
        Id = Guid.NewGuid()
    };

    Activity.Current?.AddTag("cart.operation.id", command.Id.ToString());

There is a challenge with this approach in this it uses the Activity.Current property, an AsyncLocal static property on the Activity class. This value might not be the activity you'd actually want to modify. If you have some other filter that creates an Activity:

public async Task OnActionExecutionAsync(
    ActionExecutingContext context, 
    ActionExecutionDelegate next)
{
    var activity = new Activity("Logging Activity");

    try
    {
        activity.Start();

        _logger.LogInformation("Before the action");
        
        await next();
    }
    finally
    {
        _logger.LogInformation("After the action");

        activity.Stop();
    }
}

Then the Activity.Current property isn't the activity of the incoming web request, but something else altogether. If you're intending to add details to the incoming request's span, you're not guaranteed to get the correct Activity.

This got a bit easier with ASP.NET Core 6.0 and the inclusion of a new Request Feature to access the current activity. We can request the IHttpActivityFeature from the current HttpContext to access the Activity associated with the current request:

[HttpGet]
public async Task<ActionResult<Guid>> Get(string message)
{
    var command = new SaySomething
    {
        Message = message,
        Id = Guid.NewGuid()
    };
    var activityFeature = HttpContext.Features.Get<IHttpActivityFeature>();
    
    activityFeature?.Activity.AddBaggage("cart.operation.id", command.Id.ToString());

    _logger.LogInformation("Sending message {message} with {id}", command.Message, command.Id);

    await _messageSession.Send(command);

    return Accepted(command.Id);
}

From the IHttpActivityFeature, we can add tags, baggage, events, or just generally inspect the current span. If we want to start a new activity intentionally parented from this one too, that's possible.

It's only available from anything that has access to HttpContext, which in turn is available in different means depending on what code location you need it at.

This also inspired a similar feature in my NServiceBus.Extensions.Diagnostics package, to be able to modify the exact Activity of the incoming/outgoing message:

public async Task Handle(MakeItYell message, IMessageHandlerContext context)
{
    _logger.LogInformation("Yelling out {message}", message.Value);

    var collection = _database.GetCollection<Person>(nameof(Person));

    var count = await collection.CountDocumentsAsync(p => true);
    var rng = new Random();

    var next = rng.Next((int)count);

    var currentActivity = context.Extensions.Get<ICurrentActivity>();

    currentActivity.Current?.AddTag("code.randomvalue", next);

    var favoritePerson = await collection.AsQueryable().Skip(next).FirstAsync();

    await context.Reply(new MakeItYellResponse
    {
        Value = message.Value.ToUpperInvariant(),
        FavoritePerson = $"{favoritePerson.FirstName} {favoritePerson.LastName}"
    });
}

And then this tag shows up directly in our traces:

Custom tag value flowing out to traces

Just like you might add additional log context values for your logs, you would add these custom tags to provide activity/span enrichment for better debugging and searchability.