From 5c729a287f80d86a4b5aefc4863ce9bd01061ff8 Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Fri, 5 Sep 2025 22:46:57 -0400 Subject: [PATCH 01/20] implement gzip compression in exporter --- .../ExportClient/OtlpExportClient.cs | 26 ++++++++++++--- .../ExportClient/OtlpGrpcExportClient.cs | 27 ++++++++++++++++ .../ExportClient/OtlpHttpExportClient.cs | 18 +++++++++++ .../OtlpSpecConfigDefinitions.cs | 4 +++ .../OtlpExporterOptions.cs | 32 ++++++++++++++++--- 5 files changed, 97 insertions(+), 10 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs index 24fc1551cc9..2e1127aaa73 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs @@ -44,10 +44,13 @@ protected OtlpExportClient(OtlpExporterOptions options, HttpClient httpClient, s this.Endpoint = new UriBuilder(exporterEndpoint).Uri; this.Headers = options.GetHeaders>((d, k, v) => d.Add(k, v)); this.HttpClient = httpClient; + this.CompressionEnabled = options.CompressPayload; } internal HttpClient HttpClient { get; } + internal bool CompressionEnabled { get; } + internal Uri Endpoint { get; } internal IReadOnlyDictionary Headers { get; } @@ -56,6 +59,8 @@ protected OtlpExportClient(OtlpExporterOptions options, HttpClient httpClient, s internal virtual bool RequireHttp2 => false; + protected abstract string? ContentEncodingHeader { get; } + public abstract ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default); /// @@ -70,27 +75,38 @@ protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength) var request = new HttpRequestMessage(HttpMethod.Post, this.Endpoint); if (this.RequireHttp2) - { - request.Version = Http2RequestVersion; + { + request.Version = Http2RequestVersion; #if NET6_0_OR_GREATER request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; #endif - } + } foreach (var header in this.Headers) { request.Headers.Add(header.Key, header.Value); } - // TODO: Support compression. + var data = buffer; - request.Content = new ByteArrayContent(buffer, 0, contentLength); + if (this.CompressionEnabled) + { + data = this.Compress(buffer, contentLength); + if (this.ContentEncodingHeader != null) + { + request.Headers.Add("Content-Encoding", this.ContentEncodingHeader); + } + } + + request.Content = new ByteArrayContent(data, 0, data.Length); request.Content.Headers.ContentType = this.MediaTypeHeader; return request; } + protected abstract byte[] Compress(byte[] data, int contentLength); + protected HttpResponseMessage SendHttpRequest(HttpRequestMessage request, CancellationToken cancellationToken) { #if NET diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs index 31b7d0e9bb2..60191dfd3a6 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs @@ -4,6 +4,8 @@ #if NETFRAMEWORK using System.Net.Http; #endif +using System.Buffers.Binary; +using System.IO.Compression; using System.Net.Http.Headers; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc; @@ -33,6 +35,8 @@ public OtlpGrpcExportClient(OtlpExporterOptions options, HttpClient httpClient, internal override bool RequireHttp2 => true; + protected override string? ContentEncodingHeader => null; + /// public override ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default) { @@ -158,6 +162,29 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten } } + protected override byte[] Compress(byte[] data, int contentLength) + { + using var compressedStream = new MemoryStream(); + using (var gzipStream = new GZipStream(compressedStream, CompressionLevel.Optimal)) + { +#if NET462 || NETSTANDARD2_0 + gzipStream.Write(data.ToArray(), 0, data.Length); +#else + gzipStream.Write(data); +#endif + } + + var compressedDataLength = compressedStream.Position; + var payload = new byte[compressedDataLength + 5]; + using var payloadStream = new MemoryStream(payload); + compressedStream.WriteTo(payloadStream); + + payload[0] = 1; + BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, 4), (uint)compressedDataLength); + + return payload; + } + private static bool IsTransientNetworkError(HttpRequestException ex) { return ex.InnerException is System.Net.Sockets.SocketException socketEx diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs index 0d938a90f89..d4428001471 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs @@ -4,6 +4,7 @@ #if NETFRAMEWORK using System.Net.Http; #endif +using System.IO.Compression; using System.Net.Http.Headers; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; @@ -19,6 +20,8 @@ internal OtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient { } + protected override string? ContentEncodingHeader => "gzip"; + internal override MediaTypeHeaderValue MediaTypeHeader => MediaHeaderValue; /// @@ -47,4 +50,19 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten return new ExportClientHttpResponse(success: false, deadlineUtc: deadlineUtc, response: null, exception: ex); } } + + protected override byte[] Compress(byte[] data, int contentLength) + { + using var compressedStream = new MemoryStream(); + using (var gzipStream = new GZipStream(compressedStream, CompressionLevel.Optimal)) + { +#if NET462 || NETSTANDARD2_0 + gzipStream.Write(data.ToArray(), 0, data.Length); +#else + gzipStream.Write(data); +#endif + } + + return compressedStream.ToArray(); + } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs index 3bc62218b3f..cf4ab6ecd6f 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OtlpSpecConfigDefinitions.cs @@ -15,20 +15,24 @@ internal static class OtlpSpecConfigDefinitions public const string DefaultHeadersEnvVarName = "OTEL_EXPORTER_OTLP_HEADERS"; public const string DefaultTimeoutEnvVarName = "OTEL_EXPORTER_OTLP_TIMEOUT"; public const string DefaultProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL"; + public const string DefaultCompressionEnvVarName = "OTEL_EXPORTER_OTLP_COMPRESSION"; public const string LogsEndpointEnvVarName = "OTEL_EXPORTER_OTLP_LOGS_ENDPOINT"; public const string LogsHeadersEnvVarName = "OTEL_EXPORTER_OTLP_LOGS_HEADERS"; public const string LogsTimeoutEnvVarName = "OTEL_EXPORTER_OTLP_LOGS_TIMEOUT"; public const string LogsProtocolEnvVarName = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"; + public const string LogsCompressionEnvVarName = "OTEL_EXPORTER_OTLP_LOGS_COMPRESSION"; public const string MetricsEndpointEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT"; public const string MetricsHeadersEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_HEADERS"; public const string MetricsTimeoutEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_TIMEOUT"; public const string MetricsProtocolEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL"; public const string MetricsTemporalityPreferenceEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE"; + public const string MetricsCompressionEnvVarName = "OTEL_EXPORTER_OTLP_METRICS_COMPRESSION"; public const string TracesEndpointEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"; public const string TracesHeadersEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_HEADERS"; public const string TracesTimeoutEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_TIMEOUT"; public const string TracesProtocolEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"; + public const string TracesCompressionEnvVarName = "OTEL_EXPORTER_OTLP_TRACES_COMPRESSION"; } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 91ebfdbd3e1..728953b92ee 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; +using System.Globalization; + #if NETFRAMEWORK using System.Net.Http; #endif @@ -136,6 +138,9 @@ public OtlpExportProtocol Protocol /// Note: This only applies when exporting traces. public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } + /// + public bool CompressPayload { get; set; } + /// public Func HttpClientFactory { @@ -179,7 +184,8 @@ internal void ApplyConfigurationUsingSpecificationEnvVars( bool appendSignalPathToEndpoint, string protocolEnvVarKey, string headersEnvVarKey, - string timeoutEnvVarKey) + string timeoutEnvVarKey, + string compressPayloadEnvVarKey) { if (configuration.TryGetUriValue(OpenTelemetryProtocolExporterEventSource.Log, endpointEnvVarKey, out var endpoint)) { @@ -205,6 +211,18 @@ internal void ApplyConfigurationUsingSpecificationEnvVars( { this.TimeoutMilliseconds = timeout; } + + if (configuration.TryGetStringValue(compressPayloadEnvVarKey, out var compressPayload)) + { + if (string.Equals(compressPayload.Trim(), "gzip", StringComparison.OrdinalIgnoreCase)) + { + this.CompressPayload = true; + } + else if (!string.Equals(compressPayload.Trim(), "none", StringComparison.OrdinalIgnoreCase)) + { + OpenTelemetryProtocolExporterEventSource.Log.InvalidConfigurationValue(compressPayloadEnvVarKey, compressPayload); + } + } } internal OtlpExporterOptions ApplyDefaults(OtlpExporterOptions defaultExporterOptions) @@ -250,7 +268,8 @@ private void ApplyConfiguration( appendSignalPathToEndpoint: true, OtlpSpecConfigDefinitions.DefaultProtocolEnvVarName, OtlpSpecConfigDefinitions.DefaultHeadersEnvVarName, - OtlpSpecConfigDefinitions.DefaultTimeoutEnvVarName); + OtlpSpecConfigDefinitions.DefaultTimeoutEnvVarName, + OtlpSpecConfigDefinitions.DefaultCompressionEnvVarName); } else if (configurationType == OtlpExporterOptionsConfigurationType.Logs) { @@ -260,7 +279,8 @@ private void ApplyConfiguration( appendSignalPathToEndpoint: false, OtlpSpecConfigDefinitions.LogsProtocolEnvVarName, OtlpSpecConfigDefinitions.LogsHeadersEnvVarName, - OtlpSpecConfigDefinitions.LogsTimeoutEnvVarName); + OtlpSpecConfigDefinitions.LogsTimeoutEnvVarName, + OtlpSpecConfigDefinitions.LogsCompressionEnvVarName); } else if (configurationType == OtlpExporterOptionsConfigurationType.Metrics) { @@ -270,7 +290,8 @@ private void ApplyConfiguration( appendSignalPathToEndpoint: false, OtlpSpecConfigDefinitions.MetricsProtocolEnvVarName, OtlpSpecConfigDefinitions.MetricsHeadersEnvVarName, - OtlpSpecConfigDefinitions.MetricsTimeoutEnvVarName); + OtlpSpecConfigDefinitions.MetricsTimeoutEnvVarName, + OtlpSpecConfigDefinitions.MetricsCompressionEnvVarName); } else if (configurationType == OtlpExporterOptionsConfigurationType.Traces) { @@ -280,7 +301,8 @@ private void ApplyConfiguration( appendSignalPathToEndpoint: false, OtlpSpecConfigDefinitions.TracesProtocolEnvVarName, OtlpSpecConfigDefinitions.TracesHeadersEnvVarName, - OtlpSpecConfigDefinitions.TracesTimeoutEnvVarName); + OtlpSpecConfigDefinitions.TracesTimeoutEnvVarName, + OtlpSpecConfigDefinitions.TracesCompressionEnvVarName); } else { From cc0757ddc4c3f7714473f6e49dd0dcbec8d1d340 Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Fri, 5 Sep 2025 22:48:57 -0400 Subject: [PATCH 02/20] add missing property in IOtlpExporterOptions --- .../IOtlpExporterOptions.cs | 9 +++++++++ .../Implementation/ExportClient/OtlpHttpExportClient.cs | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs index d6402bc85a8..c33f1afbbd0 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs @@ -69,6 +69,15 @@ internal interface IOtlpExporterOptions /// int TimeoutMilliseconds { get; set; } + /// + /// Gets or sets a value indicating whether to compress the payload. + /// Currently gzip is the only supported compression method. + /// Note: Refer to the + /// OpenTelemetry Specification for details />. + /// + bool CompressPayload { get; set; } + /// /// Gets or sets the factory function called to create the instance that will be used at runtime to diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs index d4428001471..39b1e75ce42 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs @@ -20,10 +20,10 @@ internal OtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient { } - protected override string? ContentEncodingHeader => "gzip"; - internal override MediaTypeHeaderValue MediaTypeHeader => MediaHeaderValue; + protected override string? ContentEncodingHeader => "gzip"; + /// public override ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default) { From 2efddd6a81429a8b9893e41bd46550bb0a00493d Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Tue, 9 Sep 2025 10:58:59 -0400 Subject: [PATCH 03/20] fix failed tests --- .../Implementation/ExportClient/OtlpExportClient.cs | 6 +++--- .../OtlpExporterOptions.cs | 1 - .../OtlpExporterOptionsTests.cs | 11 +++++++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs index 2e1127aaa73..e3d87b3f5b3 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs @@ -75,13 +75,13 @@ protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength) var request = new HttpRequestMessage(HttpMethod.Post, this.Endpoint); if (this.RequireHttp2) - { - request.Version = Http2RequestVersion; + { + request.Version = Http2RequestVersion; #if NET6_0_OR_GREATER request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; #endif - } + } foreach (var header in this.Headers) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 728953b92ee..847ff4f4d85 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 using System.Diagnostics; -using System.Globalization; #if NETFRAMEWORK using System.Net.Http; diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index 9536d283cf5..001df8f060f 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -100,6 +100,7 @@ public void OtlpExporterOptions_InvalidEnvironmentVariableOverride() ["EndpointWithInvalidValue"] = "invalid", ["TimeoutWithInvalidValue"] = "invalid", ["ProtocolWithInvalidValue"] = "invalid", + ["CompressionWithInvalidValue"] = "invalid", }; var configuration = new ConfigurationBuilder() @@ -114,7 +115,8 @@ public void OtlpExporterOptions_InvalidEnvironmentVariableOverride() appendSignalPathToEndpoint: true, "ProtocolWithInvalidValue", "NoopHeaders", - "TimeoutWithInvalidValue"); + "TimeoutWithInvalidValue", + "CompressionWithInvalidValue"); #if NET462_OR_GREATER || NETSTANDARD2_0 Assert.Equal(new Uri(OtlpExporterOptions.DefaultHttpEndpoint), options.Endpoint); @@ -125,6 +127,7 @@ public void OtlpExporterOptions_InvalidEnvironmentVariableOverride() Assert.Equal(10000, options.TimeoutMilliseconds); Assert.Equal(OtlpExporterOptions.DefaultOtlpExportProtocol, options.Protocol); Assert.Null(options.Headers); + Assert.False(options.CompressPayload); } [Fact] @@ -136,6 +139,7 @@ public void OtlpExporterOptions_SetterOverridesEnvironmentVariable() ["Timeout"] = "2000", ["Protocol"] = "grpc", ["Headers"] = "A=2,B=3", + ["Compression"] = "none", }; var configuration = new ConfigurationBuilder() @@ -150,18 +154,21 @@ public void OtlpExporterOptions_SetterOverridesEnvironmentVariable() appendSignalPathToEndpoint: true, "Protocol", "Headers", - "Timeout"); + "Timeout", + "Compression"); options.Endpoint = new Uri("http://localhost:200"); options.Headers = "C=3"; options.TimeoutMilliseconds = 40000; options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.CompressPayload = true; Assert.Equal(new Uri("http://localhost:200"), options.Endpoint); Assert.Equal("C=3", options.Headers); Assert.Equal(40000, options.TimeoutMilliseconds); Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); Assert.False(options.AppendSignalPathToEndpoint); + Assert.True(options.CompressPayload); } [Fact] From 56fd60a718ba355234ee1370926d34484feb1ce0 Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Wed, 10 Sep 2025 22:36:28 -0400 Subject: [PATCH 04/20] added tests for compression; moved grpc header from exporters into export client --- .../ExportClient/OtlpExportClient.cs | 9 +- .../ExportClient/OtlpGrpcExportClient.cs | 40 ++++-- .../OtlpLogExporter.cs | 12 -- .../OtlpMetricExporter.cs | 12 -- .../OtlpTraceExporter.cs | 12 -- .../OtlpExporterCompressionTests.cs | 136 ++++++++++++++++++ .../TestGrpcMessageHandler.cs | 66 +++++++++ 7 files changed, 239 insertions(+), 48 deletions(-) create mode 100644 test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs create mode 100644 test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestGrpcMessageHandler.cs diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs index e3d87b3f5b3..7da8855a8e4 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs @@ -93,15 +93,16 @@ protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength) if (this.CompressionEnabled) { data = this.Compress(buffer, contentLength); - if (this.ContentEncodingHeader != null) - { - request.Headers.Add("Content-Encoding", this.ContentEncodingHeader); - } } request.Content = new ByteArrayContent(data, 0, data.Length); request.Content.Headers.ContentType = this.MediaTypeHeader; + if (this.CompressionEnabled && this.ContentEncodingHeader != null) + { + request.Content.Headers.Add("Content-Encoding", this.ContentEncodingHeader); + } + return request; } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs index 60191dfd3a6..2df41d785cc 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs @@ -15,6 +15,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClie internal sealed class OtlpGrpcExportClient : OtlpExportClient { public const string GrpcStatusDetailsHeader = "grpc-status-details-bin"; + private const int GrpcMessageHeaderSize = 5; private static readonly ExportClientHttpResponse SuccessExportResponse = new(success: true, deadlineUtc: default, response: null, exception: null); private static readonly MediaTypeHeaderValue MediaHeaderValue = new("application/grpc"); @@ -42,6 +43,15 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten { try { + // A gRPC message consists of 3 parts: + // byte 0 - Compression flag (0 = not compressed, 1 = compressed). + // bytes 1-4 - Message length in big-endian format (length of the serialized data only). + // bytes 5+ - Protobuf-encoded payload. + Span data = new Span(buffer, 1, 4); + var dataLength = buffer.Length - GrpcMessageHeaderSize; + BinaryPrimitives.WriteUInt32BigEndian(data, (uint)dataLength); + buffer[0] = this.CompressionEnabled ? (byte)1 : (byte)0; + using var httpRequest = this.CreateHttpRequest(buffer, contentLength); // TE is required by some servers, e.g. C Core. @@ -165,25 +175,39 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten protected override byte[] Compress(byte[] data, int contentLength) { using var compressedStream = new MemoryStream(); - using (var gzipStream = new GZipStream(compressedStream, CompressionLevel.Optimal)) + using (var gzipStream = new GZipStream(compressedStream, CompressionLevel.Optimal, leaveOpen: true)) { -#if NET462 || NETSTANDARD2_0 - gzipStream.Write(data.ToArray(), 0, data.Length); -#else - gzipStream.Write(data); -#endif + gzipStream.Write(data, GrpcMessageHeaderSize, data.Length - GrpcMessageHeaderSize); +// #if NET462 || NETSTANDARD2_0 + // gzipStream.Write(data, 5, data.Length - 5); + // #else + // gzipStream.Write(data, 5, data.Length - 5); + // #endif } var compressedDataLength = compressedStream.Position; - var payload = new byte[compressedDataLength + 5]; + var payload = new byte[compressedDataLength + GrpcMessageHeaderSize]; using var payloadStream = new MemoryStream(payload); + payloadStream.Position = GrpcMessageHeaderSize; compressedStream.WriteTo(payloadStream); payload[0] = 1; BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, 4), (uint)compressedDataLength); return payload; - } + + // var compressedData = compressedStream.ToArray(); + // var compressedDataLength = compressedData.Length; + + // // Allocate gRPC frame: 1 flag + 4 length prefix + body + // var payload = new byte[compressedDataLength + 5]; + // payload[0] = 1; + // BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, 4), (uint)compressedDataLength); + + // Buffer.BlockCopy(compressedData, 0, payload, 5, compressedDataLength); + + // return payload; + } private static bool IsTransientNetworkError(HttpRequestException ex) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs index dbec08c771b..ec95d6727e1 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs @@ -1,7 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Buffers.Binary; using System.Diagnostics; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; @@ -78,17 +77,6 @@ public override ExportResult Export(in Batch logRecordBatch) { int writePosition = ProtobufOtlpLogSerializer.WriteLogsData(ref this.buffer, this.startWritePosition, this.sdkLimitOptions, this.experimentalOptions, this.Resource, logRecordBatch); - if (this.startWritePosition == GrpcStartWritePosition) - { - // Grpc payload consists of 3 parts - // byte 0 - Specifying if the payload is compressed. - // 1-4 byte - Specifies the length of payload in big endian format. - // 5 and above - Protobuf serialized data. - Span data = new Span(this.buffer, 1, 4); - var dataLength = writePosition - GrpcStartWritePosition; - BinaryPrimitives.WriteUInt32BigEndian(data, (uint)dataLength); - } - if (!this.transmissionHandler.TrySubmitRequest(this.buffer, writePosition)) { return ExportResult.Failure; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs index 1ff7dcc5ae5..ac95ca49934 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs @@ -1,7 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Buffers.Binary; using System.Diagnostics; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; @@ -71,17 +70,6 @@ public override ExportResult Export(in Batch metrics) { int writePosition = ProtobufOtlpMetricSerializer.WriteMetricsData(ref this.buffer, this.startWritePosition, this.Resource, metrics); - if (this.startWritePosition == GrpcStartWritePosition) - { - // Grpc payload consists of 3 parts - // byte 0 - Specifying if the payload is compressed. - // 1-4 byte - Specifies the length of payload in big endian format. - // 5 and above - Protobuf serialized data. - Span data = new Span(this.buffer, 1, 4); - var dataLength = writePosition - GrpcStartWritePosition; - BinaryPrimitives.WriteUInt32BigEndian(data, (uint)dataLength); - } - if (!this.transmissionHandler.TrySubmitRequest(this.buffer, writePosition)) { return ExportResult.Failure; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs index 6f10f51e7b2..92dc0f79bf9 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs @@ -1,7 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Buffers.Binary; using System.Diagnostics; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; @@ -74,17 +73,6 @@ public override ExportResult Export(in Batch activityBatch) { int writePosition = ProtobufOtlpTraceSerializer.WriteTraceData(ref this.buffer, this.startWritePosition, this.sdkLimitOptions, this.Resource, activityBatch); - if (this.startWritePosition == GrpcStartWritePosition) - { - // Grpc payload consists of 3 parts - // byte 0 - Specifying if the payload is compressed. - // 1-4 byte - Specifies the length of payload in big endian format. - // 5 and above - Protobuf serialized data. - Span data = new Span(this.buffer, 1, 4); - var dataLength = writePosition - GrpcStartWritePosition; - BinaryPrimitives.WriteUInt32BigEndian(data, (uint)dataLength); - } - if (!this.transmissionHandler.TrySubmitRequest(this.buffer, writePosition)) { return ExportResult.Failure; diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs new file mode 100644 index 00000000000..e0c12635144 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs @@ -0,0 +1,136 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +using System.IO.Compression; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using Xunit; +using System.Buffers.Binary; + +#if !NET +using System.Net.Http; +#endif + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.Implementation.ExportClient; + +public class OtlpExporterCompressionTests +{ + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SendExportRequest_SendsCorrectContent_Http(bool compressPayload) + { + // Arrange + var options = new OtlpExporterOptions + { + Endpoint = new Uri("http://localhost:4317"), + CompressPayload = compressPayload, + }; + + using var testHttpHandler = new TestHttpMessageHandler(); + using var httpClient = new HttpClient(testHttpHandler, false); + var exportClient = new OtlpHttpExportClient(options, httpClient, string.Empty); + + var buffer = Enumerable.Repeat((byte)65, 1000).ToArray(); + var deadlineUtc = DateTime.UtcNow.AddMilliseconds(httpClient.Timeout.TotalMilliseconds); + + // Act + var result = exportClient.SendExportRequest(buffer, buffer.Length, deadlineUtc); + var httpRequest = testHttpHandler.HttpRequestMessage; + + // Assert + Assert.True(result.Success); + Assert.NotNull(httpRequest); + Assert.Equal(HttpMethod.Post, httpRequest.Method); + Assert.NotNull(httpRequest.Content); + + if (compressPayload) + { + Assert.Contains(httpRequest.Content.Headers, h => h.Key == "Content-Encoding" && h.Value.First() == "gzip"); + + var content = testHttpHandler.HttpRequestContent; + Assert.NotNull(content); + // using var compressedStream = new MemoryStream(content); + // using var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress); + // using var decompressedStream = new MemoryStream(); + // gzipStream.CopyTo(decompressedStream); + var decompressedStream = Decompress(content); + + Assert.Equal(buffer, decompressedStream.ToArray()); + } + else + { + Assert.DoesNotContain(httpRequest.Content.Headers, h => h.Key == "Content-Encoding"); + Assert.Equal(buffer, testHttpHandler.HttpRequestContent); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SendExportRequest_SendsCorrectContent_Grpc(bool compressPayload) + { + // Arrange + var options = new OtlpExporterOptions + { + Endpoint = new Uri("http://localhost:4317"), + CompressPayload = compressPayload, + }; + + // var originalPayload = Enumerable.Repeat((byte)65, 1000).ToArray(); + // var buffer = new byte[originalPayload.Length + 5]; + // Buffer.BlockCopy(originalPayload, 0, buffer, 5, originalPayload.Length); + // buffer[0] = compressPayload ? (byte)1 : (byte)0; + // BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(1, 4), (uint)(buffer.Length - 5)); + var payload = Enumerable.Repeat((byte)65, 1000).ToArray(); + var buffer = new byte[payload.Length + 5]; + Buffer.BlockCopy(payload, 0, buffer, 5, payload.Length); + + using var testGrpcHandler = new TestGrpcMessageHandler(); + using var httpClient = new HttpClient(testGrpcHandler, false); + var exportClient = new OtlpGrpcExportClient(options, httpClient, string.Empty); + + var deadlineUtc = DateTime.UtcNow.AddMilliseconds(httpClient.Timeout.TotalMilliseconds); + + // Act + var result = exportClient.SendExportRequest(buffer, buffer.Length, deadlineUtc); + var httpRequest = testGrpcHandler.HttpRequestMessage; + var requestContent = testGrpcHandler.HttpRequestContent; + + // Assert + Assert.True(result.Success); + Assert.NotNull(httpRequest); + Assert.NotNull(requestContent); + Assert.True(requestContent.Length > 5); // gRPC frame must be at least 5 bytes + + var compressionFlag = requestContent[0]; + var declaredLength = BinaryPrimitives.ReadUInt32BigEndian(requestContent.AsSpan(1, 4)); + var body = requestContent.AsSpan(5, (int)declaredLength).ToArray(); + + Assert.Equal(compressPayload ? 1 : 0, compressionFlag); + Assert.Equal(body.Length, (int)declaredLength); + + if (compressPayload) + { + // using var bodyStream = new MemoryStream(body); + // using var gzipStream = new GZipStream(bodyStream, CompressionMode.Decompress); + // using var decompressedStream = new MemoryStream(); + // gzipStream.CopyTo(decompressedStream); + var decompressedStream = Decompress(body); + + Assert.Equal(payload, decompressedStream.ToArray()); + } + else + { + Assert.Equal(payload, body); + } + } + + private static byte[] Decompress(byte[] data) + { + using var compressedStream = new MemoryStream(data); + using var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress); + using var decompressedStream = new MemoryStream(); + gzipStream.CopyTo(decompressedStream); + return decompressedStream.ToArray(); + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestGrpcMessageHandler.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestGrpcMessageHandler.cs new file mode 100644 index 00000000000..b3d72e64847 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestGrpcMessageHandler.cs @@ -0,0 +1,66 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if !NET +using System.Net.Http; +#endif + +using System.Net.Http.Headers; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; + +internal class TestGrpcMessageHandler : HttpMessageHandler +{ + public HttpRequestMessage? HttpRequestMessage { get; private set; } + + public byte[]? HttpRequestContent { get; private set; } + + public HttpResponseMessage InternalSend(HttpRequestMessage request, CancellationToken cancellationToken) + { + this.HttpRequestMessage = request; +#if NET + this.HttpRequestContent = request.Content!.ReadAsByteArrayAsync(cancellationToken).Result; +#else + this.HttpRequestContent = request.Content!.ReadAsByteArrayAsync().Result; +#endif + var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + RequestMessage = request, + }; + +#if NETSTANDARD2_0 || NET462 + const string ResponseTrailersKey = "__ResponseTrailers"; + + if (!response.RequestMessage.Properties.TryGetValue(ResponseTrailersKey, out var value)) + { + value = new CustomResponseTrailers(); + response.RequestMessage.Properties[ResponseTrailersKey] = value; + } + + var trailers = (HttpHeaders)value; + trailers.Add("grpc-status", "0"); +#else + response.TrailingHeaders.Add("grpc-status", "0"); +#endif + + return response; + } + +#if NET + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + return this.InternalSend(request, cancellationToken); + } +#endif + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + return Task.FromResult(this.InternalSend(request, cancellationToken)); + } + +#if NETSTANDARD2_0 || NET462 + private class CustomResponseTrailers : HttpHeaders + { + } +#endif +} From 9cd706b19c98f1471d8de57031abc73aec6b984f Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Wed, 10 Sep 2025 22:57:21 -0400 Subject: [PATCH 05/20] removed comments --- .../ExportClient/OtlpGrpcExportClient.cs | 17 ----------------- .../OtlpExporterCompressionTests.cs | 15 +-------------- .../TestGrpcMessageHandler.cs | 7 +++---- 3 files changed, 4 insertions(+), 35 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs index 2df41d785cc..32dce4a602b 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs @@ -178,11 +178,6 @@ protected override byte[] Compress(byte[] data, int contentLength) using (var gzipStream = new GZipStream(compressedStream, CompressionLevel.Optimal, leaveOpen: true)) { gzipStream.Write(data, GrpcMessageHeaderSize, data.Length - GrpcMessageHeaderSize); -// #if NET462 || NETSTANDARD2_0 - // gzipStream.Write(data, 5, data.Length - 5); - // #else - // gzipStream.Write(data, 5, data.Length - 5); - // #endif } var compressedDataLength = compressedStream.Position; @@ -195,18 +190,6 @@ protected override byte[] Compress(byte[] data, int contentLength) BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, 4), (uint)compressedDataLength); return payload; - - // var compressedData = compressedStream.ToArray(); - // var compressedDataLength = compressedData.Length; - - // // Allocate gRPC frame: 1 flag + 4 length prefix + body - // var payload = new byte[compressedDataLength + 5]; - // payload[0] = 1; - // BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, 4), (uint)compressedDataLength); - - // Buffer.BlockCopy(compressedData, 0, payload, 5, compressedDataLength); - - // return payload; } private static bool IsTransientNetworkError(HttpRequestException ex) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs index e0c12635144..4d15d204f84 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs @@ -1,10 +1,10 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Buffers.Binary; using System.IO.Compression; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; using Xunit; -using System.Buffers.Binary; #if !NET using System.Net.Http; @@ -49,10 +49,6 @@ public void SendExportRequest_SendsCorrectContent_Http(bool compressPayload) var content = testHttpHandler.HttpRequestContent; Assert.NotNull(content); - // using var compressedStream = new MemoryStream(content); - // using var gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress); - // using var decompressedStream = new MemoryStream(); - // gzipStream.CopyTo(decompressedStream); var decompressedStream = Decompress(content); Assert.Equal(buffer, decompressedStream.ToArray()); @@ -76,11 +72,6 @@ public void SendExportRequest_SendsCorrectContent_Grpc(bool compressPayload) CompressPayload = compressPayload, }; - // var originalPayload = Enumerable.Repeat((byte)65, 1000).ToArray(); - // var buffer = new byte[originalPayload.Length + 5]; - // Buffer.BlockCopy(originalPayload, 0, buffer, 5, originalPayload.Length); - // buffer[0] = compressPayload ? (byte)1 : (byte)0; - // BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(1, 4), (uint)(buffer.Length - 5)); var payload = Enumerable.Repeat((byte)65, 1000).ToArray(); var buffer = new byte[payload.Length + 5]; Buffer.BlockCopy(payload, 0, buffer, 5, payload.Length); @@ -111,10 +102,6 @@ public void SendExportRequest_SendsCorrectContent_Grpc(bool compressPayload) if (compressPayload) { - // using var bodyStream = new MemoryStream(body); - // using var gzipStream = new GZipStream(bodyStream, CompressionMode.Decompress); - // using var decompressedStream = new MemoryStream(); - // gzipStream.CopyTo(decompressedStream); var decompressedStream = Decompress(body); Assert.Equal(payload, decompressedStream.ToArray()); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestGrpcMessageHandler.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestGrpcMessageHandler.cs index b3d72e64847..ef9db42790c 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestGrpcMessageHandler.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestGrpcMessageHandler.cs @@ -3,13 +3,12 @@ #if !NET using System.Net.Http; -#endif - using System.Net.Http.Headers; +#endif namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests; -internal class TestGrpcMessageHandler : HttpMessageHandler +internal sealed class TestGrpcMessageHandler : HttpMessageHandler { public HttpRequestMessage? HttpRequestMessage { get; private set; } @@ -59,7 +58,7 @@ protected override Task SendAsync(HttpRequestMessage reques } #if NETSTANDARD2_0 || NET462 - private class CustomResponseTrailers : HttpHeaders + private sealed class CustomResponseTrailers : HttpHeaders { } #endif From b7a5d6112024e24201e485672cff8d62512ceff1 Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Thu, 11 Sep 2025 12:06:58 -0400 Subject: [PATCH 06/20] add test --- .../OtlpExporterCompressionTests.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs index 4d15d204f84..694799f9703 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs @@ -15,10 +15,13 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.Implementation.Expo public class OtlpExporterCompressionTests { [Theory] - [InlineData(true)] - [InlineData(false)] - public void SendExportRequest_SendsCorrectContent_Http(bool compressPayload) + [InlineData(true, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] + [InlineData(false, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] + [InlineData(true, "")] + public void SendExportRequest_SendsCorrectContent_Http(bool compressPayload, string text) { + var buffer = System.Text.Encoding.UTF8.GetBytes(text); + // Arrange var options = new OtlpExporterOptions { @@ -30,7 +33,6 @@ public void SendExportRequest_SendsCorrectContent_Http(bool compressPayload) using var httpClient = new HttpClient(testHttpHandler, false); var exportClient = new OtlpHttpExportClient(options, httpClient, string.Empty); - var buffer = Enumerable.Repeat((byte)65, 1000).ToArray(); var deadlineUtc = DateTime.UtcNow.AddMilliseconds(httpClient.Timeout.TotalMilliseconds); // Act @@ -61,10 +63,13 @@ public void SendExportRequest_SendsCorrectContent_Http(bool compressPayload) } [Theory] - [InlineData(true)] - [InlineData(false)] - public void SendExportRequest_SendsCorrectContent_Grpc(bool compressPayload) + [InlineData(true, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] + [InlineData(false, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] + [InlineData(true, "")] + public void SendExportRequest_SendsCorrectContent_Grpc(bool compressPayload, string text) { + var payload = System.Text.Encoding.UTF8.GetBytes(text); + // Arrange var options = new OtlpExporterOptions { @@ -72,7 +77,6 @@ public void SendExportRequest_SendsCorrectContent_Grpc(bool compressPayload) CompressPayload = compressPayload, }; - var payload = Enumerable.Repeat((byte)65, 1000).ToArray(); var buffer = new byte[payload.Length + 5]; Buffer.BlockCopy(payload, 0, buffer, 5, payload.Length); @@ -91,7 +95,6 @@ public void SendExportRequest_SendsCorrectContent_Grpc(bool compressPayload) Assert.True(result.Success); Assert.NotNull(httpRequest); Assert.NotNull(requestContent); - Assert.True(requestContent.Length > 5); // gRPC frame must be at least 5 bytes var compressionFlag = requestContent[0]; var declaredLength = BinaryPrimitives.ReadUInt32BigEndian(requestContent.AsSpan(1, 4)); From 39d6187806e0ed723afd8b0f2e61626e25f5ecb6 Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Thu, 11 Sep 2025 14:12:42 -0400 Subject: [PATCH 07/20] fix dataLength --- .../Implementation/ExportClient/OtlpExportClient.cs | 4 +++- .../Implementation/ExportClient/OtlpGrpcExportClient.cs | 2 +- .../OtlpTraceExporter.cs | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs index 7da8855a8e4..1d830d0289a 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs @@ -89,13 +89,15 @@ protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength) } var data = buffer; + var dataLength = contentLength; if (this.CompressionEnabled) { data = this.Compress(buffer, contentLength); + dataLength = data.Length; } - request.Content = new ByteArrayContent(data, 0, data.Length); + request.Content = new ByteArrayContent(data, 0, dataLength); request.Content.Headers.ContentType = this.MediaTypeHeader; if (this.CompressionEnabled && this.ContentEncodingHeader != null) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs index 32dce4a602b..7377b0f5144 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs @@ -48,7 +48,7 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten // bytes 1-4 - Message length in big-endian format (length of the serialized data only). // bytes 5+ - Protobuf-encoded payload. Span data = new Span(buffer, 1, 4); - var dataLength = buffer.Length - GrpcMessageHeaderSize; + var dataLength = contentLength - GrpcMessageHeaderSize; BinaryPrimitives.WriteUInt32BigEndian(data, (uint)dataLength); buffer[0] = this.CompressionEnabled ? (byte)1 : (byte)0; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs index 92dc0f79bf9..6122541b925 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs @@ -1,6 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 +using System.Buffers.Binary; using System.Diagnostics; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; From fa128afd6be7aa974b745cbc533a04256930f7ab Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Thu, 11 Sep 2025 14:18:16 -0400 Subject: [PATCH 08/20] Updated public API files --- .../.publicApi/Stable/PublicAPI.Unshipped.txt | 2 ++ .../OtlpTraceExporter.cs | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt index e69de29bb2d..3e6160afd62 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +OpenTelemetry.Exporter.OtlpExporterOptions.CompressPayload.get -> bool +OpenTelemetry.Exporter.OtlpExporterOptions.CompressPayload.set -> void \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs index 6122541b925..92dc0f79bf9 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs @@ -1,7 +1,6 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -using System.Buffers.Binary; using System.Diagnostics; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer; From 8449eb5d5cae6dd79fdaa503ff1c90ea503d02ad Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Thu, 11 Sep 2025 15:29:46 -0400 Subject: [PATCH 09/20] added changelog entry for compression in exporter --- .../CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index 82056bf2c5f..faf1c80d95b 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -7,6 +7,13 @@ Notes](../../RELEASENOTES.md). ## Unreleased +* Added support for gzip compression in the OTLP exporter for HTTP and gRPC. + Compression can be configured using the environment variables `OTEL_EXPORTER_OTLP_COMPRESSION`, + `OTEL_EXPORTER_OTLP_TRACES_COMPRESSION`, `OTEL_EXPORTER_OTLP_METRICS_COMPRESSION`, + `OTEL_EXPORTER_OTLP_LOGS_COMPRESSION`. Setting the respective environment variable + to `gzip` activates compression. + ([#6494](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6494)) + * Fixed an issue in .NET Framework where OTLP export of traces, logs, and metrics using `OtlpExportProtocol.Grpc` did not correctly set the initial write position, resulting in gRPC protocol errors. From e720418c96673f308dbbaf7d32b85956a47eccd0 Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Thu, 11 Sep 2025 16:00:10 -0400 Subject: [PATCH 10/20] fix spacing --- .../Implementation/ExportClient/OtlpGrpcExportClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs index 7377b0f5144..7fb5780d2ff 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs @@ -190,7 +190,7 @@ protected override byte[] Compress(byte[] data, int contentLength) BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, 4), (uint)compressedDataLength); return payload; - } + } private static bool IsTransientNetworkError(HttpRequestException ex) { From e71f47293d78de60626eab635d1984127a33799f Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Wed, 17 Sep 2025 00:28:17 -0400 Subject: [PATCH 11/20] Introduced enum for export compression configuration --- .../.publicApi/Stable/PublicAPI.Unshipped.txt | 7 +++-- .../IOtlpExporterOptions.cs | 4 +-- .../ExportClient/OtlpExportClient.cs | 12 ++++----- .../ExportClient/OtlpGrpcExportClient.cs | 9 +++---- .../ExportClient/OtlpHttpExportClient.cs | 2 +- .../OtlpExportCompression.cs | 20 ++++++++++++++ .../OtlpExporterOptions.cs | 8 +++--- .../OtlpExporterCompressionTests.cs | 26 +++++++++---------- .../OtlpExporterOptionsTests.cs | 6 ++--- 9 files changed, 58 insertions(+), 36 deletions(-) create mode 100644 src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt index 3e6160afd62..9423c204626 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/Stable/PublicAPI.Unshipped.txt @@ -1,2 +1,5 @@ -OpenTelemetry.Exporter.OtlpExporterOptions.CompressPayload.get -> bool -OpenTelemetry.Exporter.OtlpExporterOptions.CompressPayload.set -> void \ No newline at end of file +OpenTelemetry.Exporter.OtlpExportCompression +OpenTelemetry.Exporter.OtlpExportCompression.Gzip = 1 -> OpenTelemetry.Exporter.OtlpExportCompression +OpenTelemetry.Exporter.OtlpExportCompression.None = 0 -> OpenTelemetry.Exporter.OtlpExportCompression +OpenTelemetry.Exporter.OtlpExporterOptions.Compression.get -> OpenTelemetry.Exporter.OtlpExportCompression +OpenTelemetry.Exporter.OtlpExporterOptions.Compression.set -> void diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs index c33f1afbbd0..92ed84568c7 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs @@ -70,13 +70,13 @@ internal interface IOtlpExporterOptions int TimeoutMilliseconds { get; set; } /// - /// Gets or sets a value indicating whether to compress the payload. + /// Gets or sets a value indicating how to compress the payload. /// Currently gzip is the only supported compression method. /// Note: Refer to the /// OpenTelemetry Specification for details />. /// - bool CompressPayload { get; set; } + OtlpExportCompression Compression { get; set; } /// /// Gets or sets the factory function called to create the >((d, k, v) => d.Add(k, v)); this.HttpClient = httpClient; - this.CompressionEnabled = options.CompressPayload; + this.Compression = options.Compression; } internal HttpClient HttpClient { get; } - internal bool CompressionEnabled { get; } + internal OtlpExportCompression Compression { get; } internal Uri Endpoint { get; } @@ -91,16 +91,16 @@ protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength) var data = buffer; var dataLength = contentLength; - if (this.CompressionEnabled) + if (this.Compression == OtlpExportCompression.Gzip) { - data = this.Compress(buffer, contentLength); + data = this.Compress(buffer); dataLength = data.Length; } request.Content = new ByteArrayContent(data, 0, dataLength); request.Content.Headers.ContentType = this.MediaTypeHeader; - if (this.CompressionEnabled && this.ContentEncodingHeader != null) + if (this.Compression == OtlpExportCompression.Gzip && this.ContentEncodingHeader != null) { request.Content.Headers.Add("Content-Encoding", this.ContentEncodingHeader); } @@ -108,7 +108,7 @@ protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength) return request; } - protected abstract byte[] Compress(byte[] data, int contentLength); + protected abstract byte[] Compress(byte[] data); protected HttpResponseMessage SendHttpRequest(HttpRequestMessage request, CancellationToken cancellationToken) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs index 7fb5780d2ff..38453553982 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs @@ -47,10 +47,10 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten // byte 0 - Compression flag (0 = not compressed, 1 = compressed). // bytes 1-4 - Message length in big-endian format (length of the serialized data only). // bytes 5+ - Protobuf-encoded payload. + buffer[0] = this.Compression == OtlpExportCompression.Gzip ? (byte)1 : (byte)0; Span data = new Span(buffer, 1, 4); var dataLength = contentLength - GrpcMessageHeaderSize; BinaryPrimitives.WriteUInt32BigEndian(data, (uint)dataLength); - buffer[0] = this.CompressionEnabled ? (byte)1 : (byte)0; using var httpRequest = this.CreateHttpRequest(buffer, contentLength); @@ -172,7 +172,7 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten } } - protected override byte[] Compress(byte[] data, int contentLength) + protected override byte[] Compress(byte[] data) { using var compressedStream = new MemoryStream(); using (var gzipStream = new GZipStream(compressedStream, CompressionLevel.Optimal, leaveOpen: true)) @@ -182,13 +182,12 @@ protected override byte[] Compress(byte[] data, int contentLength) var compressedDataLength = compressedStream.Position; var payload = new byte[compressedDataLength + GrpcMessageHeaderSize]; + payload[0] = 1; + BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, 4), (uint)compressedDataLength); using var payloadStream = new MemoryStream(payload); payloadStream.Position = GrpcMessageHeaderSize; compressedStream.WriteTo(payloadStream); - payload[0] = 1; - BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, 4), (uint)compressedDataLength); - return payload; } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs index 39b1e75ce42..26724ffd7b8 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs @@ -51,7 +51,7 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten } } - protected override byte[] Compress(byte[] data, int contentLength) + protected override byte[] Compress(byte[] data) { using var compressedStream = new MemoryStream(); using (var gzipStream = new GZipStream(compressedStream, CompressionLevel.Optimal)) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs new file mode 100644 index 00000000000..a1952d4fb76 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs @@ -0,0 +1,20 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +namespace OpenTelemetry.Exporter; + +/// +/// Supported compression methods for OTLP exporter according to the specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md. +/// +public enum OtlpExportCompression +{ + /// + /// Compression is disabled. + /// + None = 0, + + /// + /// Gzip is the only specified compression method for now. + /// + Gzip = 1, +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index 847ff4f4d85..c9f8b3b47f9 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -138,7 +138,7 @@ public OtlpExportProtocol Protocol public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } /// - public bool CompressPayload { get; set; } + public OtlpExportCompression Compression { get; set; } = OtlpExportCompression.None; /// public Func HttpClientFactory @@ -213,11 +213,11 @@ internal void ApplyConfigurationUsingSpecificationEnvVars( if (configuration.TryGetStringValue(compressPayloadEnvVarKey, out var compressPayload)) { - if (string.Equals(compressPayload.Trim(), "gzip", StringComparison.OrdinalIgnoreCase)) + if (Enum.TryParse(compressPayload.Trim(), true, out var compression)) { - this.CompressPayload = true; + this.Compression = compression; } - else if (!string.Equals(compressPayload.Trim(), "none", StringComparison.OrdinalIgnoreCase)) + else { OpenTelemetryProtocolExporterEventSource.Log.InvalidConfigurationValue(compressPayloadEnvVarKey, compressPayload); } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs index 694799f9703..d7a7df3f976 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs @@ -15,10 +15,10 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.Implementation.Expo public class OtlpExporterCompressionTests { [Theory] - [InlineData(true, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] - [InlineData(false, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] - [InlineData(true, "")] - public void SendExportRequest_SendsCorrectContent_Http(bool compressPayload, string text) + [InlineData(OtlpExportCompression.Gzip, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] + [InlineData(OtlpExportCompression.None, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] + [InlineData(OtlpExportCompression.Gzip, "")] + public void SendExportRequest_SendsCorrectContent_Http(OtlpExportCompression compression, string text) { var buffer = System.Text.Encoding.UTF8.GetBytes(text); @@ -26,7 +26,7 @@ public void SendExportRequest_SendsCorrectContent_Http(bool compressPayload, str var options = new OtlpExporterOptions { Endpoint = new Uri("http://localhost:4317"), - CompressPayload = compressPayload, + Compression = compression, }; using var testHttpHandler = new TestHttpMessageHandler(); @@ -45,7 +45,7 @@ public void SendExportRequest_SendsCorrectContent_Http(bool compressPayload, str Assert.Equal(HttpMethod.Post, httpRequest.Method); Assert.NotNull(httpRequest.Content); - if (compressPayload) + if (compression == OtlpExportCompression.Gzip) { Assert.Contains(httpRequest.Content.Headers, h => h.Key == "Content-Encoding" && h.Value.First() == "gzip"); @@ -63,10 +63,10 @@ public void SendExportRequest_SendsCorrectContent_Http(bool compressPayload, str } [Theory] - [InlineData(true, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] - [InlineData(false, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] - [InlineData(true, "")] - public void SendExportRequest_SendsCorrectContent_Grpc(bool compressPayload, string text) + [InlineData(OtlpExportCompression.Gzip, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] + [InlineData(OtlpExportCompression.None, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] + [InlineData(OtlpExportCompression.Gzip, "")] + public void SendExportRequest_SendsCorrectContent_Grpc(OtlpExportCompression compression, string text) { var payload = System.Text.Encoding.UTF8.GetBytes(text); @@ -74,7 +74,7 @@ public void SendExportRequest_SendsCorrectContent_Grpc(bool compressPayload, str var options = new OtlpExporterOptions { Endpoint = new Uri("http://localhost:4317"), - CompressPayload = compressPayload, + Compression = compression, }; var buffer = new byte[payload.Length + 5]; @@ -100,10 +100,10 @@ public void SendExportRequest_SendsCorrectContent_Grpc(bool compressPayload, str var declaredLength = BinaryPrimitives.ReadUInt32BigEndian(requestContent.AsSpan(1, 4)); var body = requestContent.AsSpan(5, (int)declaredLength).ToArray(); - Assert.Equal(compressPayload ? 1 : 0, compressionFlag); + Assert.Equal(compression == OtlpExportCompression.Gzip ? 1 : 0, compressionFlag); Assert.Equal(body.Length, (int)declaredLength); - if (compressPayload) + if (compression == OtlpExportCompression.Gzip) { var decompressedStream = Decompress(body); diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index 001df8f060f..ff42f1251ba 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -127,7 +127,7 @@ public void OtlpExporterOptions_InvalidEnvironmentVariableOverride() Assert.Equal(10000, options.TimeoutMilliseconds); Assert.Equal(OtlpExporterOptions.DefaultOtlpExportProtocol, options.Protocol); Assert.Null(options.Headers); - Assert.False(options.CompressPayload); + Assert.Equal(OtlpExportCompression.None, options.Compression); } [Fact] @@ -161,14 +161,14 @@ public void OtlpExporterOptions_SetterOverridesEnvironmentVariable() options.Headers = "C=3"; options.TimeoutMilliseconds = 40000; options.Protocol = OtlpExportProtocol.HttpProtobuf; - options.CompressPayload = true; + options.Compression = OtlpExportCompression.Gzip; Assert.Equal(new Uri("http://localhost:200"), options.Endpoint); Assert.Equal("C=3", options.Headers); Assert.Equal(40000, options.TimeoutMilliseconds); Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); Assert.False(options.AppendSignalPathToEndpoint); - Assert.True(options.CompressPayload); + Assert.Equal(OtlpExportCompression.Gzip, options.Compression); } [Fact] From 2b869ae999033849a5735ecf2bbc009b0bc456f3 Mon Sep 17 00:00:00 2001 From: Hannah Haering <157852144+hannahhaering@users.noreply.github.com> Date: Wed, 17 Sep 2025 00:29:11 -0400 Subject: [PATCH 12/20] Update test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestGrpcMessageHandler.cs Co-authored-by: Martin Costello --- .../TestGrpcMessageHandler.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestGrpcMessageHandler.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestGrpcMessageHandler.cs index ef9db42790c..e56a4d8cd57 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestGrpcMessageHandler.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/TestGrpcMessageHandler.cs @@ -58,8 +58,6 @@ protected override Task SendAsync(HttpRequestMessage reques } #if NETSTANDARD2_0 || NET462 - private sealed class CustomResponseTrailers : HttpHeaders - { - } + private sealed class CustomResponseTrailers : HttpHeaders; #endif } From 1e60db88032d9c56dc10cb5c7ccc66fa836ac8b5 Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Wed, 17 Sep 2025 17:32:05 -0400 Subject: [PATCH 13/20] use streams in compression method --- .../ExportClient/OtlpExportClient.cs | 10 +++---- .../ExportClient/OtlpGrpcExportClient.cs | 26 ++++++++++--------- .../ExportClient/OtlpHttpExportClient.cs | 15 +++++------ 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs index ebf2b22b903..e230c6ad361 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs @@ -88,16 +88,14 @@ protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength) request.Headers.Add(header.Key, header.Value); } - var data = buffer; - var dataLength = contentLength; + Stream data = new MemoryStream(buffer, 0, contentLength); if (this.Compression == OtlpExportCompression.Gzip) { - data = this.Compress(buffer); - dataLength = data.Length; + data = this.Compress(data); } - request.Content = new ByteArrayContent(data, 0, dataLength); + request.Content = new StreamContent(data); request.Content.Headers.ContentType = this.MediaTypeHeader; if (this.Compression == OtlpExportCompression.Gzip && this.ContentEncodingHeader != null) @@ -108,7 +106,7 @@ protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength) return request; } - protected abstract byte[] Compress(byte[] data); + protected abstract Stream Compress(Stream dataStream); protected HttpResponseMessage SendHttpRequest(HttpRequestMessage request, CancellationToken cancellationToken) { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs index 38453553982..bd434bfe745 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs @@ -172,23 +172,25 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten } } - protected override byte[] Compress(byte[] data) + protected override Stream Compress(Stream dataStream) { - using var compressedStream = new MemoryStream(); - using (var gzipStream = new GZipStream(compressedStream, CompressionLevel.Optimal, leaveOpen: true)) + var compressedStream = new MemoryStream(); + + compressedStream.WriteByte(1); + compressedStream.Write(new byte[4], 0, 4); + + dataStream.Position = GrpcMessageHeaderSize; + + using (var gzipStream = new GZipStream(compressedStream, CompressionLevel.Fastest, leaveOpen: true)) { - gzipStream.Write(data, GrpcMessageHeaderSize, data.Length - GrpcMessageHeaderSize); + dataStream.CopyTo(gzipStream); } - var compressedDataLength = compressedStream.Position; - var payload = new byte[compressedDataLength + GrpcMessageHeaderSize]; - payload[0] = 1; - BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(1, 4), (uint)compressedDataLength); - using var payloadStream = new MemoryStream(payload); - payloadStream.Position = GrpcMessageHeaderSize; - compressedStream.WriteTo(payloadStream); + var compressedDataLength = (int)(compressedStream.Length - GrpcMessageHeaderSize); + BinaryPrimitives.WriteUInt32BigEndian(compressedStream.GetBuffer().AsSpan(1, 4), (uint)compressedDataLength); - return payload; + compressedStream.Position = 0; + return compressedStream; } 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 26724ffd7b8..9ebd8913657 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs @@ -51,18 +51,15 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten } } - protected override byte[] Compress(byte[] data) + protected override Stream Compress(Stream dataStream) { - using var compressedStream = new MemoryStream(); - using (var gzipStream = new GZipStream(compressedStream, CompressionLevel.Optimal)) + var compressedStream = new MemoryStream(); + using (var gzipStream = new GZipStream(compressedStream, CompressionLevel.Fastest, leaveOpen: true)) { -#if NET462 || NETSTANDARD2_0 - gzipStream.Write(data.ToArray(), 0, data.Length); -#else - gzipStream.Write(data); -#endif + dataStream.CopyTo(gzipStream); } - return compressedStream.ToArray(); + compressedStream.Position = 0; + return compressedStream; } } From d30fa88b79ac12956a3393e02add838a30dfb6af Mon Sep 17 00:00:00 2001 From: Hannah Haering <157852144+hannahhaering@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:08:21 -0400 Subject: [PATCH 14/20] Update src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs Co-authored-by: Martin Costello --- .../IOtlpExporterOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs index 92ed84568c7..a900d9074f0 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs @@ -71,8 +71,8 @@ internal interface IOtlpExporterOptions /// /// Gets or sets a value indicating how to compress the payload. - /// Currently gzip is the only supported compression method. - /// Note: Refer to the /// OpenTelemetry Specification for details />. /// From 4ddd75e28c5f9a0b4f31b764b6a2e285a980a67d Mon Sep 17 00:00:00 2001 From: Hannah Haering <157852144+hannahhaering@users.noreply.github.com> Date: Thu, 18 Sep 2025 22:09:14 -0400 Subject: [PATCH 15/20] Update src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs Co-authored-by: Martin Costello --- .../OtlpExportCompression.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs index a1952d4fb76..c4b7af5b097 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs @@ -14,7 +14,7 @@ public enum OtlpExportCompression None = 0, /// - /// Gzip is the only specified compression method for now. + /// Compress with Gzip. /// Gzip = 1, } From 047044e18f0c7f4e0f2569316b8c69990448e3ac Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Thu, 18 Sep 2025 23:02:51 -0400 Subject: [PATCH 16/20] split unit tests by compression; added grpc compression header --- .../ExportClient/OtlpExportClient.cs | 8 +- .../ExportClient/OtlpGrpcExportClient.cs | 6 +- .../ExportClient/OtlpHttpExportClient.cs | 4 +- .../OtlpExporterCompressionTests.cs | 114 ++++++++++-------- 4 files changed, 76 insertions(+), 56 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs index e230c6ad361..535918c8e2e 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs @@ -59,7 +59,9 @@ protected OtlpExportClient(OtlpExporterOptions options, HttpClient httpClient, s internal virtual bool RequireHttp2 => false; - protected abstract string? ContentEncodingHeader { get; } + protected abstract string? ContentEncodingHeaderKey { get; } + + protected abstract string? ContentEncodingHeaderValue { get; } public abstract ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default); @@ -98,9 +100,9 @@ protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength) request.Content = new StreamContent(data); request.Content.Headers.ContentType = this.MediaTypeHeader; - if (this.Compression == OtlpExportCompression.Gzip && this.ContentEncodingHeader != null) + if (this.Compression == OtlpExportCompression.Gzip && this.ContentEncodingHeaderKey != null) { - request.Content.Headers.Add("Content-Encoding", this.ContentEncodingHeader); + request.Content.Headers.Add(this.ContentEncodingHeaderKey, this.ContentEncodingHeaderValue); } return request; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs index bd434bfe745..a4d7cea9510 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs @@ -36,7 +36,9 @@ public OtlpGrpcExportClient(OtlpExporterOptions options, HttpClient httpClient, internal override bool RequireHttp2 => true; - protected override string? ContentEncodingHeader => null; + protected override string ContentEncodingHeaderKey => "grpc-encoding"; + + protected override string ContentEncodingHeaderValue => "gzip"; /// public override ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default) @@ -177,7 +179,7 @@ protected override Stream Compress(Stream dataStream) var compressedStream = new MemoryStream(); compressedStream.WriteByte(1); - compressedStream.Write(new byte[4], 0, 4); + compressedStream.Write([0, 0, 0, 0], 0, 4); dataStream.Position = GrpcMessageHeaderSize; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs index 9ebd8913657..026e907f4b2 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs @@ -22,7 +22,9 @@ internal OtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient internal override MediaTypeHeaderValue MediaTypeHeader => MediaHeaderValue; - protected override string? ContentEncodingHeader => "gzip"; + protected override string ContentEncodingHeaderKey => "Content-Encoding"; + + protected override string ContentEncodingHeaderValue => "gzip"; /// public override ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs index d7a7df3f976..fc899835695 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpExporterCompressionTests.cs @@ -3,6 +3,7 @@ using System.Buffers.Binary; using System.IO.Compression; +using System.Net.Http.Headers; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; using Xunit; @@ -15,58 +16,50 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.Implementation.Expo public class OtlpExporterCompressionTests { [Theory] - [InlineData(OtlpExportCompression.Gzip, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] - [InlineData(OtlpExportCompression.None, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] - [InlineData(OtlpExportCompression.Gzip, "")] - public void SendExportRequest_SendsCorrectContent_Http(OtlpExportCompression compression, string text) + [InlineData("")] + [InlineData("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] + public void SendExportRequest_SendsCorrectContent_Http_NonCompressed(string text) { - var buffer = System.Text.Encoding.UTF8.GetBytes(text); - - // Arrange - var options = new OtlpExporterOptions + SendExportRequest_Http(OtlpExportCompression.None, text, (requestHeaders, testHttpHandlerContent, buffer) => { - Endpoint = new Uri("http://localhost:4317"), - Compression = compression, - }; - - using var testHttpHandler = new TestHttpMessageHandler(); - using var httpClient = new HttpClient(testHttpHandler, false); - var exportClient = new OtlpHttpExportClient(options, httpClient, string.Empty); - - var deadlineUtc = DateTime.UtcNow.AddMilliseconds(httpClient.Timeout.TotalMilliseconds); - - // Act - var result = exportClient.SendExportRequest(buffer, buffer.Length, deadlineUtc); - var httpRequest = testHttpHandler.HttpRequestMessage; - - // Assert - Assert.True(result.Success); - Assert.NotNull(httpRequest); - Assert.Equal(HttpMethod.Post, httpRequest.Method); - Assert.NotNull(httpRequest.Content); + Assert.DoesNotContain(requestHeaders, h => h.Key == "Content-Encoding"); + Assert.Equal(buffer, testHttpHandlerContent); + }); + } - if (compression == OtlpExportCompression.Gzip) + [Theory] + [InlineData("")] + [InlineData("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] + public void SendExportRequest_SendsCorrectContent_Http_Compressed(string text) + { + SendExportRequest_Http(OtlpExportCompression.Gzip, text, (requestHeaders, testHttpHandlerContent, buffer) => { - Assert.Contains(httpRequest.Content.Headers, h => h.Key == "Content-Encoding" && h.Value.First() == "gzip"); + Assert.Contains(requestHeaders, h => h.Key == "Content-Encoding" && h.Value.First() == "gzip"); - var content = testHttpHandler.HttpRequestContent; - Assert.NotNull(content); - var decompressedStream = Decompress(content); + Assert.NotNull(testHttpHandlerContent); + var decompressedStream = Decompress(testHttpHandlerContent); Assert.Equal(buffer, decompressedStream.ToArray()); - } - else - { - Assert.DoesNotContain(httpRequest.Content.Headers, h => h.Key == "Content-Encoding"); - Assert.Equal(buffer, testHttpHandler.HttpRequestContent); - } + }); + } + + [Theory] + [InlineData("")] + [InlineData("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] + public void SendExportRequest_SendsCorrectContent_Grpc_NonCompressed(string text) + { + SendExportRequest_Grpc(OtlpExportCompression.None, text, body => body); } [Theory] - [InlineData(OtlpExportCompression.Gzip, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] - [InlineData(OtlpExportCompression.None, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] - [InlineData(OtlpExportCompression.Gzip, "")] - public void SendExportRequest_SendsCorrectContent_Grpc(OtlpExportCompression compression, string text) + [InlineData("")] + [InlineData("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] + public void SendExportRequest_SendsCorrectContent_Grpc_Compressed(string text) + { + SendExportRequest_Grpc(OtlpExportCompression.Gzip, text, Decompress); + } + + private static void SendExportRequest_Grpc(OtlpExportCompression compression, string text, Func readBody) { var payload = System.Text.Encoding.UTF8.GetBytes(text); @@ -103,16 +96,37 @@ public void SendExportRequest_SendsCorrectContent_Grpc(OtlpExportCompression com Assert.Equal(compression == OtlpExportCompression.Gzip ? 1 : 0, compressionFlag); Assert.Equal(body.Length, (int)declaredLength); - if (compression == OtlpExportCompression.Gzip) - { - var decompressedStream = Decompress(body); + Assert.Equal(payload, readBody(body)); + } + + private static void SendExportRequest_Http(OtlpExportCompression compression, string text, Action assertions) + { + var buffer = System.Text.Encoding.UTF8.GetBytes(text); - Assert.Equal(payload, decompressedStream.ToArray()); - } - else + // Arrange + var options = new OtlpExporterOptions { - Assert.Equal(payload, body); - } + Endpoint = new Uri("http://localhost:4317"), + Compression = compression, + }; + + using var testHttpHandler = new TestHttpMessageHandler(); + using var httpClient = new HttpClient(testHttpHandler, false); + var exportClient = new OtlpHttpExportClient(options, httpClient, string.Empty); + + var deadlineUtc = DateTime.UtcNow.AddMilliseconds(httpClient.Timeout.TotalMilliseconds); + + // Act + var result = exportClient.SendExportRequest(buffer, buffer.Length, deadlineUtc); + var httpRequest = testHttpHandler.HttpRequestMessage; + + // Assert + Assert.True(result.Success); + Assert.NotNull(httpRequest); + Assert.Equal(HttpMethod.Post, httpRequest.Method); + Assert.NotNull(httpRequest.Content); + + assertions(httpRequest.Content.Headers, testHttpHandler.HttpRequestContent, buffer); } private static byte[] Decompress(byte[] data) From 8c99fd9a7451685153fb6938ddf05b4ed0f76d00 Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Fri, 19 Sep 2025 16:05:11 -0400 Subject: [PATCH 17/20] added support for gzip compression in example console application --- examples/Console/Program.cs | 3 +++ examples/Console/TestOtlpExporter.cs | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/examples/Console/Program.cs b/examples/Console/Program.cs index a7d0e822f47..06c14ecf048 100644 --- a/examples/Console/Program.cs +++ b/examples/Console/Program.cs @@ -125,6 +125,9 @@ internal sealed class OtlpOptions [Option('p', "protocol", HelpText = "Transport protocol used by exporter. Supported values: grpc and http/protobuf.", Default = "grpc")] public string? Protocol { get; set; } + + [Option('c', "compression", HelpText = "Compression algorithm used by exporter. Supported values: none and gzip.", Default = "None")] + public string? Compression { get; set; } } [Verb("logs", HelpText = "Specify the options required to test Logs")] diff --git a/examples/Console/TestOtlpExporter.cs b/examples/Console/TestOtlpExporter.cs index c38ac518a18..eb9402975be 100644 --- a/examples/Console/TestOtlpExporter.cs +++ b/examples/Console/TestOtlpExporter.cs @@ -62,6 +62,10 @@ private static int RunWithActivitySource(OtlpOptions options) } opt.Protocol = otlpExportProtocol.Value; + if (Enum.TryParse(options.Compression, true, out var compression)) + { + opt.Compression = compression; + } System.Console.WriteLine($"OTLP Exporter is using {opt.Protocol} protocol and endpoint {opt.Endpoint}"); }) From 804f09a31eac00ad31b8773055993e28a7588929 Mon Sep 17 00:00:00 2001 From: Hannah Haering <157852144+hannahhaering@users.noreply.github.com> Date: Fri, 19 Sep 2025 17:43:27 -0400 Subject: [PATCH 18/20] Update src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs Co-authored-by: Martin Costello --- .../OtlpExportCompression.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs index c4b7af5b097..cdc920c73bd 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportCompression.cs @@ -9,7 +9,7 @@ namespace OpenTelemetry.Exporter; public enum OtlpExportCompression { /// - /// Compression is disabled. + /// No compression. /// None = 0, From 27220c73a02520a4e9ba40b4f9b5e06ebf57c89c Mon Sep 17 00:00:00 2001 From: hannahhaering Date: Fri, 19 Sep 2025 17:44:53 -0400 Subject: [PATCH 19/20] remove unnecessary cast --- .../Implementation/ExportClient/OtlpGrpcExportClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs index a4d7cea9510..86231364e14 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs @@ -188,8 +188,8 @@ protected override Stream Compress(Stream dataStream) dataStream.CopyTo(gzipStream); } - var compressedDataLength = (int)(compressedStream.Length - GrpcMessageHeaderSize); - BinaryPrimitives.WriteUInt32BigEndian(compressedStream.GetBuffer().AsSpan(1, 4), (uint)compressedDataLength); + var compressedDataLength = (uint)(compressedStream.Length - GrpcMessageHeaderSize); + BinaryPrimitives.WriteUInt32BigEndian(compressedStream.GetBuffer().AsSpan(1, 4), compressedDataLength); compressedStream.Position = 0; return compressedStream; From 41dc74727b1d7d2a08ad841385d3243f6cf624cf Mon Sep 17 00:00:00 2001 From: Hannah Haering <157852144+hannahhaering@users.noreply.github.com> Date: Fri, 19 Sep 2025 17:50:22 -0400 Subject: [PATCH 20/20] Update examples/Console/Program.cs Co-authored-by: Cijo Thomas --- examples/Console/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/Console/Program.cs b/examples/Console/Program.cs index 06c14ecf048..3a29544a608 100644 --- a/examples/Console/Program.cs +++ b/examples/Console/Program.cs @@ -126,7 +126,7 @@ internal sealed class OtlpOptions [Option('p', "protocol", HelpText = "Transport protocol used by exporter. Supported values: grpc and http/protobuf.", Default = "grpc")] public string? Protocol { get; set; } - [Option('c', "compression", HelpText = "Compression algorithm used by exporter. Supported values: none and gzip.", Default = "None")] + [Option('c', "compression", HelpText = "Compression algorithm used by exporter. Supported values: none and gzip.", Default = "none")] public string? Compression { get; set; } }