Skip to content

Activity.DisplayName is overwritten by instrumentation libraries #1948

Open
@johncrim

Description

@johncrim

Component

OpenTelemetry.Instrumentation.AspNetCore
OpenTelemetry.Instrumentation.Http

Package Version

Package Name Version
OpenTelemetry 1.9.0
OpenTelemetry.Instrumentation.AspNetCore 1.9.0
OpenTelemetry.Instrumentation.Http 1.9.0

Runtime Version

net8.0

Description

It should be possible to set the Activity.DisplayName in application code to provide a more useful span name than the default auto-instrumented value. For both OpenTelemetry.Instrumentation.AspNetCore and OpenTelemetry.Instrumentation.Http any value stored in .DisplayName before the Activity is stopped is subsequently overwritten by the instrumentation library.

Steps to Reproduce

Here's a test that should pass IMO:

using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenTelemetry.Trace;
using Xunit;

public class DisplayNameOverwrittenTest
{

    [Fact]
    public async Task ActivityDisplayName_CanBeSetInHandler()
    {
        var recordedActivities = new List<Activity>();

        var builder = new HostBuilder()
           .ConfigureWebHost(webBuilder =>
                             {
                                 webBuilder
                                    .UseTestServer()
                                    .ConfigureServices(services =>
                                                       {
                                                           services.AddOpenTelemetry()
                                                                   .WithTracing(builder =>
                                                                                {
                                                                                    builder.AddAspNetCoreInstrumentation();
                                                                                    builder.AddInMemoryExporter(recordedActivities);
                                                                                });
                                                           services.AddRouting();
                                                       })
                                    .Configure(app =>
                                               {
                                                   app.UseRouting();
                                                   app.UseEndpoints(SetupEndpoints);
                                               });
                             });

        void SetupEndpoints(IEndpointRouteBuilder routeBuilder)
        {
            routeBuilder.Map("{**path}",
                             context =>
                             {
                                 // Here I'm setting the Activity.DisplayName to something more useful for this specific route
                                 var activity = context.Features.Get<IHttpActivityFeature>()?.Activity;
                                 if (activity?.IsAllDataRequested == true)
                                 {
                                     var request = context.Request;
                                     activity.DisplayName = $"{request.Method.ToUpperInvariant()} {request.Path}";
                                 }

                                 context.Response.StatusCode = 200;
                                 return Task.CompletedTask;
                             });
        }

        using var host = await builder.StartAsync();
        using var client = host.GetTestClient();
        var response = await client.GetAsync("/foo/bar");

        var requestActivity = Assert.Single(recordedActivities);
        Assert.Equal("GET /foo/bar", requestActivity.DisplayName);
    }

}

Expected Result

Passing test

Actual Result

The test fails because Activity.DisplayName is overwritten when the Activity is stopped.

DisplayNameOverwrittenTest.ActivityDisplayName_CanBeSetInHandler
  Source: DisplayNameOverwrittenTest.cs line 22
  Duration: 87 ms

 Message: 
   Assert.Equal() Failure: Strings differ
                  ↓ (pos 4)
   Expected: "GET /foo/bar"
   Actual:   "GET {**path}"
                  ↑ (pos 4)

Additional Context

A workaround is to store the .DisplayName value you want in Activity.SetCustomProperty(), then add a processor that reads that value and if present sets the Activity.DisplayName after it has stopped.

The same logic is present in OpenTelemetry.Instrumentation.Http. I haven't checked the other instrumentation libraries, but I wouldn't be surprised in this pattern is pervasive.

Related to #1744 and possibly #1792

Metadata

Metadata

Labels

comp:instrumentation.aspnetcoreThings related to OpenTelemetry.Instrumentation.AspNetCorequestionFurther information is requested

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions