Repository navigation
Description
Recently I've started writing some integration tests, and earlier today I started trying to add tests for my telemetry. Using the System.Diagnostics.ActivitySource.AddActivityListener method this seemingly worked quite well - while running a single test at a time. After finalizing the tests I pressed the "run all tests" button in my IDE to validate that I hadn't broken anything, and everything blew up. This took me several hours to debug, but in the end I figured out it was a heavy concurrency issue at play with regards to the ActivityListener.
Currently, the only way to test activities [... that I've figured out] is using the ActivityListener, and for the life of me I haven't been able to figure out how to a) Work around the concurrency issue, or b) Make xUnit not run in parallel (which is far from ideal even if I did figure out how).
Here are a few concrete examples of what I want to test:
// Arrange
ValidationProblemDetails expectedValidationProblemDetails = ...;
MultipartFormDataContent formContent = ...;
using var client = _factory.CreateClient();
Uri requestUri = ...;
// Act
var response = await client.PostAsync(requestUri, formContent).ConfigureAwait(true);
var responseContent = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>().ConfigureAwait(true);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
responseContent.Should().BeEquivalentTo(expectedValidationProblemDetails);
_activities
.Should()
.ContainSingle(x =>
string.Equals(x.OperationName, "Microsoft.AspNetCore.Hosting.HttpRequestIn", StringComparison.Ordinal)
)
.Which.TagObjects.Should()
.ContainSingle(x => string.Equals(x.Key, "http.response.status_code", StringComparison.Ordinal))
.Which.Value.Should()
.Be(expected: 422);where _activities collection is populated like this (this is where the problem lies).
_activities = [];
ActivityListener listener =
new()
{
Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
ActivityStarted = activity => _activities.Add(activity),
ShouldListenTo = _ => true,
SampleUsingParentId = (ref ActivityCreationOptions<string> _) => ActivitySamplingResult.AllData,
};
ActivitySource.AddActivityListener(listener);So my question is this:
- Is this how you're supposed to test activities? If it is, then it's clearly not working. It does feel using the listener like this, having a closure and updating it in a callback like this.
- If it's not, what is it? I couldn't find documentation anywhere on this. The closest I got was this article, in which case all I saw was the following, which implies it's possible
In my typical use case, I'm using sampling to do integration testing, so I'll typically have my listener set to ActivitySamplingResult.AllData.
Activity
WeihanLi commentedon Feb 24, 2024
Have a similar question before dotnet/extensions#4825
Maybe we could start an activity before the tracing logic then we could filter the activities using parentId/traceId
KennethHoff commentedon Feb 24, 2024
@WeihanLi I saw your thread (probably should've mentioned it), but I didn't get it to work.
Parentwas always null. Did you figure out how to do it?It's still very much a hack, but still - if it works, it works 😅
WeihanLi commentedon Feb 25, 2024
@KennethHoff I add an example here, hope it helps
https://github.com/WeihanLi/SamplesInPractice/blob/main/XunitSample/ActivityTest.cs
KennethHoff commentedon Feb 25, 2024
@WeihanLi For whatever reason, the
traceIddoes not cross this boundary. I'm guessing this is an Asp.Net Core thing though.(No, moving the activity creation to after factory or client creation has no effect)
WeihanLi commentedon Feb 25, 2024
for asp.net core, there's little difference, let me try with another example
https://github.com/WeihanLi/XunitDependencyInjection.Samples/blob/main/P6_MinimalAPI/MinimalApiTest/ApiTest.cs#L33
you can attach the parentId as
traceparentrequest header, see details: https://github.com/dotnet/aspnetcore/blob/db01a95c38c1966664f311d5507c600f210f475d/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs#L379KennethHoff commentedon Feb 25, 2024
@WeihanLi !! That seems to have worked! Hurray!
It's still very ugly, but at least you (seem to be able to) work around it somewhat!
WeihanLi commentedon Feb 25, 2024
Yeah, it is 😅. Hope to see some more elegant ways from others.
@tarekgh any suggestions about this?
KennethHoff commentedon Feb 25, 2024
@WeihanLi
Actually, never mind - It doesn't seem like it actually fixed it. It made it better, but by no means "fixed".
Concurrency issues like this are an absolute hell to debug, as they work while debugging/running one at a time...
tarekgh commentedon Feb 25, 2024
@KennethHoff in your test mentioned in #98854 (comment) what exactly the failure you were getting?
Your listener's callbacks can be called synchronousely if more than one activity get started in same time from different threads. I am seeing your listener callback is not written in thread safe way. I mean:
Should be done like
If you can tell exactly the failure you are seeing or send a complete isolated repro I can try to help you more. Thanks!
CC @noahfalk
14 remaining items
KennethHoff commentedon Feb 26, 2024
@tarekgh https://github.com/KennethHoff/Repros/tree/b25106f575ee5c782f99dfcfe89b5fc6eb53a900/ActivityTesting
I've figured a few things out:
1.
Azure.Monitor.OpenTelemetry.AspNetCoreis implemented even worse than I originally thought. I've always known it does things in their own wonky non-composable way, but I didn't think they did it in such a bad way that it causes concurrency issues.2.
OpenTelemetry.Instrumentation.AspNetCoreis inconsistent. Sometimes theDisplayNamefor the request's activity is"POST <endpointName>"other times it's just"POST"This is much less of an issue than theAzure.Monitor.OpenTelemetry.AspNetCoreone though, but still good to know.tarekgh commentedon Feb 27, 2024
I think the issue is not related to activity listener or concurrency inside the runtime.
@reyang @CodeBlanch @cijothomas Do you have any insights into what might be happening in this situation?
The test:
https://github.com/KennethHoff/Repros/blob/b25106f575ee5c782f99dfcfe89b5fc6eb53a900/ActivityTesting/ActivityTesting.Tests/IntegrationTests.cs#L62
is calling
var response = await client.PostAsync(uri, httpContent);and then attempt to retrieve the count of activities generated from this call, using the same trace ID as the parent activity created by the test. The number of activities sharing the same trace ID also increases. This issue not happening when not using Azure monitor configuration. Here is small example of the activities get created during one iteration of the test.tarekgh commentedon Feb 27, 2024
@KennethHoff as it looks the issue now is not about the activity listening, I am closing this issue here and suggest you log it in https://github.com/microsoft/AzureMonitorCommunity to get better traction. Thanks for your report.
noahfalk commentedon Feb 27, 2024
@KennethHoff - Are your tests using
WebApplicationFactoryand divided across multiple test classes? I'm guessing they are based on what you described although nothing in the example code explicitly shows it.Each
WebApplicationFactoryhas a service container in it and one of the items in that container is theActivitySourcethat the corresponding Kestrel host is using to emit all of its telemetry. You could retrieve thatActivitySourceinstance like this:Then down in your
ActivityListenerinstead of telling it to listen to all sources (which includes the sources being used by the concurrently running tests), just tell it to listen to the source being used in this test:This way you can listen to only the telemetry from your test and not the telemetry from other concurrently running tests. It's possible there are still some other issues lurking but I suspect this was probably a big one causing you concurrency trouble.
Hope that helps!
KennethHoff commentedon Feb 27, 2024
@noahfalk This seems to have fixed the concurrency issue, but it creates another issue; It only works for the built-in
ActivitySource, so it doesn't allow me to test my own custom activities.I tried adding my own (keyed) singleton
ActivitySourceand use that for my own activities. That worked, but that created it's own concurrency issues (... somehow... See the previously linked repro repo (Here) - now updated with DI-based version.I then tried to just use the Kestrel-provided one*. That worked, but that feels wrong, but is there a reason not to do that?
It also seemed to have cleaned up the mess caused by
UseAzureMonitor. All combinations ofUseAzureMonitorandUseAspNetCoreInstrumentationnow works identically (Except for the one with both set tofalsewhich is lacking OTel-stuff, which makes sense), but that in itself is a little worrying - What telemetry did I just lose that AzureMonitor previously added? 🤔* Technically I did it the other way around, as you can see in the GitHub commit history ;)