Skip to content

Commit 6232fb2

Browse files
[OTLP] Add support for GZip compression (#7055)
Co-authored-by: Hannah Haering <157852144+hannahhaering@users.noreply.github.com>
1 parent 86f0c3f commit 6232fb2

18 files changed

Lines changed: 1267 additions & 59 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#nullable enable
2+
OpenTelemetry.Exporter.OtlpExportCompression
3+
OpenTelemetry.Exporter.OtlpExportCompression.GZip = 1 -> OpenTelemetry.Exporter.OtlpExportCompression
4+
OpenTelemetry.Exporter.OtlpExportCompression.None = 0 -> OpenTelemetry.Exporter.OtlpExportCompression
5+
OpenTelemetry.Exporter.OtlpExporterOptions.Compression.get -> OpenTelemetry.Exporter.OtlpExportCompression
6+
OpenTelemetry.Exporter.OtlpExporterOptions.Compression.set -> void

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ Notes](../../RELEASENOTES.md).
1010
* Fixed `NullReferenceException` when exporting logs if the scope key is null.
1111
([#7186](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7186))
1212

13+
* Added opt-in support for gzip compression. Compression can be configured
14+
programmatically via the new `OtlpExporterOptions.Compression` property,
15+
or through the environment variables such as `OTEL_EXPORTER_OTLP_COMPRESSION=gzip`.
16+
([#7055](https://github.com/open-telemetry/opentelemetry-dotnet/issues/7055))
17+
1318
## 1.15.3
1419

1520
Released 2026-Apr-21

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/IOtlpExporterOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,9 @@ internal interface IOtlpExporterOptions
103103
/// </list>
104104
/// </remarks>
105105
Func<HttpClient> HttpClientFactory { get; set; }
106+
107+
/// <summary>
108+
/// Gets or sets the compression method to use when sending telemetry.
109+
/// </summary>
110+
OtlpExportCompression Compression { get; set; }
106111
}

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpExportClient.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ protected OtlpExportClient(OtlpExporterOptions options, HttpClient httpClient, s
4444
this.Endpoint = new UriBuilder(exporterEndpoint).Uri;
4545
this.Headers = options.GetHeaders<Dictionary<string, string>>((d, k, v) => d.Add(k, v));
4646
this.HttpClient = httpClient;
47+
this.CompressionEnabled = options.Compression == OtlpExportCompression.GZip;
4748
}
4849

4950
internal HttpClient HttpClient { get; }
@@ -52,6 +53,8 @@ protected OtlpExportClient(OtlpExporterOptions options, HttpClient httpClient, s
5253

5354
internal IReadOnlyDictionary<string, string> Headers { get; }
5455

56+
internal bool CompressionEnabled { get; }
57+
5558
internal abstract MediaTypeHeaderValue MediaTypeHeader { get; }
5659

5760
internal virtual bool RequireHttp2 => false;
@@ -88,14 +91,25 @@ protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength)
8891
request.Headers.Add(header.Key, header.Value);
8992
}
9093

91-
// TODO: Support compression.
92-
93-
request.Content = new ByteArrayContent(buffer, 0, contentLength);
94-
request.Content.Headers.ContentType = this.MediaTypeHeader;
94+
request.Content = this.CreateHttpContent(buffer, contentLength);
9595

9696
return request;
9797
}
9898

99+
/// <summary>
100+
/// Creates the <see cref="HttpContent"/> for a request. Override in subclasses to
101+
/// customise content creation (e.g. to apply compression).
102+
/// </summary>
103+
/// <param name="buffer">The serialized protobuf payload buffer.</param>
104+
/// <param name="contentLength">The number of bytes within <paramref name="buffer"/> that make up the message.</param>
105+
/// <returns>An <see cref="HttpContent"/> representing the export payload.</returns>
106+
protected virtual HttpContent CreateHttpContent(byte[] buffer, int contentLength)
107+
{
108+
var content = new ByteArrayContent(buffer, 0, contentLength);
109+
content.Headers.ContentType = this.MediaTypeHeader;
110+
return content;
111+
}
112+
99113
protected HttpResponseMessage SendHttpRequest(HttpRequestMessage request, CancellationToken cancellationToken) =>
100114
#if NET
101115
// Note: SendAsync must be used with HTTP/2 because synchronous send is

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcExportClient.cs

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
#if NETFRAMEWORK
55
using System.Net.Http;
66
#endif
7+
using System.Buffers.Binary;
78
using System.Diagnostics.Tracing;
9+
using System.IO.Compression;
810
using System.Net.Http.Headers;
11+
using System.Net.Sockets;
912
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc;
1013

1114
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
@@ -14,6 +17,12 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClie
1417
internal sealed class OtlpGrpcExportClient : OtlpExportClient
1518
{
1619
public const string GrpcStatusDetailsHeader = "grpc-status-details-bin";
20+
21+
// A gRPC message frame header is 5 bytes:
22+
// byte 0 - Compression flag (0 = not compressed, 1 = compressed).
23+
// bytes 1-4 - Message length in big-endian format.
24+
private const int GrpcMessageHeaderSize = 5;
25+
1726
private static readonly ExportClientHttpResponse SuccessExportResponse = new(success: true, deadlineUtc: default, response: null, exception: null);
1827
private static readonly MediaTypeHeaderValue MediaHeaderValue = new("application/grpc");
1928

@@ -25,6 +34,10 @@ private static readonly ExportClientGrpcResponse DefaultExceptionExportClientGrp
2534
status: null,
2635
grpcStatusDetailsHeader: null);
2736

37+
#if !NET
38+
private static readonly byte[] GrpcFrameHeader = [0, 0, 0, 0, 0];
39+
#endif
40+
2841
public OtlpGrpcExportClient(OtlpExporterOptions options, HttpClient httpClient, string signalPath)
2942
: base(options, httpClient, signalPath)
3043
{
@@ -37,6 +50,11 @@ public OtlpGrpcExportClient(OtlpExporterOptions options, HttpClient httpClient,
3750
// We need the entire response content to ensure that the response trailers are received
3851
internal override HttpCompletionOption CompletionOption => HttpCompletionOption.ResponseContentRead;
3952

53+
#if NET
54+
// See https://vcsjones.dev/csharp-readonly-span-bytes-static/
55+
private static ReadOnlySpan<byte> GrpcFrameHeader => [0, 0, 0, 0, 0];
56+
#endif
57+
4058
/// <inheritdoc/>
4159
public override ExportClientResponse SendExportRequest(byte[] buffer, int contentLength, DateTime deadlineUtc, CancellationToken cancellationToken = default)
4260
{
@@ -50,6 +68,12 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten
5068
// A missing TE header results in servers aborting the gRPC call.
5169
httpRequest.Headers.TryAddWithoutValidation("TE", "trailers");
5270

71+
if (this.CompressionEnabled)
72+
{
73+
httpRequest.Headers.Remove("grpc-encoding");
74+
httpRequest.Headers.TryAddWithoutValidation("grpc-encoding", "gzip");
75+
}
76+
5377
httpResponse = this.SendHttpRequest(httpRequest, cancellationToken);
5478

5579
httpResponse.EnsureSuccessStatusCode();
@@ -173,10 +197,54 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten
173197
}
174198
}
175199

200+
protected override HttpContent CreateHttpContent(byte[] buffer, int contentLength)
201+
{
202+
if (!this.CompressionEnabled)
203+
{
204+
return base.CreateHttpContent(buffer, contentLength);
205+
}
206+
207+
// Build a gzip-compressed gRPC message frame:
208+
// byte 0 - Compression flag = 1 (gzip).
209+
// bytes 1-4 - Compressed payload length in big-endian format.
210+
// bytes 5+ - Gzip-compressed protobuf payload.
211+
#if NET
212+
var compressedStream = new PooledBufferStream();
213+
#else
214+
var compressedStream = new MemoryStream();
215+
#endif
216+
217+
// Reserve space for the gRPC frame header.
218+
#if NET
219+
compressedStream.Write(GrpcFrameHeader);
220+
#else
221+
compressedStream.Write(GrpcFrameHeader, 0, GrpcFrameHeader.Length);
222+
#endif
223+
224+
using (var gzipStream = new GZipStream(compressedStream, CompressionLevel.Fastest, leaveOpen: true))
225+
{
226+
gzipStream.Write(buffer, GrpcMessageHeaderSize, contentLength - GrpcMessageHeaderSize);
227+
}
228+
229+
var compressedPayloadLength = (uint)(compressedStream.Length - GrpcMessageHeaderSize);
230+
231+
// Write the gRPC frame header: compression flag + big-endian payload length.
232+
compressedStream.Position = 0;
233+
compressedStream.WriteByte(1);
234+
235+
var lengthBytes = new byte[4];
236+
BinaryPrimitives.WriteUInt32BigEndian(lengthBytes, compressedPayloadLength);
237+
compressedStream.Write(lengthBytes, 0, 4);
238+
239+
compressedStream.Position = 0;
240+
241+
OpenTelemetryProtocolExporterEventSource.Log.CompressedGrpcPayload("gzip", contentLength, compressedStream.Length);
242+
243+
var content = new StreamContent(compressedStream);
244+
content.Headers.ContentType = this.MediaTypeHeader;
245+
return content;
246+
}
247+
176248
private static bool IsTransientNetworkError(HttpRequestException ex) =>
177-
ex.InnerException is System.Net.Sockets.SocketException socketEx
178-
&& (socketEx.SocketErrorCode == System.Net.Sockets.SocketError.TimedOut
179-
|| socketEx.SocketErrorCode == System.Net.Sockets.SocketError.ConnectionReset
180-
|| socketEx.SocketErrorCode == System.Net.Sockets.SocketError.HostUnreachable
181-
|| socketEx.SocketErrorCode == System.Net.Sockets.SocketError.ConnectionRefused);
249+
ex.InnerException is SocketException { SocketErrorCode: SocketError.TimedOut or SocketError.ConnectionReset or SocketError.HostUnreachable or SocketError.ConnectionRefused };
182250
}

src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpExportClient.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Net.Http;
66
#endif
77
using System.Diagnostics.Tracing;
8+
using System.IO.Compression;
89
using System.Net.Http.Headers;
910

1011
namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
@@ -54,4 +55,34 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten
5455
return new ExportClientHttpResponse(success: false, deadlineUtc: deadlineUtc, response: null, exception: ex);
5556
}
5657
}
58+
59+
protected override HttpContent CreateHttpContent(byte[] buffer, int contentLength)
60+
{
61+
if (!this.CompressionEnabled)
62+
{
63+
return base.CreateHttpContent(buffer, contentLength);
64+
}
65+
66+
#if NET
67+
var compressedStream = new PooledBufferStream();
68+
#else
69+
var compressedStream = new MemoryStream();
70+
#endif
71+
72+
using (var gzipStream = new GZipStream(compressedStream, CompressionLevel.Fastest, leaveOpen: true))
73+
{
74+
gzipStream.Write(buffer, 0, contentLength);
75+
}
76+
77+
compressedStream.Position = 0;
78+
79+
OpenTelemetryProtocolExporterEventSource.Log.CompressedHttpPayload("gzip", contentLength, compressedStream.Length);
80+
81+
var content = new StreamContent(compressedStream);
82+
83+
content.Headers.ContentType = this.MediaTypeHeader;
84+
content.Headers.Add("Content-Encoding", "gzip");
85+
86+
return content;
87+
}
5788
}

0 commit comments

Comments
 (0)