Skip to content

Commit

Permalink
[ContinuousProfiler] Context Tracking Test - switch to OTLP (#3917)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kielek authored Jan 9, 2025
1 parent 72f1742 commit 142d826
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 77 deletions.
63 changes: 19 additions & 44 deletions test/IntegrationTests/ContinuousProfilerContextTrackingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@

#if NET

using System.Text.Json;
using FluentAssertions;
using IntegrationTests.Helpers;
using OpenTelemetry.Proto.Collector.Profiles.V1Development;
using OpenTelemetry.Proto.Profiles.V1Development;
using Xunit.Abstractions;

namespace IntegrationTests;
Expand All @@ -22,69 +23,43 @@ public ContinuousProfilerContextTrackingTests(ITestOutputHelper output)
public void TraceContextIsCorrectlyAssociatedWithThreadSamples()
{
EnableBytecodeInstrumentation();
using var collector = new MockProfilesCollector(Output);
SetExporter(collector);
SetEnvironmentVariable("OTEL_DOTNET_AUTO_PLUGINS", "TestApplication.ContinuousProfiler.ContextTracking.TestPlugin, TestApplication.ContinuousProfiler.ContextTracking, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");
SetEnvironmentVariable("OTEL_DOTNET_AUTO_TRACES_ADDITIONAL_SOURCES", "TestApplication.ContinuousProfiler.ContextTracking");

var (standardOutput, _, _) = RunTestApplication();
collector.ExpectCollected(AssertAllProfiles, $"{nameof(AssertAllProfiles)} failed");

var batchSeparator = $"{Environment.NewLine}{Environment.NewLine}";
RunTestApplication();

collector.AssertCollected();
}

private bool AssertAllProfiles(ICollection<ExportProfilesServiceRequest> profilesServiceRequests)
{
var totalSamplesWithTraceContextCount = 0;
var managedThreadsWithTraceContext = new HashSet<string>();

var exportedSampleBatches = standardOutput.TrimEnd().Split(batchSeparator);

foreach (var sampleBatch in exportedSampleBatches)
foreach (var batch in profilesServiceRequests)
{
var batch = JsonDocument.Parse(sampleBatch.TrimStart());
var profile = batch.ResourceProfiles.Single().ScopeProfiles.Single().Profiles.Single();

var samplesInBatch = profile.Sample;

var samplesWithTraceContext = samplesInBatch.Where(s => s.HasLinkIndex).ToList();

var samplesWithTraceContext = batch
.RootElement
.EnumerateArray()
.Select(
sample =>
ConvertToPropertyList(sample))
.Where(
sampleProperties =>
HasTraceContextAssociated(sampleProperties))
.ToList();
samplesWithTraceContext.Count.Should().BeLessOrEqualTo(1, "at most one sample in a batch should have trace context associated.");

totalSamplesWithTraceContextCount += samplesWithTraceContext.Count;
if (samplesWithTraceContext.FirstOrDefault() is { } sampleWithTraceContext)
{
managedThreadsWithTraceContext.Add(GetPropertyValue("ThreadName", sampleWithTraceContext).GetString()!);
managedThreadsWithTraceContext.Add(profile.AttributeTable[sampleWithTraceContext.AttributeIndices.Single()].Value.StringValue);
}
}

managedThreadsWithTraceContext.Should().HaveCountGreaterThan(1, "at least 2 distinct threads should have trace context associated.");
totalSamplesWithTraceContextCount.Should().BeGreaterOrEqualTo(3, "there should be sample with trace context in most of the batches.");
}

private static bool HasTraceContextAssociated(List<JsonProperty> sample)
{
const int defaultTraceContextValue = 0;

return GetPropertyValue("SpanId", sample).GetInt64() != defaultTraceContextValue &&
GetPropertyValue("TraceIdHigh", sample).GetInt64() != defaultTraceContextValue &&
GetPropertyValue("TraceIdLow", sample).GetInt64() != defaultTraceContextValue &&
!string.IsNullOrWhiteSpace(GetPropertyValue("ThreadName", sample).GetString());
}

private static JsonElement GetPropertyValue(string propertyName, List<JsonProperty> jsonProperties)
{
return jsonProperties
.Single(
property =>
property.Name == propertyName)
.Value;
}

private static List<JsonProperty> ConvertToPropertyList(JsonElement threadSampleDocument)
{
return threadSampleDocument
.EnumerateObject()
.ToList();
return true;
}
}
#endif
47 changes: 47 additions & 0 deletions test/IntegrationTests/Helpers/MockProfilesCollector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class MockProfilesCollector : IDisposable

private readonly List<Expectation> _expectations = new();
private readonly BlockingCollection<Collected> _profilesSnapshots = new(10); // bounded to avoid memory leak; contains protobuf type
private CollectedExpectation? _collectedExpectation;

public MockProfilesCollector(ITestOutputHelper output)
{
Expand Down Expand Up @@ -48,6 +49,26 @@ public void Expect(Func<ExportProfilesServiceRequest, bool>? predicate = null, s
_expectations.Add(new Expectation(predicate, description));
}

public void ExpectCollected(Func<ICollection<ExportProfilesServiceRequest>, bool> collectedExpectation, string description)
{
_collectedExpectation = new(collectedExpectation, description);
}

public void AssertCollected()
{
if (_collectedExpectation == null)
{
throw new InvalidOperationException("Expectation for collected profiling snapshot was not set");
}

var collected = _profilesSnapshots.Select(collected => collected.ExportProfilesServiceRequest).ToArray();

if (!_collectedExpectation.Predicate(collected))
{
FailCollectedExpectation(_collectedExpectation.Description, collected);
}
}

public void AssertExpectations(TimeSpan? timeout = null)
{
if (_expectations.Count == 0)
Expand Down Expand Up @@ -133,6 +154,19 @@ private static void FailExpectations(
Assert.Fail(message.ToString());
}

private static void FailCollectedExpectation(string? collectedExpectationDescription, ExportProfilesServiceRequest[] collectedExportProfilesServiceRequests)
{
var message = new StringBuilder();
message.AppendLine($"Collected logs expectation failed: {collectedExpectationDescription}");
message.AppendLine("Collected logs:");
foreach (var logRecord in collectedExportProfilesServiceRequests)
{
message.AppendLine($" \"{logRecord}\"");
}

Assert.Fail(message.ToString());
}

private async Task HandleHttpRequests(HttpContext ctx)
{
using var bodyStream = await ctx.ReadBodyToMemoryAsync();
Expand Down Expand Up @@ -185,6 +219,19 @@ public Expectation(Func<ExportProfilesServiceRequest, bool> predicate, string? d

public string? Description { get; }
}

private class CollectedExpectation
{
public CollectedExpectation(Func<ICollection<ExportProfilesServiceRequest>, bool> predicate, string? description)
{
Predicate = predicate;
Description = description;
}

public Func<ICollection<ExportProfilesServiceRequest>, bool> Predicate { get; }

public string? Description { get; }
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ private static async Task DoSomethingAsync()
{
// timeout aligned with thread sampling interval
var timeout = TimeSpan.FromSeconds(1);
Thread.Sleep(timeout);

// extended first time sleep to ensure that stack will be fetched from the main thread
Thread.Sleep(TimeSpan.FromSeconds(5));
// continue on thread pool thread
await Task.Yield();
Thread.Sleep(timeout);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,30 @@
<TargetFrameworks>net9.0;net8.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- SA1518 needed for files generated by Grpc.Tools -->
<NoWarn>SA1518;$(NoWarn)</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
<PackageReference Include="Grpc.Net.Client" />
<PackageReference Include="Google.Protobuf" />
<PackageReference Include="Grpc.Tools" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\TestApplication.ContinuousProfiler\Exporter\AllocationSample.cs">
<Link>Exporter\AllocationSample.cs</Link>
</Compile>
<Compile Include="..\TestApplication.ContinuousProfiler\Exporter\ExtendedPprofBuilder.cs">
<Link>Exporter\ExtendedPprofBuilder.cs</Link>
</Compile>
<Compile Include="..\TestApplication.ContinuousProfiler\Exporter\OtlpOverHttpExporter.cs">
<Link>Exporter\OtlpOverHttpExporter.cs</Link>
</Compile>
<Compile Include="..\TestApplication.ContinuousProfiler\Exporter\SampleBuilder.cs">
<Link>Exporter\SampleBuilder.cs</Link>
</Compile>
<Compile Include="..\TestApplication.ContinuousProfiler\Exporter\SampleNativeFormatParser.cs">
<Link>Exporter\SampleNativeFormatParser.cs</Link>
</Compile>
Expand All @@ -23,4 +37,10 @@
</Compile>
</ItemGroup>

<ItemGroup>
<Protobuf Include="..\..\..\IntegrationTests\opentelemetry\proto\common\v1\common.proto" Link="opentelemetry\proto\common\v1\common.proto" ProtoRoot="..\..\..\IntegrationTests" Access="internal" />
<Protobuf Include="..\..\..\IntegrationTests\opentelemetry\proto\resource\v1\resource.proto" Link="opentelemetry\proto\resource\v1\resource.proto" ProtoRoot="..\..\..\IntegrationTests" Access="internal" />
<Protobuf Include="..\..\..\IntegrationTests\opentelemetry\proto\profiles\v1development\profiles.proto" Link="opentelemetry\proto\profiles\v1development\profiles.proto" ProtoRoot="..\..\..\IntegrationTests" Access="internal" />
<Protobuf Include="..\..\..\IntegrationTests\opentelemetry\proto\collector\profiles\v1development\profiles_service.proto" Link="opentelemetry\proto\collector\profiles\v1development\profiles_service.proto" ProtoRoot="..\..\..\IntegrationTests" Access="internal" />
</ItemGroup>
</Project>

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public Tuple<bool, uint, bool, uint, TimeSpan, TimeSpan, object> GetContinuousPr
var maxMemorySamplesPerMinute = 200u;
var exportInterval = TimeSpan.FromMilliseconds(500);
var exportTimeout = TimeSpan.FromMilliseconds(500);
object continuousProfilerExporter = new TestExporter();
object continuousProfilerExporter = new OtlpOverHttpExporter();

return Tuple.Create(threadSamplingEnabled, threadSamplingInterval, allocationSamplingEnabled, maxMemorySamplesPerMinute, exportInterval, exportTimeout, continuousProfilerExporter);
}
Expand Down

0 comments on commit 142d826

Please sign in to comment.