From accc6de7ebeac6f07f5071493dd2e0dc65983f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Wed, 8 Jan 2025 09:43:18 +0100 Subject: [PATCH] [ContinuousProfiler] Context Tracking Test - switch to OTLP --- .../ContinuousProfilerContextTrackingTests.cs | 67 ++++++------------- .../Helpers/MockProfilesCollector.cs | 47 +++++++++++++ .../Program.cs | 4 +- ....ContinuousProfiler.ContextTracking.csproj | 20 ++++++ .../TestExporter.cs | 31 --------- .../TestPlugin.cs | 2 +- 6 files changed, 93 insertions(+), 78 deletions(-) delete mode 100644 test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/TestExporter.cs diff --git a/test/IntegrationTests/ContinuousProfilerContextTrackingTests.cs b/test/IntegrationTests/ContinuousProfilerContextTrackingTests.cs index 0c7b94a9f9..4b714f1653 100644 --- a/test/IntegrationTests/ContinuousProfilerContextTrackingTests.cs +++ b/test/IntegrationTests/ContinuousProfilerContextTrackingTests.cs @@ -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; @@ -22,69 +23,45 @@ 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(); - var totalSamplesWithTraceContextCount = 0; - var managedThreadsWithTraceContext = new HashSet(); + collector.AssertCollected(); + } - var exportedSampleBatches = standardOutput.TrimEnd().Split(batchSeparator); + private bool AssertAllProfiles(ICollection profilesServiceRequests) + { + var totalSamplesWithTraceContextCount = 0; + var managedThreadsWithTraceContext = new HashSet(); - foreach (var sampleBatch in exportedSampleBatches) + foreach (var batch in profilesServiceRequests) { - var batch = JsonDocument.Parse(sampleBatch.TrimStart()); + IEnumerable samplesInBatch = batch + .ResourceProfiles + .SelectMany(rp => rp.ScopeProfiles) + .SelectMany(sp => sp.Profiles) + .SelectMany(p => p.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(sampleWithTraceContext.AttributeIndices.Single()); } } 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 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 jsonProperties) - { - return jsonProperties - .Single( - property => - property.Name == propertyName) - .Value; - } - - private static List ConvertToPropertyList(JsonElement threadSampleDocument) - { - return threadSampleDocument - .EnumerateObject() - .ToList(); + return true; } } #endif diff --git a/test/IntegrationTests/Helpers/MockProfilesCollector.cs b/test/IntegrationTests/Helpers/MockProfilesCollector.cs index aa0ae3c434..f8495a68b4 100644 --- a/test/IntegrationTests/Helpers/MockProfilesCollector.cs +++ b/test/IntegrationTests/Helpers/MockProfilesCollector.cs @@ -19,6 +19,7 @@ public class MockProfilesCollector : IDisposable private readonly List _expectations = new(); private readonly BlockingCollection _profilesSnapshots = new(10); // bounded to avoid memory leak; contains protobuf type + private CollectedExpectation? _collectedExpectation; public MockProfilesCollector(ITestOutputHelper output) { @@ -48,6 +49,26 @@ public void Expect(Func? predicate = null, s _expectations.Add(new Expectation(predicate, description)); } + public void ExpectCollected(Func, 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) @@ -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(); @@ -185,6 +219,19 @@ public Expectation(Func predicate, string? d public string? Description { get; } } + + private class CollectedExpectation + { + public CollectedExpectation(Func, bool> predicate, string? description) + { + Predicate = predicate; + Description = description; + } + + public Func, bool> Predicate { get; } + + public string? Description { get; } + } } #endif diff --git a/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/Program.cs b/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/Program.cs index 264c02dc83..84780dfbeb 100644 --- a/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/Program.cs +++ b/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/Program.cs @@ -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); diff --git a/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/TestApplication.ContinuousProfiler.ContextTracking.csproj b/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/TestApplication.ContinuousProfiler.ContextTracking.csproj index e59c6c6912..5250ceb330 100644 --- a/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/TestApplication.ContinuousProfiler.ContextTracking.csproj +++ b/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/TestApplication.ContinuousProfiler.ContextTracking.csproj @@ -5,16 +5,30 @@ net9.0;net8.0 enable enable + + SA1518;$(NoWarn) + + + Exporter\AllocationSample.cs + + Exporter\ExtendedPprofBuilder.cs + + + Exporter\OtlpOverHttpExporter.cs + + + Exporter\SampleBuilder.cs + Exporter\SampleNativeFormatParser.cs @@ -23,4 +37,10 @@ + + + + + + diff --git a/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/TestExporter.cs b/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/TestExporter.cs deleted file mode 100644 index ff9d091cbf..0000000000 --- a/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/TestExporter.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright The OpenTelemetry Authors -// SPDX-License-Identifier: Apache-2.0 - -using System.Text; -using System.Text.Json; - -namespace TestApplication.ContinuousProfiler.ContextTracking; - -public class TestExporter -{ - static TestExporter() - { - Console.OutputEncoding = Encoding.Default; - } - - public void ExportThreadSamples(byte[] buffer, int read, CancellationToken cancellationToken) - { - var threadSamples = SampleNativeFormatParser.ParseThreadSamples(buffer, read); - - var value = JsonSerializer.Serialize(threadSamples, new JsonSerializerOptions - { - WriteIndented = true, - }); - Console.WriteLine(value); - Console.WriteLine(); - } - - public void ExportAllocationSamples(byte[] buffer, int read, CancellationToken cancellationToken) - { - } -} diff --git a/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/TestPlugin.cs b/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/TestPlugin.cs index 6f33e3ac9b..9cb22022c3 100644 --- a/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/TestPlugin.cs +++ b/test/test-applications/integrations/TestApplication.ContinuousProfiler.ContextTracking/TestPlugin.cs @@ -13,7 +13,7 @@ public Tuple 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); }