opentelemetry-dotnet/test/OpenTelemetry.Instrumentati.../BasicTests.cs

1196 lines
45 KiB
C#

// <copyright file="BasicTests.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>
using System.Diagnostics;
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Moq;
using OpenTelemetry.Context.Propagation;
using OpenTelemetry.Instrumentation.AspNetCore.Implementation;
using OpenTelemetry.Tests;
using OpenTelemetry.Trace;
using TestApp.AspNetCore;
using TestApp.AspNetCore.Filters;
using Xunit;
using static OpenTelemetry.Internal.HttpSemanticConventionHelper;
namespace OpenTelemetry.Instrumentation.AspNetCore.Tests;
// See https://github.com/aspnet/Docs/tree/master/aspnetcore/test/integration-tests/samples/2.x/IntegrationTestsSample
public sealed class BasicTests
: IClassFixture<WebApplicationFactory<Program>>, IDisposable
{
private readonly WebApplicationFactory<Program> factory;
private TracerProvider tracerProvider = null;
public BasicTests(WebApplicationFactory<Program> factory)
{
this.factory = factory;
}
[Fact]
public void AddAspNetCoreInstrumentation_BadArgs()
{
TracerProviderBuilder builder = null;
Assert.Throws<ArgumentNullException>(() => builder.AddAspNetCoreInstrumentation());
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task StatusIsUnsetOn200Response(bool disableLogging)
{
var exportedItems = new List<Activity>();
void ConfigureTestServices(IServiceCollection services)
{
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(exportedItems)
.Build();
}
// Arrange
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(ConfigureTestServices);
if (disableLogging)
{
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
}
})
.CreateClient())
{
// Act
using var response = await client.GetAsync("/api/values");
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
WaitForActivityExport(exportedItems, 1);
}
Assert.Single(exportedItems);
var activity = exportedItems[0];
Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpResponseStatusCode));
Assert.Equal(ActivityStatusCode.Unset, activity.Status);
ValidateAspNetCoreActivity(activity, "/api/values");
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task SuccessfulTemplateControllerCallGeneratesASpan(bool shouldEnrich)
{
var exportedItems = new List<Activity>();
void ConfigureTestServices(IServiceCollection services)
{
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddAspNetCoreInstrumentation(options =>
{
if (shouldEnrich)
{
options.EnrichWithHttpRequest = (activity, request) => { activity.SetTag("enrichedOnStart", "yes"); };
options.EnrichWithHttpResponse = (activity, response) => { activity.SetTag("enrichedOnStop", "yes"); };
}
})
.AddInMemoryExporter(exportedItems)
.Build();
}
// Arrange
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(ConfigureTestServices);
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
// Act
using var response = await client.GetAsync("/api/values");
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
WaitForActivityExport(exportedItems, 1);
}
Assert.Single(exportedItems);
var activity = exportedItems[0];
if (shouldEnrich)
{
Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedOnStart" && tag.Value == "yes"));
Assert.NotEmpty(activity.Tags.Where(tag => tag.Key == "enrichedOnStop" && tag.Value == "yes"));
}
ValidateAspNetCoreActivity(activity, "/api/values");
}
[Fact]
public async Task SuccessfulTemplateControllerCallUsesParentContext()
{
var exportedItems = new List<Activity>();
var expectedTraceId = ActivityTraceId.CreateRandom();
var expectedSpanId = ActivitySpanId.CreateRandom();
// Arrange
using (var testFactory = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(exportedItems)
.Build();
});
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
}))
{
using var client = testFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "/api/values/2");
request.Headers.Add("traceparent", $"00-{expectedTraceId}-{expectedSpanId}-01");
// Act
var response = await client.SendAsync(request);
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
WaitForActivityExport(exportedItems, 1);
}
Assert.Single(exportedItems);
var activity = exportedItems[0];
Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName);
Assert.Equal(expectedTraceId, activity.Context.TraceId);
Assert.Equal(expectedSpanId, activity.ParentSpanId);
ValidateAspNetCoreActivity(activity, "/api/values/2");
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task CustomPropagator(bool addSampler)
{
try
{
var exportedItems = new List<Activity>();
var expectedTraceId = ActivityTraceId.CreateRandom();
var expectedSpanId = ActivitySpanId.CreateRandom();
var propagator = new Mock<TextMapPropagator>();
propagator.Setup(m => m.Extract(It.IsAny<PropagationContext>(), It.IsAny<HttpRequest>(), It.IsAny<Func<HttpRequest, string, IEnumerable<string>>>())).Returns(
new PropagationContext(
new ActivityContext(
expectedTraceId,
expectedSpanId,
ActivityTraceFlags.Recorded),
default));
// Arrange
using (var testFactory = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
Sdk.SetDefaultTextMapPropagator(propagator.Object);
var tracerProviderBuilder = Sdk.CreateTracerProviderBuilder();
if (addSampler)
{
tracerProviderBuilder
.SetSampler(new TestSampler(SamplingDecision.RecordAndSample, new Dictionary<string, object> { { "SomeTag", "SomeKey" }, }));
}
this.tracerProvider = tracerProviderBuilder
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(exportedItems)
.Build();
});
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
}))
{
using var client = testFactory.CreateClient();
using var response = await client.GetAsync("/api/values/2");
response.EnsureSuccessStatusCode(); // Status Code 200-299
WaitForActivityExport(exportedItems, 1);
}
Assert.Single(exportedItems);
var activity = exportedItems[0];
Assert.True(activity.Duration != TimeSpan.Zero);
Assert.Equal(expectedTraceId, activity.Context.TraceId);
Assert.Equal(expectedSpanId, activity.ParentSpanId);
ValidateAspNetCoreActivity(activity, "/api/values/2");
}
finally
{
Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[]
{
new TraceContextPropagator(),
new BaggagePropagator(),
}));
}
}
[Fact]
public async Task RequestNotCollectedWhenFilterIsApplied()
{
var exportedItems = new List<Activity>();
void ConfigureTestServices(IServiceCollection services)
{
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => ctx.Request.Path != "/api/values/2")
.AddInMemoryExporter(exportedItems)
.Build();
}
// Arrange
using (var testFactory = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(ConfigureTestServices);
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
}))
{
using var client = testFactory.CreateClient();
// Act
using var response1 = await client.GetAsync("/api/values");
using var response2 = await client.GetAsync("/api/values/2");
// Assert
response1.EnsureSuccessStatusCode(); // Status Code 200-299
response2.EnsureSuccessStatusCode(); // Status Code 200-299
WaitForActivityExport(exportedItems, 1);
}
Assert.Single(exportedItems);
var activity = exportedItems[0];
ValidateAspNetCoreActivity(activity, "/api/values");
}
[Fact]
public async Task RequestNotCollectedWhenFilterThrowException()
{
var exportedItems = new List<Activity>();
void ConfigureTestServices(IServiceCollection services)
{
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) =>
{
if (ctx.Request.Path == "/api/values/2")
{
throw new Exception("from InstrumentationFilter");
}
else
{
return true;
}
})
.AddInMemoryExporter(exportedItems)
.Build();
}
// Arrange
using (var testFactory = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(ConfigureTestServices);
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
}))
{
using var client = testFactory.CreateClient();
// Act
using (var inMemoryEventListener = new InMemoryEventListener(AspNetCoreInstrumentationEventSource.Log))
{
using var response1 = await client.GetAsync("/api/values");
using var response2 = await client.GetAsync("/api/values/2");
response1.EnsureSuccessStatusCode(); // Status Code 200-299
response2.EnsureSuccessStatusCode(); // Status Code 200-299
Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 3));
}
WaitForActivityExport(exportedItems, 1);
}
// As InstrumentationFilter threw, we continue as if the
// InstrumentationFilter did not exist.
Assert.Single(exportedItems);
var activity = exportedItems[0];
ValidateAspNetCoreActivity(activity, "/api/values");
}
[Theory]
[InlineData(SamplingDecision.Drop)]
[InlineData(SamplingDecision.RecordOnly)]
[InlineData(SamplingDecision.RecordAndSample)]
public async Task ExtractContextIrrespectiveOfSamplingDecision(SamplingDecision samplingDecision)
{
try
{
var expectedTraceId = ActivityTraceId.CreateRandom();
var expectedParentSpanId = ActivitySpanId.CreateRandom();
var expectedTraceState = "rojo=1,congo=2";
var activityContext = new ActivityContext(expectedTraceId, expectedParentSpanId, ActivityTraceFlags.Recorded, expectedTraceState, true);
var expectedBaggage = Baggage.SetBaggage("key1", "value1").SetBaggage("key2", "value2");
Sdk.SetDefaultTextMapPropagator(new ExtractOnlyPropagator(activityContext, expectedBaggage));
// Arrange
using var testFactory = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services => { this.tracerProvider = Sdk.CreateTracerProviderBuilder().SetSampler(new TestSampler(samplingDecision)).AddAspNetCoreInstrumentation().Build(); });
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
});
using var client = testFactory.CreateClient();
// Test TraceContext Propagation
var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityTraceContext");
var response = await client.SendAsync(request);
var childActivityTraceContext = JsonSerializer.Deserialize<Dictionary<string, string>>(await response.Content.ReadAsStringAsync());
response.EnsureSuccessStatusCode();
Assert.Equal(expectedTraceId.ToString(), childActivityTraceContext["TraceId"]);
Assert.Equal(expectedTraceState, childActivityTraceContext["TraceState"]);
Assert.NotEqual(expectedParentSpanId.ToString(), childActivityTraceContext["ParentSpanId"]); // there is a new activity created in instrumentation therefore the ParentSpanId is different that what is provided in the headers
// Test Baggage Context Propagation
request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityBaggageContext");
response = await client.SendAsync(request);
var childActivityBaggageContext = JsonSerializer.Deserialize<IReadOnlyDictionary<string, string>>(await response.Content.ReadAsStringAsync());
response.EnsureSuccessStatusCode();
Assert.Single(childActivityBaggageContext, item => item.Key == "key1" && item.Value == "value1");
Assert.Single(childActivityBaggageContext, item => item.Key == "key2" && item.Value == "value2");
}
finally
{
Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[]
{
new TraceContextPropagator(),
new BaggagePropagator(),
}));
}
}
[Fact]
public async Task ExtractContextIrrespectiveOfTheFilterApplied()
{
try
{
var expectedTraceId = ActivityTraceId.CreateRandom();
var expectedParentSpanId = ActivitySpanId.CreateRandom();
var expectedTraceState = "rojo=1,congo=2";
var activityContext = new ActivityContext(expectedTraceId, expectedParentSpanId, ActivityTraceFlags.Recorded, expectedTraceState);
var expectedBaggage = Baggage.SetBaggage("key1", "value1").SetBaggage("key2", "value2");
Sdk.SetDefaultTextMapPropagator(new ExtractOnlyPropagator(activityContext, expectedBaggage));
// Arrange
bool isFilterCalled = false;
using var testFactory = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddAspNetCoreInstrumentation(options =>
{
options.Filter = context =>
{
isFilterCalled = true;
return false;
};
})
.Build();
});
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
});
using var client = testFactory.CreateClient();
// Test TraceContext Propagation
var request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityTraceContext");
var response = await client.SendAsync(request);
// Ensure that filter was called
Assert.True(isFilterCalled);
var childActivityTraceContext = JsonSerializer.Deserialize<Dictionary<string, string>>(await response.Content.ReadAsStringAsync());
response.EnsureSuccessStatusCode();
Assert.Equal(expectedTraceId.ToString(), childActivityTraceContext["TraceId"]);
Assert.Equal(expectedTraceState, childActivityTraceContext["TraceState"]);
Assert.NotEqual(expectedParentSpanId.ToString(), childActivityTraceContext["ParentSpanId"]); // there is a new activity created in instrumentation therefore the ParentSpanId is different that what is provided in the headers
// Test Baggage Context Propagation
request = new HttpRequestMessage(HttpMethod.Get, "/api/GetChildActivityBaggageContext");
response = await client.SendAsync(request);
var childActivityBaggageContext = JsonSerializer.Deserialize<IReadOnlyDictionary<string, string>>(await response.Content.ReadAsStringAsync());
response.EnsureSuccessStatusCode();
Assert.Single(childActivityBaggageContext, item => item.Key == "key1" && item.Value == "value1");
Assert.Single(childActivityBaggageContext, item => item.Key == "key2" && item.Value == "value2");
}
finally
{
Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[]
{
new TraceContextPropagator(),
new BaggagePropagator(),
}));
}
}
[Fact]
public async Task BaggageIsNotClearedWhenActivityStopped()
{
int? baggageCountAfterStart = null;
int? baggageCountAfterStop = null;
using EventWaitHandle stopSignal = new EventWaitHandle(false, EventResetMode.ManualReset);
void ConfigureTestServices(IServiceCollection services)
{
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddAspNetCoreInstrumentation(
new TestHttpInListener(new AspNetCoreInstrumentationOptions())
{
OnEventWrittenCallback = (name, payload) =>
{
switch (name)
{
case HttpInListener.OnStartEvent:
{
baggageCountAfterStart = Baggage.Current.Count;
}
break;
case HttpInListener.OnStopEvent:
{
baggageCountAfterStop = Baggage.Current.Count;
stopSignal.Set();
}
break;
}
},
})
.Build();
}
// Arrange
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(ConfigureTestServices);
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values");
request.Headers.TryAddWithoutValidation("baggage", "TestKey1=123,TestKey2=456");
// Act
using var response = await client.SendAsync(request);
}
stopSignal.WaitOne(5000);
// Assert
Assert.NotNull(baggageCountAfterStart);
Assert.Equal(2, baggageCountAfterStart);
Assert.NotNull(baggageCountAfterStop);
Assert.Equal(2, baggageCountAfterStop);
}
[Theory]
[InlineData(SamplingDecision.Drop, false, false)]
[InlineData(SamplingDecision.RecordOnly, true, true)]
[InlineData(SamplingDecision.RecordAndSample, true, true)]
public async Task FilterAndEnrichAreOnlyCalledWhenSampled(SamplingDecision samplingDecision, bool shouldFilterBeCalled, bool shouldEnrichBeCalled)
{
bool filterCalled = false;
bool enrichWithHttpRequestCalled = false;
bool enrichWithHttpResponseCalled = false;
void ConfigureTestServices(IServiceCollection services)
{
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.SetSampler(new TestSampler(samplingDecision))
.AddAspNetCoreInstrumentation(options =>
{
options.Filter = (context) =>
{
filterCalled = true;
return true;
};
options.EnrichWithHttpRequest = (activity, request) =>
{
enrichWithHttpRequestCalled = true;
};
options.EnrichWithHttpResponse = (activity, request) =>
{
enrichWithHttpResponseCalled = true;
};
})
.Build();
}
// Arrange
using var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(ConfigureTestServices);
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient();
// Act
using var response = await client.GetAsync("/api/values");
// Assert
Assert.Equal(shouldFilterBeCalled, filterCalled);
Assert.Equal(shouldEnrichBeCalled, enrichWithHttpRequestCalled);
Assert.Equal(shouldEnrichBeCalled, enrichWithHttpResponseCalled);
}
[Fact]
public async Task ActivitiesStartedInMiddlewareShouldNotBeUpdated()
{
var exportedItems = new List<Activity>();
var activitySourceName = "TestMiddlewareActivitySource";
var activityName = "TestMiddlewareActivity";
void ConfigureTestServices(IServiceCollection services)
{
services.AddSingleton<ActivityMiddleware.ActivityMiddlewareImpl>(new TestActivityMiddlewareImpl(activitySourceName, activityName));
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddAspNetCoreInstrumentation()
.AddSource(activitySourceName)
.AddInMemoryExporter(exportedItems)
.Build();
}
// Arrange
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(ConfigureTestServices);
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
using var response = await client.GetAsync("/api/values/2");
response.EnsureSuccessStatusCode();
WaitForActivityExport(exportedItems, 2);
}
Assert.Equal(2, exportedItems.Count);
var middlewareActivity = exportedItems[0];
var aspnetcoreframeworkactivity = exportedItems[1];
// Middleware activity name should not be changed
Assert.Equal(ActivityKind.Internal, middlewareActivity.Kind);
Assert.Equal(activityName, middlewareActivity.OperationName);
Assert.Equal(activityName, middlewareActivity.DisplayName);
// tag http.method should be added on activity started by asp.net core
Assert.Equal("GET", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string);
Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName);
}
[Theory]
[InlineData("CONNECT", "CONNECT")]
[InlineData("DELETE", "DELETE")]
[InlineData("GET", "GET")]
[InlineData("PUT", "PUT")]
[InlineData("HEAD", "HEAD")]
[InlineData("OPTIONS", "OPTIONS")]
[InlineData("PATCH", "PATCH")]
[InlineData("Get", "GET")]
[InlineData("POST", "POST")]
[InlineData("TRACE", "TRACE")]
[InlineData("CUSTOM", "_OTHER")]
public async Task HttpRequestMethodIsSetAsPerSpec(string originalMethod, string expectedMethod)
{
var exportedItems = new List<Activity>();
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> { [SemanticConventionOptInKeyName] = "http" })
.Build();
void ConfigureTestServices(IServiceCollection services)
{
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.ConfigureServices(services => services.AddSingleton<IConfiguration>(configuration))
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(exportedItems)
.Build();
}
// Arrange
using var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(ConfigureTestServices);
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient();
var message = new HttpRequestMessage();
message.Method = new HttpMethod(originalMethod);
try
{
using var response = await client.SendAsync(message);
response.EnsureSuccessStatusCode();
}
catch
{
// ignore error.
}
WaitForActivityExport(exportedItems, 1);
Assert.Single(exportedItems);
var activity = exportedItems[0];
Assert.Contains(activity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRequestMethod);
if (originalMethod.Equals(expectedMethod, StringComparison.OrdinalIgnoreCase))
{
Assert.DoesNotContain(activity.TagObjects, t => t.Key == SemanticConventions.AttributeHttpRequestMethodOriginal);
}
else
{
Assert.Equal(originalMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethodOriginal) as string);
}
Assert.Equal(expectedMethod, activity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string);
}
[Fact]
public async Task ActivitiesStartedInMiddlewareBySettingHostActivityToNullShouldNotBeUpdated()
{
var exportedItems = new List<Activity>();
var activitySourceName = "TestMiddlewareActivitySource";
var activityName = "TestMiddlewareActivity";
// Arrange
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices((IServiceCollection services) =>
{
services.AddSingleton<ActivityMiddleware.ActivityMiddlewareImpl>(new TestNullHostActivityMiddlewareImpl(activitySourceName, activityName));
services.AddOpenTelemetry()
.WithTracing(builder => builder
.AddAspNetCoreInstrumentation()
.AddSource(activitySourceName)
.AddInMemoryExporter(exportedItems));
});
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
using var response = await client.GetAsync("/api/values/2");
response.EnsureSuccessStatusCode();
WaitForActivityExport(exportedItems, 2);
}
Assert.Equal(2, exportedItems.Count);
var middlewareActivity = exportedItems[0];
var aspnetcoreframeworkactivity = exportedItems[1];
// Middleware activity name should not be changed
Assert.Equal(ActivityKind.Internal, middlewareActivity.Kind);
Assert.Equal(activityName, middlewareActivity.OperationName);
Assert.Equal(activityName, middlewareActivity.DisplayName);
// tag http.method should be added on activity started by asp.net core
Assert.Equal("GET", aspnetcoreframeworkactivity.GetTagValue(SemanticConventions.AttributeHttpRequestMethod) as string);
Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", aspnetcoreframeworkactivity.OperationName);
}
#if NET7_0_OR_GREATER
[Fact]
public async Task UserRegisteredActivitySourceIsUsedForActivityCreationByAspNetCore()
{
var exportedItems = new List<Activity>();
void ConfigureTestServices(IServiceCollection services)
{
services.AddOpenTelemetry()
.WithTracing(builder => builder
.AddAspNetCoreInstrumentation()
.AddInMemoryExporter(exportedItems));
// Register ActivitySource here so that it will be used
// by ASP.NET Core to create activities
// https://github.com/dotnet/aspnetcore/blob/0e5cbf447d329a1e7d69932c3decd1c70a00fbba/src/Hosting/Hosting/src/Internal/WebHost.cs#L152
services.AddSingleton(sp => new ActivitySource("UserRegisteredActivitySource"));
}
// Arrange
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(ConfigureTestServices);
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
// Act
using var response = await client.GetAsync("/api/values");
// Assert
response.EnsureSuccessStatusCode(); // Status Code 200-299
WaitForActivityExport(exportedItems, 1);
}
Assert.Single(exportedItems);
var activity = exportedItems[0];
Assert.Equal("UserRegisteredActivitySource", activity.Source.Name);
}
#endif
[Theory]
[InlineData(1)]
[InlineData(2)]
public async Task ShouldExportActivityWithOneOrMoreExceptionFilters(int mode)
{
var exportedItems = new List<Activity>();
// Arrange
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(
(s) => this.ConfigureExceptionFilters(s, mode, ref exportedItems));
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
// Act
using var response = await client.GetAsync("/api/error");
WaitForActivityExport(exportedItems, 1);
}
// Assert
AssertException(exportedItems);
}
[Fact]
public async Task DiagnosticSourceCallbacksAreReceivedOnlyForSubscribedEvents()
{
int numberOfUnSubscribedEvents = 0;
int numberofSubscribedEvents = 0;
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddAspNetCoreInstrumentation(
new TestHttpInListener(new AspNetCoreInstrumentationOptions())
{
OnEventWrittenCallback = (name, payload) =>
{
switch (name)
{
case HttpInListener.OnStartEvent:
{
numberofSubscribedEvents++;
}
break;
case HttpInListener.OnStopEvent:
{
numberofSubscribedEvents++;
}
break;
default:
{
numberOfUnSubscribedEvents++;
}
break;
}
},
})
.Build();
// Arrange
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values");
// Act
using var response = await client.SendAsync(request);
}
Assert.Equal(0, numberOfUnSubscribedEvents);
Assert.Equal(2, numberofSubscribedEvents);
}
[Fact]
public async Task DiagnosticSourceExceptionCallbackIsReceivedForUnHandledException()
{
int numberOfUnSubscribedEvents = 0;
int numberofSubscribedEvents = 0;
int numberOfExceptionCallbacks = 0;
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddAspNetCoreInstrumentation(
new TestHttpInListener(new AspNetCoreInstrumentationOptions())
{
OnEventWrittenCallback = (name, payload) =>
{
switch (name)
{
case HttpInListener.OnStartEvent:
{
numberofSubscribedEvents++;
}
break;
case HttpInListener.OnStopEvent:
{
numberofSubscribedEvents++;
}
break;
// TODO: Add test case for validating name for both the types
// of exception event.
case HttpInListener.OnUnhandledHostingExceptionEvent:
case HttpInListener.OnUnHandledDiagnosticsExceptionEvent:
{
numberofSubscribedEvents++;
numberOfExceptionCallbacks++;
}
break;
default:
{
numberOfUnSubscribedEvents++;
}
break;
}
},
})
.Build();
// Arrange
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
})
.CreateClient())
{
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/error");
// Act
using var response = await client.SendAsync(request);
}
catch
{
// ignore exception
}
}
Assert.Equal(1, numberOfExceptionCallbacks);
Assert.Equal(0, numberOfUnSubscribedEvents);
Assert.Equal(3, numberofSubscribedEvents);
}
[Fact]
public async Task DiagnosticSourceExceptionCallBackIsNotReceivedForExceptionsHandledInMiddleware()
{
int numberOfUnSubscribedEvents = 0;
int numberOfSubscribedEvents = 0;
int numberOfExceptionCallbacks = 0;
// configure SDK
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddAspNetCoreInstrumentation(
new TestHttpInListener(new AspNetCoreInstrumentationOptions())
{
OnEventWrittenCallback = (name, payload) =>
{
switch (name)
{
case HttpInListener.OnStartEvent:
{
numberOfSubscribedEvents++;
}
break;
case HttpInListener.OnStopEvent:
{
numberOfSubscribedEvents++;
}
break;
// TODO: Add test case for validating name for both the types
// of exception event.
case HttpInListener.OnUnhandledHostingExceptionEvent:
case HttpInListener.OnUnHandledDiagnosticsExceptionEvent:
{
numberOfSubscribedEvents++;
numberOfExceptionCallbacks++;
}
break;
default:
{
numberOfUnSubscribedEvents++;
}
break;
}
},
})
.Build();
using (var client = this.factory
.WithWebHostBuilder(builder =>
{
builder.ConfigureLogging(loggingBuilder => loggingBuilder.ClearProviders());
builder.Configure(app => app
.UseExceptionHandler(handler =>
{
handler.Run(async (ctx) =>
{
await ctx.Response.WriteAsync("handled");
});
}));
})
.CreateClient())
{
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/error");
using var response = await client.SendAsync(request);
}
catch
{
// ignore exception
}
}
Assert.Equal(0, numberOfExceptionCallbacks);
Assert.Equal(0, numberOfUnSubscribedEvents);
Assert.Equal(2, numberOfSubscribedEvents);
}
public void Dispose()
{
this.tracerProvider?.Dispose();
}
private static void WaitForActivityExport(List<Activity> exportedItems, int count)
{
// We need to let End callback execute as it is executed AFTER response was returned.
// In unit tests environment there may be a lot of parallel unit tests executed, so
// giving some breezing room for the End callback to complete
Assert.True(SpinWait.SpinUntil(
() =>
{
Thread.Sleep(10);
return exportedItems.Count >= count;
},
TimeSpan.FromSeconds(1)));
}
private static void ValidateAspNetCoreActivity(Activity activityToValidate, string expectedHttpPath)
{
Assert.Equal(ActivityKind.Server, activityToValidate.Kind);
#if NET7_0_OR_GREATER
Assert.Equal(HttpInListener.AspNetCoreActivitySourceName, activityToValidate.Source.Name);
Assert.Empty(activityToValidate.Source.Version);
#else
Assert.Equal(HttpInListener.ActivitySourceName, activityToValidate.Source.Name);
Assert.Equal(HttpInListener.Version.ToString(), activityToValidate.Source.Version);
#endif
Assert.Equal(expectedHttpPath, activityToValidate.GetTagValue(SemanticConventions.AttributeUrlPath) as string);
}
private static void AssertException(List<Activity> exportedItems)
{
Assert.Single(exportedItems);
var activity = exportedItems[0];
var exMessage = "something's wrong!";
Assert.Single(activity.Events);
Assert.Equal("System.Exception", activity.Events.First().Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionType).Value);
Assert.Equal(exMessage, activity.Events.First().Tags.FirstOrDefault(t => t.Key == SemanticConventions.AttributeExceptionMessage).Value);
ValidateAspNetCoreActivity(activity, "/api/error");
}
private void ConfigureExceptionFilters(IServiceCollection services, int mode, ref List<Activity> exportedItems)
{
switch (mode)
{
case 1:
services.AddMvc(x => x.Filters.Add<ExceptionFilter1>());
break;
case 2:
services.AddMvc(x => x.Filters.Add<ExceptionFilter1>());
services.AddMvc(x => x.Filters.Add<ExceptionFilter2>());
break;
default:
break;
}
this.tracerProvider = Sdk.CreateTracerProviderBuilder()
.AddAspNetCoreInstrumentation(x => x.RecordException = true)
.AddInMemoryExporter(exportedItems)
.Build();
}
private class ExtractOnlyPropagator(ActivityContext activityContext, Baggage baggage) : TextMapPropagator
{
private readonly ActivityContext activityContext = activityContext;
private readonly Baggage baggage = baggage;
public override ISet<string> Fields => throw new NotImplementedException();
public override PropagationContext Extract<T>(PropagationContext context, T carrier, Func<T, string, IEnumerable<string>> getter)
{
return new PropagationContext(this.activityContext, this.baggage);
}
public override void Inject<T>(PropagationContext context, T carrier, Action<T, string, string> setter)
{
throw new NotImplementedException();
}
}
private class TestSampler(SamplingDecision samplingDecision, IEnumerable<KeyValuePair<string, object>> attributes = null) : Sampler
{
private readonly SamplingDecision samplingDecision = samplingDecision;
private readonly IEnumerable<KeyValuePair<string, object>> attributes = attributes;
public override SamplingResult ShouldSample(in SamplingParameters samplingParameters)
{
return new SamplingResult(this.samplingDecision, this.attributes);
}
}
private class TestHttpInListener(AspNetCoreInstrumentationOptions options) : HttpInListener(options)
{
public Action<string, object> OnEventWrittenCallback;
public override void OnEventWritten(string name, object payload)
{
base.OnEventWritten(name, payload);
this.OnEventWrittenCallback?.Invoke(name, payload);
}
}
private class TestNullHostActivityMiddlewareImpl(string activitySourceName, string activityName) : ActivityMiddleware.ActivityMiddlewareImpl
{
private readonly ActivitySource activitySource = new(activitySourceName);
private readonly string activityName = activityName;
private Activity activity;
public override void PreProcess(HttpContext context)
{
// Setting the host activity i.e. activity started by asp.net core
// to null here will have no impact on middleware activity.
// This also means that asp.net core activity will not be found
// during OnEventWritten event.
Activity.Current = null;
this.activity = this.activitySource.StartActivity(this.activityName);
}
public override void PostProcess(HttpContext context)
{
this.activity?.Stop();
}
}
private class TestActivityMiddlewareImpl(string activitySourceName, string activityName) : ActivityMiddleware.ActivityMiddlewareImpl
{
private readonly ActivitySource activitySource = new(activitySourceName);
private readonly string activityName = activityName;
private Activity activity;
public override void PreProcess(HttpContext context)
{
this.activity = this.activitySource.StartActivity(this.activityName);
}
public override void PostProcess(HttpContext context)
{
this.activity?.Stop();
}
}
}