434 lines
19 KiB
C#
434 lines
19 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;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using Microsoft.AspNetCore.TestHost;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Moq;
|
|
using OpenTelemetry.Context.Propagation;
|
|
using OpenTelemetry.Instrumentation.AspNetCore.Implementation;
|
|
using OpenTelemetry.Tests;
|
|
using OpenTelemetry.Trace;
|
|
#if NETCOREAPP2_1
|
|
using TestApp.AspNetCore._2._1;
|
|
#elif NETCOREAPP3_1
|
|
using TestApp.AspNetCore._3._1;
|
|
#else
|
|
using TestApp.AspNetCore._5._0;
|
|
#endif
|
|
using Xunit;
|
|
|
|
namespace OpenTelemetry.Instrumentation.AspNetCore.Tests
|
|
{
|
|
// See https://github.com/aspnet/Docs/tree/master/aspnetcore/test/integration-tests/samples/2.x/IntegrationTestsSample
|
|
public class BasicTests
|
|
: IClassFixture<WebApplicationFactory<Startup>>, IDisposable
|
|
{
|
|
private readonly WebApplicationFactory<Startup> factory;
|
|
private TracerProvider openTelemetrySdk = null;
|
|
|
|
public BasicTests(WebApplicationFactory<Startup> factory)
|
|
{
|
|
this.factory = factory;
|
|
}
|
|
|
|
[Fact]
|
|
public void AddAspNetCoreInstrumentation_BadArgs()
|
|
{
|
|
TracerProviderBuilder builder = null;
|
|
Assert.Throws<ArgumentNullException>(() => builder.AddAspNetCoreInstrumentation());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task StatusIsUnsetOn200Response()
|
|
{
|
|
var activityProcessor = new Mock<BaseProcessor<Activity>>();
|
|
void ConfigureTestServices(IServiceCollection services)
|
|
{
|
|
this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder()
|
|
.AddAspNetCoreInstrumentation()
|
|
.AddProcessor(activityProcessor.Object)
|
|
.Build();
|
|
}
|
|
|
|
// Arrange
|
|
using (var client = this.factory
|
|
.WithWebHostBuilder(builder =>
|
|
builder.ConfigureTestServices(ConfigureTestServices))
|
|
.CreateClient())
|
|
{
|
|
// Act
|
|
var response = await client.GetAsync("/api/values");
|
|
|
|
// Assert
|
|
response.EnsureSuccessStatusCode(); // Status Code 200-299
|
|
|
|
WaitForProcessorInvocations(activityProcessor, 3);
|
|
}
|
|
|
|
Assert.Equal(3, activityProcessor.Invocations.Count); // begin and end was called
|
|
|
|
// we should only call Processor.OnEnd for the "/api/values" request
|
|
Assert.Single(activityProcessor.Invocations, invo => invo.Method.Name == "OnEnd");
|
|
var activity = activityProcessor.Invocations.FirstOrDefault(invo => invo.Method.Name == "OnEnd").Arguments[0] as Activity;
|
|
|
|
Assert.Equal(200, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode));
|
|
|
|
var status = activity.GetStatus();
|
|
Assert.Equal(status, Status.Unset);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(true)]
|
|
[InlineData(false)]
|
|
public async Task SuccessfulTemplateControllerCallGeneratesASpan(bool shouldEnrich)
|
|
{
|
|
var activityProcessor = new Mock<BaseProcessor<Activity>>();
|
|
void ConfigureTestServices(IServiceCollection services)
|
|
{
|
|
this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder()
|
|
.AddAspNetCoreInstrumentation(options =>
|
|
{
|
|
if (shouldEnrich)
|
|
{
|
|
options.Enrich = ActivityEnrichment;
|
|
}
|
|
})
|
|
.AddProcessor(activityProcessor.Object)
|
|
.Build();
|
|
}
|
|
|
|
// Arrange
|
|
using (var client = this.factory
|
|
.WithWebHostBuilder(builder =>
|
|
builder.ConfigureTestServices(ConfigureTestServices))
|
|
.CreateClient())
|
|
{
|
|
// Act
|
|
var response = await client.GetAsync("/api/values");
|
|
|
|
// Assert
|
|
response.EnsureSuccessStatusCode(); // Status Code 200-299
|
|
|
|
WaitForProcessorInvocations(activityProcessor, 3);
|
|
}
|
|
|
|
Assert.Equal(3, activityProcessor.Invocations.Count); // begin and end was called
|
|
var activity = (Activity)activityProcessor.Invocations[2].Arguments[0];
|
|
|
|
ValidateAspNetCoreActivity(activity, "/api/values");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SuccessfulTemplateControllerCallUsesParentContext()
|
|
{
|
|
var activityProcessor = new Mock<BaseProcessor<Activity>>();
|
|
|
|
var expectedTraceId = ActivityTraceId.CreateRandom();
|
|
var expectedSpanId = ActivitySpanId.CreateRandom();
|
|
|
|
// Arrange
|
|
using (var testFactory = this.factory
|
|
.WithWebHostBuilder(builder =>
|
|
builder.ConfigureTestServices(services =>
|
|
{
|
|
this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder().AddAspNetCoreInstrumentation()
|
|
.AddProcessor(activityProcessor.Object)
|
|
.Build();
|
|
})))
|
|
{
|
|
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
|
|
|
|
WaitForProcessorInvocations(activityProcessor, 3);
|
|
}
|
|
|
|
// List of invocations
|
|
// 1. SetParentProvider for TracerProviderSdk
|
|
// 2. OnStart for the activity created by AspNetCore with the OperationName: Microsoft.AspNetCore.Hosting.HttpRequestIn
|
|
// 3. OnStart for the sibling activity created by the instrumentation library with the OperationName: ActivityCreatedByHttpInListener
|
|
// 4. OnEnd for the sibling activity created by the instrumentation library with the OperationName: ActivityCreatedByHttpInListener
|
|
|
|
// we should only call Processor.OnEnd once for the sibling activity with the OperationName ActivityCreatedByHttpInListener
|
|
Assert.Single(activityProcessor.Invocations, invo => invo.Method.Name == "OnEnd");
|
|
var activity = activityProcessor.Invocations.FirstOrDefault(invo => invo.Method.Name == "OnEnd").Arguments[0] as Activity;
|
|
|
|
#if !NETCOREAPP2_1
|
|
// ASP.NET Core after 2.x is W3C aware and hence Activity created by it
|
|
// must be used.
|
|
Assert.Equal("Microsoft.AspNetCore.Hosting.HttpRequestIn", activity.OperationName);
|
|
#else
|
|
// ASP.NET Core before 3.x is not W3C aware and hence Activity created by it
|
|
// is always ignored and new one is created by the Instrumentation
|
|
Assert.Equal("ActivityCreatedByHttpInListener", activity.OperationName);
|
|
#endif
|
|
Assert.Equal(ActivityKind.Server, activity.Kind);
|
|
Assert.Equal("api/Values/{id}", activity.DisplayName);
|
|
Assert.Equal("/api/values/2", activity.GetTagValue(SpanAttributeConstants.HttpPathKey) as string);
|
|
|
|
Assert.Equal(expectedTraceId, activity.Context.TraceId);
|
|
Assert.Equal(expectedSpanId, activity.ParentSpanId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CustomPropagator()
|
|
{
|
|
var activityProcessor = new Mock<BaseProcessor<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);
|
|
this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder()
|
|
.AddAspNetCoreInstrumentation()
|
|
.AddProcessor(activityProcessor.Object)
|
|
.Build();
|
|
})))
|
|
{
|
|
using var client = testFactory.CreateClient();
|
|
var response = await client.GetAsync("/api/values/2");
|
|
response.EnsureSuccessStatusCode(); // Status Code 200-299
|
|
|
|
WaitForProcessorInvocations(activityProcessor, 4);
|
|
}
|
|
|
|
// List of invocations on the processor
|
|
// 1. SetParentProvider for TracerProviderSdk
|
|
// 2. OnStart for the activity created by AspNetCore with the OperationName: Microsoft.AspNetCore.Hosting.HttpRequestIn
|
|
// 3. OnStart for the sibling activity created by the instrumentation library with the OperationName: ActivityCreatedByHttpInListener
|
|
// 4. OnEnd for the sibling activity created by the instrumentation library with the OperationName: ActivityCreatedByHttpInListener
|
|
Assert.Equal(4, activityProcessor.Invocations.Count);
|
|
|
|
var startedActivities = activityProcessor.Invocations.Where(invo => invo.Method.Name == "OnStart");
|
|
var stoppedActivities = activityProcessor.Invocations.Where(invo => invo.Method.Name == "OnEnd");
|
|
Assert.Equal(2, startedActivities.Count());
|
|
Assert.Single(stoppedActivities);
|
|
|
|
// The activity created by the framework and the sibling activity are both sent to Processor.OnStart
|
|
Assert.Contains(startedActivities, item =>
|
|
{
|
|
var startedActivity = item.Arguments[0] as Activity;
|
|
return startedActivity.OperationName == HttpInListener.ActivityOperationName;
|
|
});
|
|
|
|
Assert.Contains(startedActivities, item =>
|
|
{
|
|
var startedActivity = item.Arguments[0] as Activity;
|
|
return startedActivity.OperationName == HttpInListener.ActivityNameByHttpInListener;
|
|
});
|
|
|
|
// Only the sibling activity is sent to Processor.OnEnd
|
|
Assert.Contains(stoppedActivities, item =>
|
|
{
|
|
var stoppedActivity = item.Arguments[0] as Activity;
|
|
return stoppedActivity.OperationName == HttpInListener.ActivityNameByHttpInListener;
|
|
});
|
|
|
|
var activity = activityProcessor.Invocations.FirstOrDefault(invo => invo.Method.Name == "OnEnd").Arguments[0] as Activity;
|
|
Assert.Equal(ActivityKind.Server, activity.Kind);
|
|
Assert.True(activity.Duration != TimeSpan.Zero);
|
|
Assert.Equal("api/Values/{id}", activity.DisplayName);
|
|
Assert.Equal("/api/values/2", activity.GetTagValue(SpanAttributeConstants.HttpPathKey) as string);
|
|
|
|
Assert.Equal(expectedTraceId, activity.Context.TraceId);
|
|
Assert.Equal(expectedSpanId, activity.ParentSpanId);
|
|
Sdk.SetDefaultTextMapPropagator(new CompositeTextMapPropagator(new TextMapPropagator[]
|
|
{
|
|
new TraceContextPropagator(),
|
|
new BaggagePropagator(),
|
|
}));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RequestNotCollectedWhenFilterIsApplied()
|
|
{
|
|
var activityProcessor = new Mock<BaseProcessor<Activity>>();
|
|
|
|
void ConfigureTestServices(IServiceCollection services)
|
|
{
|
|
this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder()
|
|
.AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => ctx.Request.Path != "/api/values/2")
|
|
.AddProcessor(activityProcessor.Object)
|
|
.Build();
|
|
}
|
|
|
|
// Arrange
|
|
using (var testFactory = this.factory
|
|
.WithWebHostBuilder(builder =>
|
|
builder.ConfigureTestServices(ConfigureTestServices)))
|
|
{
|
|
using var client = testFactory.CreateClient();
|
|
|
|
// Act
|
|
var response1 = await client.GetAsync("/api/values");
|
|
var response2 = await client.GetAsync("/api/values/2");
|
|
|
|
// Assert
|
|
response1.EnsureSuccessStatusCode(); // Status Code 200-299
|
|
response2.EnsureSuccessStatusCode(); // Status Code 200-299
|
|
|
|
WaitForProcessorInvocations(activityProcessor, 4);
|
|
}
|
|
|
|
// 1. SetParentProvider for TracerProviderSdk
|
|
// 2. OnStart for the activity created by AspNetCore for "/api/values" with the OperationName: Microsoft.AspNetCore.Hosting.HttpRequestIn
|
|
// 3. OnEnd for the activity created by AspNetCore for "/api/values" with the OperationName: Microsoft.AspNetCore.Hosting.HttpRequestIn
|
|
// 4. OnStart for the activity created by AspNetCore for "/api/values/2" with the OperationName: Microsoft.AspNetCore.Hosting.HttpRequestIn
|
|
Assert.Equal(4, activityProcessor.Invocations.Count);
|
|
|
|
// we should only call Processor.OnEnd for the "/api/values" request
|
|
Assert.Single(activityProcessor.Invocations, invo => invo.Method.Name == "OnEnd");
|
|
var activity = activityProcessor.Invocations.FirstOrDefault(invo => invo.Method.Name == "OnEnd").Arguments[0] as Activity;
|
|
|
|
Assert.Equal(ActivityKind.Server, activity.Kind);
|
|
Assert.Equal("/api/values", activity.GetTagValue(SpanAttributeConstants.HttpPathKey) as string);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RequestNotCollectedWhenFilterThrowException()
|
|
{
|
|
var activityProcessor = new Mock<BaseProcessor<Activity>>();
|
|
|
|
void ConfigureTestServices(IServiceCollection services)
|
|
{
|
|
this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder()
|
|
.AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) =>
|
|
{
|
|
if (ctx.Request.Path == "/api/values/2")
|
|
{
|
|
throw new Exception("from InstrumentationFilter");
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
})
|
|
.AddProcessor(activityProcessor.Object)
|
|
.Build();
|
|
}
|
|
|
|
// Arrange
|
|
using (var testFactory = this.factory
|
|
.WithWebHostBuilder(builder =>
|
|
builder.ConfigureTestServices(ConfigureTestServices)))
|
|
{
|
|
using var client = testFactory.CreateClient();
|
|
|
|
// Act
|
|
using (var inMemoryEventListener = new InMemoryEventListener(AspNetCoreInstrumentationEventSource.Log))
|
|
{
|
|
var response1 = await client.GetAsync("/api/values");
|
|
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));
|
|
}
|
|
|
|
WaitForProcessorInvocations(activityProcessor, 3);
|
|
}
|
|
|
|
// As InstrumentationFilter threw, we continue as if the
|
|
// InstrumentationFilter did not exist.
|
|
|
|
// List of invocations on the processor
|
|
// 1. SetParentProvider for TracerProviderSdk
|
|
// 2. OnStart for the activity created by AspNetCore for "/api/values" with the OperationName: Microsoft.AspNetCore.Hosting.HttpRequestIn
|
|
// 3. OnEnd for the activity created by AspNetCore for "/api/values" with the OperationName: Microsoft.AspNetCore.Hosting.HttpRequestIn
|
|
// 4. OnStart for the activity created by AspNetCore for "/api/values/2" with the OperationName: Microsoft.AspNetCore.Hosting.HttpRequestIn
|
|
|
|
// we should only call Processor.OnEnd for the "/api/values" request
|
|
Assert.Single(activityProcessor.Invocations, invo => invo.Method.Name == "OnEnd");
|
|
var activity = activityProcessor.Invocations.FirstOrDefault(invo => invo.Method.Name == "OnEnd").Arguments[0] as Activity;
|
|
|
|
Assert.Equal(ActivityKind.Server, activity.Kind);
|
|
Assert.Equal("/api/values", activity.GetTagValue(SpanAttributeConstants.HttpPathKey) as string);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
this.openTelemetrySdk?.Dispose();
|
|
}
|
|
|
|
private static void WaitForProcessorInvocations(Mock<BaseProcessor<Activity>> activityProcessor, int invocationCount)
|
|
{
|
|
// 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 activityProcessor.Invocations.Count >= invocationCount;
|
|
},
|
|
TimeSpan.FromSeconds(1)));
|
|
}
|
|
|
|
private static void ValidateAspNetCoreActivity(Activity activityToValidate, string expectedHttpPath)
|
|
{
|
|
Assert.Equal(ActivityKind.Server, activityToValidate.Kind);
|
|
Assert.Equal(expectedHttpPath, activityToValidate.GetTagValue(SpanAttributeConstants.HttpPathKey) as string);
|
|
}
|
|
|
|
private static void ActivityEnrichment(Activity activity, string method, object obj)
|
|
{
|
|
Assert.True(activity.IsAllDataRequested);
|
|
switch (method)
|
|
{
|
|
case "OnStartActivity":
|
|
Assert.True(obj is HttpRequest);
|
|
break;
|
|
|
|
case "OnStopActivity":
|
|
Assert.True(obj is HttpResponse);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|