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)] diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTest/IntegrationTests.cs index d5d69cca093..f4089c99875 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() { - this.openTelemetryEventListener.Dispose(); + 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() + { + 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 exported = 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; @@ -85,7 +142,7 @@ public void TraceExportResultIsSuccess(OtlpExportProtocol protocol, string endpo { var result = otlpExporter.Export(batch); exportResults.Add(result); - handle.Set(); + exported.Set(); return result; }, }; @@ -103,47 +160,43 @@ public void TraceExportResultIsSuccess(OtlpExportProtocol protocol, string endpo 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]); + Assert.True(exported.WaitOne(ExportIntervalMilliseconds * 2)); + 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() + { + var result = Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, result); } } -#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 exported = 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(); @@ -151,7 +204,8 @@ public void MetricExportResultIsSuccess(OtlpExportProtocol protocol, string endp 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; @@ -169,7 +223,7 @@ public void MetricExportResultIsSuccess(OtlpExportProtocol protocol, string endp { var result = otlpExporter.Export(batch); exportResults.Add(result); - handle.Set(); + exported.Set(); return result; }, }; @@ -181,51 +235,55 @@ public void MetricExportResultIsSuccess(OtlpExportProtocol protocol, string endp 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) { 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]); + Assert.True(exported.WaitOne(ExportIntervalMilliseconds * 2)); + 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() + { + var result = Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, result); } } -#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 exported = 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(); @@ -257,7 +315,7 @@ public void LogExportResultIsSuccess(OtlpExportProtocol protocol, string endpoin { var result = otlpExporter.Export(batch); exportResults.Add(result); - handle.Set(); + exported.Set(); return result; }, }; @@ -271,27 +329,35 @@ public void LogExportResultIsSuccess(OtlpExportProtocol protocol, string endpoin switch (processorOptions.ExportProcessorType) { case ExportProcessorType.Batch: - Assert.True(handle.WaitOne(ExportIntervalMilliseconds * 2)); - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); + Assert.True(exported.WaitOne(ExportIntervalMilliseconds * 2)); break; + case ExportProcessorType.Simple: - Assert.Single(exportResults); - Assert.Equal(ExportResult.Success, exportResults[0]); break; + default: throw new NotSupportedException("Unexpected processor type encountered."); } + + var result = Assert.Single(exportResults); + Assert.Equal(ExportResult.Success, result); + + Assert.Empty(this.openTelemetryEventListener.Errors); + Assert.Empty(this.openTelemetryEventListener.Warnings); } - private sealed class OpenTelemetryEventListener : EventListener + private static OtlpExporterOptions CreateExporterOptions(OtlpExportProtocol protocol, string scheme, string endpoint) => + new() + { + Endpoint = new($"{scheme}://{CollectorHostname}{endpoint}"), + Protocol = protocol, + }; + + private sealed class OpenTelemetryEventListener(ITestOutputHelper outputHelper) : EventListener { - private readonly ITestOutputHelper outputHelper; + public List Errors { get; } = []; - public OpenTelemetryEventListener(ITestOutputHelper outputHelper) - { - this.outputHelper = outputHelper; - } + public List Warnings { get; } = []; protected override void OnEventSourceCreated(EventSource eventSource) { @@ -305,17 +371,22 @@ 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) + 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); + + outputHelper.WriteLine(message); + + if (eventData.Level == EventLevel.Error) { - message = string.Format(CultureInfo.InvariantCulture, eventData.Message, eventData.Payload.ToArray()); + this.Errors.Add(message); } - else + else if (eventData.Level == EventLevel.Warning) { - message = eventData.Message; + this.Warnings.Add(message); } - - this.outputHelper.WriteLine(message); } } }