diff --git a/src/Service.Tests/Configuration/Telemetry/OpenTelemetryTests.cs b/src/Service.Tests/Configuration/Telemetry/OpenTelemetryTests.cs index df213c6f48..e1602517a0 100644 --- a/src/Service.Tests/Configuration/Telemetry/OpenTelemetryTests.cs +++ b/src/Service.Tests/Configuration/Telemetry/OpenTelemetryTests.cs @@ -2,7 +2,10 @@ // Licensed under the MIT License. using System; +using System.Diagnostics; using System.IO; +using System.Net.Http; +using System.Threading.Tasks; using Azure.DataApiBuilder.Config.ObjectModel; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; @@ -24,6 +27,7 @@ public class OpenTelemetryTests private const string CONFIG_WITH_TELEMETRY = "dab-open-telemetry-test-config.json"; private const string CONFIG_WITHOUT_TELEMETRY = "dab-no-open-telemetry-test-config.json"; + private const string ASPNETCORE_ACTIVITY_SOURCE_NAME = "Microsoft.AspNetCore"; private static RuntimeConfig _configuration; /// @@ -88,6 +92,61 @@ public void TestOpenTelemetryServicesEnabled() Assert.IsNotNull(meterProvider, "MeterProvider should be registered."); } + /// + /// Tests that an inbound HTTP request produces a recorded server span when Open Telemetry is enabled, + /// and that this span continues the trace context received in the W3C traceparent header. + /// The listener below never samples by itself, so the span can only be recorded because + /// the OpenTelemetry TracerProvider subscribes to the ASP.NET Core activity source. + /// + [TestMethod] + public async Task TestOpenTelemetryRecordsInboundHttpRequestSpan() + { + // Arrange + SetUpTelemetryInConfig(CONFIG_WITH_TELEMETRY, true, "http://localhost:4317", "key=key", OtlpExportProtocol.Grpc); + + ActivityTraceId incomingTraceId = ActivityTraceId.CreateRandom(); + ActivitySpanId incomingParentSpanId = ActivitySpanId.CreateRandom(); + TaskCompletionSource recordedServerActivity = new(TaskCreationOptions.RunContinuationsAsynchronously); + using ActivityListener listener = new() + { + ShouldListenTo = source => source.Name == ASPNETCORE_ACTIVITY_SOURCE_NAME, + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.None, + ActivityStopped = activity => + { + if (activity.TraceId == incomingTraceId) + { + recordedServerActivity.TrySetResult(activity); + } + } + }; + ActivitySource.AddActivityListener(listener); + + string[] args = new[] + { + $"--ConfigFileName={CONFIG_WITH_TELEMETRY}" + }; + using TestServer server = new(Program.CreateWebHostBuilder(args)); + Assert.IsNotNull(server.Services.GetService(), "TracerProvider should be registered."); + using HttpClient client = server.CreateClient(); + + using HttpRequestMessage request = new(HttpMethod.Get, "/"); + request.Headers.Add("traceparent", $"00-{incomingTraceId.ToHexString()}-{incomingParentSpanId.ToHexString()}-01"); + + // Act + using HttpResponseMessage response = await client.SendAsync(request); + + // Assert + // The server activity is stopped once the request pipeline completes, which can happen after the response is returned. + Task completedTask = await Task.WhenAny(recordedServerActivity.Task, Task.Delay(TimeSpan.FromSeconds(10))); + Assert.AreSame(recordedServerActivity.Task, completedTask, "An inbound HTTP request should produce a server span."); + + Activity serverActivity = await recordedServerActivity.Task; + Assert.IsTrue(serverActivity.Recorded, "The server span should be recorded."); + Assert.AreEqual(ActivityKind.Server, serverActivity.Kind, "The span should be a server span."); + Assert.AreEqual(incomingTraceId, serverActivity.TraceId, "The server span should continue the incoming trace."); + Assert.AreEqual(incomingParentSpanId, serverActivity.ParentSpanId, "The server span should be parented to the incoming span."); + } + /// /// Tests if the services are correctly disabled for Open Telemetry. /// diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index 5b6ffbb7d7..3ceb194212 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -193,6 +193,9 @@ public void ConfigureServices(IServiceCollection services) .WithTracing(tracing => { tracing.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(runtimeConfig.Runtime.Telemetry.OpenTelemetry.ServiceName!)) + // Creates a server span for every inbound HTTP request (REST, GraphQL, MCP, health) + // and continues the incoming W3C traceparent, so DAB spans are parented correctly. + .AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() // TODO: should we also add FusionCache traces? // To do so we just need to add the package ZiggyCreatures.FusionCache.OpenTelemetry and call