From 22bb5ef15ed81a1a8e422aea98ef9266a891280d Mon Sep 17 00:00:00 2001 From: martincostello Date: Thu, 2 Oct 2025 16:56:50 +0100 Subject: [PATCH 1/6] [OTLP] Refactor integration tests - Refactor tests to use `[MemberData]` to avoid duplication. - Fix code analysis suggestions. - Log event level. --- .../IntegrationTest/IntegrationTests.cs | 186 +++++++++++------- 1 file changed, 112 insertions(+), 74 deletions(-) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs index d5d69cca093..f91af939b60 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs @@ -19,10 +19,19 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; public sealed class IntegrationTests : IDisposable { private const string CollectorHostnameEnvVarName = "OTEL_COLLECTOR_HOSTNAME"; - private const int ExportIntervalMilliseconds = 10000; + private const int ExportIntervalMilliseconds = 10_000; + private const string GrpcEndpointHttp = ":4317"; + private const string GrpcEndpointHttps = ":5317"; + private const string ProtobufEndpointHttp = ":4318/v1/"; + private const string ProtobufEndpointHttps = ":5318/v1/"; + private static readonly SdkLimitOptions DefaultSdkLimitOptions = new(); private static readonly ExperimentalOptions DefaultExperimentalOptions = new(); private static readonly string? CollectorHostname = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(CollectorHostnameEnvVarName); + + private static readonly bool[] BooleanValues = [false, true]; + private static readonly ExportProcessorType[] ExportProcessorTypes = [ExportProcessorType.Batch, ExportProcessorType.Simple]; + private readonly OpenTelemetryEventListener openTelemetryEventListener; public IntegrationTests(ITestOutputHelper outputHelper) @@ -30,38 +39,86 @@ public IntegrationTests(ITestOutputHelper outputHelper) this.openTelemetryEventListener = new(outputHelper); } - public void Dispose() + public static TheoryData TraceTestCases() + { + var data = new TheoryData(); + +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning + foreach (var exportType in ExportProcessorTypes) + { + foreach (var forceFlush in BooleanValues) + { + data.Add(OtlpExportProtocol.Grpc, GrpcEndpointHttp, exportType, forceFlush, Uri.UriSchemeHttp); + data.Add(OtlpExportProtocol.HttpProtobuf, $"{ProtobufEndpointHttp}traces", exportType, forceFlush, Uri.UriSchemeHttp); + } + } + + data.Add(OtlpExportProtocol.Grpc, GrpcEndpointHttps, ExportProcessorType.Simple, true, Uri.UriSchemeHttps); + data.Add(OtlpExportProtocol.HttpProtobuf, $"{ProtobufEndpointHttps}traces", ExportProcessorType.Simple, true, Uri.UriSchemeHttps); +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning + + return data; + } + + public static TheoryData MetricsTestCases() { - this.openTelemetryEventListener.Dispose(); + var data = new TheoryData(); + +#pragma warning disable CS0618 // Suppressing gRPC obsolete warning + foreach (var useManualExport in BooleanValues) + { + foreach (var forceFlush in BooleanValues) + { + data.Add(OtlpExportProtocol.Grpc, GrpcEndpointHttp, useManualExport, forceFlush, Uri.UriSchemeHttp); + data.Add(OtlpExportProtocol.HttpProtobuf, $"{ProtobufEndpointHttp}metrics", useManualExport, forceFlush, Uri.UriSchemeHttp); + } + } + + data.Add(OtlpExportProtocol.Grpc, GrpcEndpointHttps, true, true, Uri.UriSchemeHttps); + data.Add(OtlpExportProtocol.HttpProtobuf, $"{ProtobufEndpointHttps}metrics", true, true, Uri.UriSchemeHttps); +#pragma warning restore CS0618 // Suppressing gRPC obsolete warning + + return data; } + public static TheoryData LogsTestCases() + { + var data = new TheoryData(); + #pragma warning disable CS0618 // Suppressing gRPC obsolete warning - [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Batch, false)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces", ExportProcessorType.Batch, false)] - [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Batch, true)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces", ExportProcessorType.Batch, true)] - [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Simple, false)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces", ExportProcessorType.Simple, false)] - [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Simple, true)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces", ExportProcessorType.Simple, true)] - [InlineData(OtlpExportProtocol.Grpc, ":5317", ExportProcessorType.Simple, true, "https")] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":5318/v1/traces", ExportProcessorType.Simple, true, "https")] + foreach (var exportType in ExportProcessorTypes) + { + data.Add(OtlpExportProtocol.Grpc, GrpcEndpointHttp, exportType, Uri.UriSchemeHttp); + data.Add(OtlpExportProtocol.HttpProtobuf, $"{ProtobufEndpointHttp}logs", exportType, Uri.UriSchemeHttp); + } + + data.Add(OtlpExportProtocol.Grpc, GrpcEndpointHttps, ExportProcessorType.Simple, Uri.UriSchemeHttps); + data.Add(OtlpExportProtocol.HttpProtobuf, $"{ProtobufEndpointHttps}logs", ExportProcessorType.Simple, Uri.UriSchemeHttps); #pragma warning restore CS0618 // Suppressing gRPC obsolete warning + + return data; + } + + public void Dispose() => this.openTelemetryEventListener.Dispose(); + [Trait("CategoryName", "CollectorIntegrationTests")] [SkipUnlessEnvVarFoundTheory(CollectorHostnameEnvVarName)] - public void TraceExportResultIsSuccess(OtlpExportProtocol protocol, string endpoint, ExportProcessorType exportProcessorType, bool forceFlush, string scheme = "http") + [MemberData(nameof(TraceTestCases))] + public void TraceExportResultIsSuccess( + OtlpExportProtocol protocol, + string endpoint, + ExportProcessorType exportProcessorType, + bool forceFlush, + string scheme) { - using EventWaitHandle handle = new ManualResetEvent(false); + using var handle = new ManualResetEvent(false); + + var exporterOptions = CreateExporterOptions(protocol, scheme, endpoint); - var exporterOptions = new OtlpExporterOptions + exporterOptions.ExportProcessorType = exportProcessorType; + exporterOptions.BatchExportProcessorOptions = new() { - Endpoint = new Uri($"{scheme}://{CollectorHostname}{endpoint}"), - Protocol = protocol, - ExportProcessorType = exportProcessorType, - BatchExportProcessorOptions = new() - { - ScheduledDelayMilliseconds = ExportIntervalMilliseconds, - }, + ScheduledDelayMilliseconds = ExportIntervalMilliseconds, }; DelegatingExporter? delegatingExporter = null; @@ -121,29 +178,19 @@ public void TraceExportResultIsSuccess(OtlpExportProtocol protocol, string endpo } } -#pragma warning disable CS0618 // Suppressing gRPC obsolete warning - [InlineData(OtlpExportProtocol.Grpc, ":4317", false, false)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", false, false)] - [InlineData(OtlpExportProtocol.Grpc, ":4317", false, true)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", false, true)] - [InlineData(OtlpExportProtocol.Grpc, ":4317", true, false)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", true, false)] - [InlineData(OtlpExportProtocol.Grpc, ":4317", true, true)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/metrics", true, true)] - [InlineData(OtlpExportProtocol.Grpc, ":5317", true, true, "https")] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":5318/v1/metrics", true, true, "https")] -#pragma warning restore CS0618 // Suppressing gRPC obsolete warning [Trait("CategoryName", "CollectorIntegrationTests")] [SkipUnlessEnvVarFoundTheory(CollectorHostnameEnvVarName)] - public void MetricExportResultIsSuccess(OtlpExportProtocol protocol, string endpoint, bool useManualExport, bool forceFlush, string scheme = "http") + [MemberData(nameof(MetricsTestCases))] + public void MetricExportResultIsSuccess( + OtlpExportProtocol protocol, + string endpoint, + bool useManualExport, + bool forceFlush, + string scheme) { - using EventWaitHandle handle = new ManualResetEvent(false); + using var handle = new ManualResetEvent(false); - var exporterOptions = new OtlpExporterOptions - { - Endpoint = new Uri($"{scheme}://{CollectorHostname}{endpoint}"), - Protocol = protocol, - }; + var exporterOptions = CreateExporterOptions(protocol, scheme, endpoint); DelegatingExporter? delegatingExporter = null; var exportResults = new List(); @@ -207,25 +254,18 @@ public void MetricExportResultIsSuccess(OtlpExportProtocol protocol, string endp } } -#pragma warning disable CS0618 // Suppressing gRPC obsolete warning - [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Batch)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/logs", ExportProcessorType.Batch)] - [InlineData(OtlpExportProtocol.Grpc, ":4317", ExportProcessorType.Simple)] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/logs", ExportProcessorType.Simple)] - [InlineData(OtlpExportProtocol.Grpc, ":5317", ExportProcessorType.Simple, "https")] - [InlineData(OtlpExportProtocol.HttpProtobuf, ":5318/v1/logs", ExportProcessorType.Simple, "https")] -#pragma warning restore CS0618 // Suppressing gRPC obsolete warning [Trait("CategoryName", "CollectorIntegrationTests")] [SkipUnlessEnvVarFoundTheory(CollectorHostnameEnvVarName)] - public void LogExportResultIsSuccess(OtlpExportProtocol protocol, string endpoint, ExportProcessorType exportProcessorType, string scheme = "http") + [MemberData(nameof(LogsTestCases))] + public void LogExportResultIsSuccess( + OtlpExportProtocol protocol, + string endpoint, + ExportProcessorType exportProcessorType, + string scheme) { - using EventWaitHandle handle = new ManualResetEvent(false); + using var handle = new ManualResetEvent(false); - var exporterOptions = new OtlpExporterOptions - { - Endpoint = new Uri($"{scheme}://{CollectorHostname}{endpoint}"), - Protocol = protocol, - }; + var exporterOptions = CreateExporterOptions(protocol, scheme, endpoint); DelegatingExporter delegatingExporter; var exportResults = new List(); @@ -275,24 +315,26 @@ public void LogExportResultIsSuccess(OtlpExportProtocol protocol, string endpoin Assert.Single(exportResults); Assert.Equal(ExportResult.Success, exportResults[0]); break; + case ExportProcessorType.Simple: Assert.Single(exportResults); Assert.Equal(ExportResult.Success, exportResults[0]); break; + default: throw new NotSupportedException("Unexpected processor type encountered."); } } - private sealed class OpenTelemetryEventListener : EventListener - { - private readonly ITestOutputHelper outputHelper; - - public OpenTelemetryEventListener(ITestOutputHelper outputHelper) + private static OtlpExporterOptions CreateExporterOptions(OtlpExportProtocol protocol, string scheme, string endpoint) => + new() { - this.outputHelper = outputHelper; - } + Endpoint = new($"{scheme}://{CollectorHostname}{endpoint}"), + Protocol = protocol, + }; + private sealed class OpenTelemetryEventListener(ITestOutputHelper outputHelper) : EventListener + { protected override void OnEventSourceCreated(EventSource eventSource) { base.OnEventSourceCreated(eventSource); @@ -305,17 +347,13 @@ protected override void OnEventSourceCreated(EventSource eventSource) protected override void OnEventWritten(EventWrittenEventArgs eventData) { - string? message; - if (eventData.Message != null && eventData.Payload != null && eventData.Payload.Count > 0) - { - message = string.Format(CultureInfo.InvariantCulture, eventData.Message, eventData.Payload.ToArray()); - } - else - { - message = eventData.Message; - } + var message = eventData.Message != null && eventData.Payload?.Count > 0 + ? string.Format(CultureInfo.InvariantCulture, eventData.Message, [.. eventData.Payload]) + : eventData.Message; + + message = string.Format(CultureInfo.InvariantCulture, "[{0}] {1}", eventData.Level, message); - this.outputHelper.WriteLine(message); + outputHelper.WriteLine(message); } } } From 4fe49d67e12254d358243fde1080e343a67c3606 Mon Sep 17 00:00:00 2001 From: martincostello Date: Fri, 3 Oct 2025 10:20:45 +0100 Subject: [PATCH 2/6] [OTLP] Assert no warnings or errors Verify that there are no warnings or errors during the integration tests. --- .../IntegrationTest/IntegrationTests.cs | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs index f91af939b60..335910a0278 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs @@ -176,6 +176,9 @@ public void TraceExportResultIsSuccess( Assert.Single(exportResults); Assert.Equal(ExportResult.Success, exportResults[0]); } + + Assert.Empty(this.openTelemetryEventListener.Errors); + Assert.Empty(this.openTelemetryEventListener.Warnings); } [Trait("CategoryName", "CollectorIntegrationTests")] @@ -198,7 +201,8 @@ public void MetricExportResultIsSuccess( var meterName = "otlp.collector.test"; var builder = Sdk.CreateMeterProviderBuilder() - .AddMeter(meterName); + .AddMeter(meterName) + .AddMeter("System.Net.Http", "System.Net.NameResolution", "System.Runtime"); var readerOptions = new MetricReaderOptions(); readerOptions.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = useManualExport ? Timeout.Infinite : ExportIntervalMilliseconds; @@ -252,6 +256,9 @@ public void MetricExportResultIsSuccess( Assert.Single(exportResults); Assert.Equal(ExportResult.Success, exportResults[0]); } + + Assert.Empty(this.openTelemetryEventListener.Errors); + Assert.Empty(this.openTelemetryEventListener.Warnings); } [Trait("CategoryName", "CollectorIntegrationTests")] @@ -324,6 +331,9 @@ public void LogExportResultIsSuccess( default: throw new NotSupportedException("Unexpected processor type encountered."); } + + Assert.Empty(this.openTelemetryEventListener.Errors); + Assert.Empty(this.openTelemetryEventListener.Warnings); } private static OtlpExporterOptions CreateExporterOptions(OtlpExportProtocol protocol, string scheme, string endpoint) => @@ -335,6 +345,10 @@ private static OtlpExporterOptions CreateExporterOptions(OtlpExportProtocol prot private sealed class OpenTelemetryEventListener(ITestOutputHelper outputHelper) : EventListener { + public List Errors { get; } = []; + + public List Warnings { get; } = []; + protected override void OnEventSourceCreated(EventSource eventSource) { base.OnEventSourceCreated(eventSource); @@ -354,6 +368,15 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) message = string.Format(CultureInfo.InvariantCulture, "[{0}] {1}", eventData.Level, message); outputHelper.WriteLine(message); + + if (eventData.Level == EventLevel.Error) + { + this.Errors.Add(message); + } + else if (eventData.Level == EventLevel.Warning) + { + this.Warnings.Add(message); + } } } } From 6947c4f1062607187e05f1830082ef4e2b986322 Mon Sep 17 00:00:00 2001 From: martincostello Date: Fri, 3 Oct 2025 10:33:41 +0100 Subject: [PATCH 3/6] [OTLP] Refactor duplicated assertions Refactor tests to de-duplicate assertion code. --- .../IntegrationTest/IntegrationTests.cs | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs index 335910a0278..8943d755c60 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs @@ -160,25 +160,28 @@ public void TraceExportResultIsSuccess( if (forceFlush) { Assert.True(tracerProvider.ForceFlush()); - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); + AssertExpectedTraces(); } else if (exporterOptions.ExportProcessorType == ExportProcessorType.Batch) { Assert.True(handle.WaitOne(ExportIntervalMilliseconds * 2)); - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); + AssertExpectedTraces(); } } if (!forceFlush && exportProcessorType == ExportProcessorType.Simple) { - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); + AssertExpectedTraces(); } Assert.Empty(this.openTelemetryEventListener.Errors); Assert.Empty(this.openTelemetryEventListener.Warnings); + + void AssertExpectedTraces() + { + Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, exportResults[0]); + } } [Trait("CategoryName", "CollectorIntegrationTests")] @@ -240,25 +243,28 @@ public void MetricExportResultIsSuccess( if (forceFlush) { Assert.True(meterProvider.ForceFlush()); - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); + AssertExpectedMetrics(); } else if (!useManualExport) { Assert.True(handle.WaitOne(ExportIntervalMilliseconds * 2)); - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); + AssertExpectedMetrics(); } } if (!forceFlush && useManualExport) { - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); + AssertExpectedMetrics(); } Assert.Empty(this.openTelemetryEventListener.Errors); Assert.Empty(this.openTelemetryEventListener.Warnings); + + void AssertExpectedMetrics() + { + Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, exportResults[0]); + } } [Trait("CategoryName", "CollectorIntegrationTests")] @@ -319,19 +325,18 @@ public void LogExportResultIsSuccess( { case ExportProcessorType.Batch: Assert.True(handle.WaitOne(ExportIntervalMilliseconds * 2)); - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); break; case ExportProcessorType.Simple: - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); break; default: throw new NotSupportedException("Unexpected processor type encountered."); } + Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, exportResults[0]); + Assert.Empty(this.openTelemetryEventListener.Errors); Assert.Empty(this.openTelemetryEventListener.Warnings); } From ee87dc21398bea2881d1765afa9d42a2fed53783 Mon Sep 17 00:00:00 2001 From: martincostello Date: Fri, 3 Oct 2025 10:51:29 +0100 Subject: [PATCH 4/6] [OTLP] Assert gauge and histogram - Add a gauge and a histogram to the metrics test. - Rename variable. - Refactor use of `Assert.Single()`. --- .../IntegrationTest/IntegrationTests.cs | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs index 8943d755c60..f4089c99875 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs @@ -111,7 +111,7 @@ public void TraceExportResultIsSuccess( bool forceFlush, string scheme) { - using var handle = new ManualResetEvent(false); + using var exported = new ManualResetEvent(false); var exporterOptions = CreateExporterOptions(protocol, scheme, endpoint); @@ -142,7 +142,7 @@ public void TraceExportResultIsSuccess( { var result = otlpExporter.Export(batch); exportResults.Add(result); - handle.Set(); + exported.Set(); return result; }, }; @@ -164,7 +164,7 @@ public void TraceExportResultIsSuccess( } else if (exporterOptions.ExportProcessorType == ExportProcessorType.Batch) { - Assert.True(handle.WaitOne(ExportIntervalMilliseconds * 2)); + Assert.True(exported.WaitOne(ExportIntervalMilliseconds * 2)); AssertExpectedTraces(); } } @@ -179,8 +179,8 @@ public void TraceExportResultIsSuccess( void AssertExpectedTraces() { - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); + var result = Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, result); } } @@ -194,7 +194,7 @@ public void MetricExportResultIsSuccess( bool forceFlush, string scheme) { - using var handle = new ManualResetEvent(false); + using var exported = new ManualResetEvent(false); var exporterOptions = CreateExporterOptions(protocol, scheme, endpoint); @@ -223,7 +223,7 @@ public void MetricExportResultIsSuccess( { var result = otlpExporter.Export(batch); exportResults.Add(result); - handle.Set(); + exported.Set(); return result; }, }; @@ -235,9 +235,14 @@ public void MetricExportResultIsSuccess( using var meter = new Meter(meterName); var counter = meter.CreateCounter("test_counter"); - counter.Add(18); + var gauge = meter.CreateGauge("test_gauge"); + gauge.Record(42); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(100); + Assert.NotNull(delegatingExporter); if (forceFlush) @@ -247,7 +252,7 @@ public void MetricExportResultIsSuccess( } else if (!useManualExport) { - Assert.True(handle.WaitOne(ExportIntervalMilliseconds * 2)); + Assert.True(exported.WaitOne(ExportIntervalMilliseconds * 2)); AssertExpectedMetrics(); } } @@ -262,8 +267,8 @@ public void MetricExportResultIsSuccess( void AssertExpectedMetrics() { - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); + var result = Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, result); } } @@ -276,7 +281,7 @@ public void LogExportResultIsSuccess( ExportProcessorType exportProcessorType, string scheme) { - using var handle = new ManualResetEvent(false); + using var exported = new ManualResetEvent(false); var exporterOptions = CreateExporterOptions(protocol, scheme, endpoint); @@ -310,7 +315,7 @@ public void LogExportResultIsSuccess( { var result = otlpExporter.Export(batch); exportResults.Add(result); - handle.Set(); + exported.Set(); return result; }, }; @@ -324,7 +329,7 @@ public void LogExportResultIsSuccess( switch (processorOptions.ExportProcessorType) { case ExportProcessorType.Batch: - Assert.True(handle.WaitOne(ExportIntervalMilliseconds * 2)); + Assert.True(exported.WaitOne(ExportIntervalMilliseconds * 2)); break; case ExportProcessorType.Simple: @@ -334,8 +339,8 @@ public void LogExportResultIsSuccess( throw new NotSupportedException("Unexpected processor type encountered."); } - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); + var result = Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, result); Assert.Empty(this.openTelemetryEventListener.Errors); Assert.Empty(this.openTelemetryEventListener.Warnings); From 1a50965eabc7a1824b645b8a10d4af52a1f35cf6 Mon Sep 17 00:00:00 2001 From: martincostello Date: Fri, 3 Oct 2025 11:18:46 +0100 Subject: [PATCH 5/6] [OTLP] Log HTTP response body Log the HTTP response body, if available. Resolves #6454. --- .../ExportClient/OtlpExportClient.cs | 23 +++++++++++++++++++ .../ExportClient/OtlpGrpcExportClient.cs | 11 +++++++-- .../ExportClient/OtlpHttpExportClient.cs | 3 ++- ...penTelemetryProtocolExporterEventSource.cs | 10 ++++---- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs index 24fc1551cc9..ab1a1b98b36 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs @@ -65,6 +65,29 @@ public bool Shutdown(int timeoutMilliseconds) return true; } + protected static string? TryGetResponseBody(HttpResponseMessage? httpResponse, CancellationToken cancellationToken) + { + if (httpResponse?.Content == null) + { + return null; + } + + try + { +#if NET + var stream = httpResponse.Content.ReadAsStream(cancellationToken); + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); +#else + return httpResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult(); +#endif + } + catch (Exception) + { + return null; + } + } + protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength) { var request = new HttpRequestMessage(HttpMethod.Post, this.Endpoint); diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs index dc2d9a133cd..1735d64b98e 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs @@ -36,6 +36,8 @@ public OtlpGrpcExportClient(OtlpExporterOptions options, HttpClient httpClient, /// public override ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default) { + HttpResponseMessage? httpResponse = null; + try { using var httpRequest = this.CreateHttpRequest(buffer, contentLength); @@ -44,7 +46,7 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten // A missing TE header results in servers aborting the gRPC call. httpRequest.Headers.TryAddWithoutValidation("TE", "trailers"); - using var httpResponse = this.SendHttpRequest(httpRequest, cancellationToken); + httpResponse = this.SendHttpRequest(httpRequest, cancellationToken); httpResponse.EnsureSuccessStatusCode(); @@ -121,7 +123,8 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten catch (HttpRequestException ex) { // Handle non-retryable HTTP errors. - OpenTelemetryProtocolExporterEventSource.Log.HttpRequestFailed(this.Endpoint, ex); + var response = TryGetResponseBody(httpResponse, cancellationToken); + OpenTelemetryProtocolExporterEventSource.Log.HttpRequestFailed(this.Endpoint, response, ex); return new ExportClientGrpcResponse( success: false, deadlineUtc: deadlineUtc, @@ -156,6 +159,10 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Endpoint, ex); return DefaultExceptionExportClientGrpcResponse; } + finally + { + httpResponse?.Dispose(); + } } private static bool IsTransientNetworkError(HttpRequestException ex) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs index 351c692a76a..dfff80fffcb 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs @@ -35,7 +35,8 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten } catch (HttpRequestException ex) { - OpenTelemetryProtocolExporterEventSource.Log.HttpRequestFailed(this.Endpoint, ex); + var response = TryGetResponseBody(httpResponse, cancellationToken); + OpenTelemetryProtocolExporterEventSource.Log.HttpRequestFailed(this.Endpoint, response, ex); return new ExportClientHttpResponse(success: false, deadlineUtc: deadlineUtc, response: httpResponse, ex); } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs index 134488bfc1e..e1a406c17f3 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs @@ -60,11 +60,11 @@ public void TransientHttpError(Uri endpoint, Exception ex) } [NonEvent] - public void HttpRequestFailed(Uri endpoint, Exception ex) + public void HttpRequestFailed(Uri endpoint, string? response, Exception ex) { if (Log.IsEnabled(EventLevel.Error, EventKeywords.All)) { - this.HttpRequestFailed(endpoint.ToString(), ex.ToInvariantString()); + this.HttpRequestFailed(endpoint.ToString(), response, ex.ToInvariantString()); } } @@ -200,10 +200,10 @@ public void TransientHttpError(string endpoint, string exceptionMessage) this.WriteEvent(16, endpoint, exceptionMessage); } - [Event(17, Message = "HTTP request to {0} failed. Exception: {1}", Level = EventLevel.Error)] - public void HttpRequestFailed(string endpoint, string exceptionMessage) + [Event(17, Message = "HTTP request to {0} failed. Response: {1}. Exception: {2}", Level = EventLevel.Error)] + public void HttpRequestFailed(string endpoint, string? response, string exceptionMessage) { - this.WriteEvent(17, endpoint, exceptionMessage); + this.WriteEvent(17, endpoint, response, exceptionMessage); } [Event(18, Message = "Operation unexpectedly canceled for endpoint {0}. Exception: {1}", Level = EventLevel.Warning)] From d0e6c360afc7be731789fd6d4d9b73e2b754be34 Mon Sep 17 00:00:00 2001 From: martincostello Date: Fri, 3 Oct 2025 11:57:38 +0100 Subject: [PATCH 6/6] [OLTP] Downgrade OTel Collector to v0.133.0 Downgrade the collector to verify the tests detect the issue. --- .../IntegrationTest/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/docker-compose.yml b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/docker-compose.yml index d6bc9f4aefc..5bc04bd0e47 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/docker-compose.yml +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/docker-compose.yml @@ -19,7 +19,7 @@ services: " otel-collector: - image: otel/opentelemetry-collector:0.136.0@sha256:98fd3b410ae8a939be9588f1580c4b7c3da6ebba49f5363df4259a827aabb779 + image: otel/opentelemetry-collector:0.133.0 volumes: - ./test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest:/cfg command: --config=/cfg/otel-collector-config.yaml