Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ Notes](../../RELEASENOTES.md).

## Unreleased

* Limit how much of the response body is read when export fails and
error logging is enabled.
([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet/pull/TODO))
Comment thread
martincostello marked this conversation as resolved.
Outdated

## 1.15.1

Released 2026-Mar-27
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public bool Shutdown(int timeoutMilliseconds)
return true;
}

protected static string? TryGetResponseBody(HttpResponseMessage? httpResponse, CancellationToken cancellationToken)
protected internal static string? TryGetResponseBody(HttpResponseMessage? httpResponse, CancellationToken cancellationToken)
{
if (httpResponse?.Content == null)
{
Expand All @@ -76,16 +76,40 @@ public bool Shutdown(int timeoutMilliseconds)
{
#if NET
var stream = httpResponse.Content.ReadAsStream(cancellationToken);
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
#else
return httpResponse.Content.ReadAsStringAsync().GetAwaiter().GetResult();
var stream = httpResponse.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
#endif

using var reader = new StreamReader(stream);

Comment thread
martincostello marked this conversation as resolved.
Outdated
int length = GetSafeStreamLength(stream);
var buffer = new char[length];

length = reader.Read(buffer, 0, buffer.Length);

return new(buffer, 0, length);
Comment thread
martincostello marked this conversation as resolved.
Outdated
}
catch (Exception)
{
return null;
}

static int GetSafeStreamLength(Stream stream)
{
// See https://github.com/open-telemetry/opentelemetry-proto/pull/781
const int MessageSizeLimit = 32 * 1024;

try
{
// Avoid allocating an overly large buffer if the stream is smaller than the size limit
return stream.Length < MessageSizeLimit ? (int)stream.Length : MessageSizeLimit;
}
Comment thread
martincostello marked this conversation as resolved.
Outdated
catch (Exception)
{
// Not all Stream types support Length, so default to the maximum
return MessageSizeLimit;
}
}
}

protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength)
Expand Down Expand Up @@ -114,16 +138,14 @@ protected HttpRequestMessage CreateHttpRequest(byte[] buffer, int contentLength)
return request;
}

protected HttpResponseMessage SendHttpRequest(HttpRequestMessage request, CancellationToken cancellationToken)
{
protected HttpResponseMessage SendHttpRequest(HttpRequestMessage request, CancellationToken cancellationToken) =>
#if NET
// Note: SendAsync must be used with HTTP/2 because synchronous send is
// not supported.
return this.RequireHttp2 || !SynchronousSendSupportedByCurrentPlatform
this.RequireHttp2 || !SynchronousSendSupportedByCurrentPlatform
? this.HttpClient.SendAsync(request, cancellationToken).GetAwaiter().GetResult()
: this.HttpClient.Send(request, cancellationToken);
#else
return this.HttpClient.SendAsync(request, cancellationToken).GetAwaiter().GetResult();
this.HttpClient.SendAsync(request, cancellationToken).GetAwaiter().GetResult();
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#if NETFRAMEWORK
using System.Net.Http;
#endif
using System.Diagnostics.Tracing;
using System.Net.Http.Headers;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient.Grpc;

Expand Down Expand Up @@ -123,8 +124,12 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten
catch (HttpRequestException ex)
{
// Handle non-retryable HTTP errors.
var response = TryGetResponseBody(httpResponse, cancellationToken);
OpenTelemetryProtocolExporterEventSource.Log.HttpRequestFailed(this.Endpoint, response, ex);
if (OpenTelemetryProtocolExporterEventSource.Log.IsEnabled(EventLevel.Error, EventKeywords.All))
{
var response = TryGetResponseBody(httpResponse, cancellationToken);
OpenTelemetryProtocolExporterEventSource.Log.HttpRequestFailed(this.Endpoint, response, ex);
}

return new ExportClientGrpcResponse(
success: false,
deadlineUtc: deadlineUtc,
Expand Down Expand Up @@ -165,12 +170,10 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten
}
}

private static bool IsTransientNetworkError(HttpRequestException ex)
{
return ex.InnerException is System.Net.Sockets.SocketException socketEx
&& (socketEx.SocketErrorCode == System.Net.Sockets.SocketError.TimedOut
|| socketEx.SocketErrorCode == System.Net.Sockets.SocketError.ConnectionReset
|| socketEx.SocketErrorCode == System.Net.Sockets.SocketError.HostUnreachable
|| socketEx.SocketErrorCode == System.Net.Sockets.SocketError.ConnectionRefused);
}
private static bool IsTransientNetworkError(HttpRequestException ex) =>
ex.InnerException is System.Net.Sockets.SocketException socketEx
&& (socketEx.SocketErrorCode == System.Net.Sockets.SocketError.TimedOut
|| socketEx.SocketErrorCode == System.Net.Sockets.SocketError.ConnectionReset
|| socketEx.SocketErrorCode == System.Net.Sockets.SocketError.HostUnreachable
|| socketEx.SocketErrorCode == System.Net.Sockets.SocketError.ConnectionRefused);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#if NETFRAMEWORK
using System.Net.Http;
#endif
using System.Diagnostics.Tracing;
using System.Net.Http.Headers;

namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;
Expand Down Expand Up @@ -35,8 +36,12 @@ public override ExportClientResponse SendExportRequest(byte[] buffer, int conten
}
catch (HttpRequestException ex)
{
var response = TryGetResponseBody(httpResponse, cancellationToken);
OpenTelemetryProtocolExporterEventSource.Log.HttpRequestFailed(this.Endpoint, response, ex);
if (OpenTelemetryProtocolExporterEventSource.Log.IsEnabled(EventLevel.Error, EventKeywords.All))
{
var response = TryGetResponseBody(httpResponse, cancellationToken);
OpenTelemetryProtocolExporterEventSource.Log.HttpRequestFailed(this.Endpoint, response, ex);
}

return new ExportClientHttpResponse(success: false, deadlineUtc: deadlineUtc, response: httpResponse, ex);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

#if NETFRAMEWORK
using System.Net.Http;
#endif
using System.Net;
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient;

namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests;

public class OtlpExportClientTests
{
private const int MessageSizeLimit = 32 * 1024;

[Fact]
public void TryGetResponseBody_NullHttpResponse_ReturnsNull()
{
// Arrange
HttpResponseMessage? httpResponse = null;
var cancellationToken = CancellationToken.None;

// Act
var actual = OtlpExportClient.TryGetResponseBody(httpResponse, cancellationToken);

// Assert
Assert.Null(actual);
}

[Fact]
public void TryGetResponseBody_HttpResponseWithoutContent_ReturnsNull()
{
// Arrange
using var httpResponse = new HttpResponseMessage() { Content = null };
var cancellationToken = CancellationToken.None;

// Act
var actual = OtlpExportClient.TryGetResponseBody(httpResponse, cancellationToken);

// Assert
#if NETFRAMEWORK
Assert.Null(actual);
#else
Assert.Equal(string.Empty, actual);
#endif
}
Comment thread
martincostello marked this conversation as resolved.

[Theory]
[InlineData(0)]
[InlineData(1)]
[InlineData(256)]
[InlineData(1024)]
[InlineData((30 * 1024) - 1)]
[InlineData(30 * 1024)]
public void TryGetResponseBody_SmallContent_ReturnsFullContent(int length)
{
// Arrange
var expected = new string('A', length);
var cancellationToken = CancellationToken.None;

using var httpResponse = new HttpResponseMessage()
{
Content = new StringContent(expected),
};

// Act
var actual = OtlpExportClient.TryGetResponseBody(httpResponse, cancellationToken);

// Assert
Assert.Equal(expected, actual);
}

[Theory]
[InlineData(1)]
[InlineData(1024)]
[InlineData(2048)]
public void TryGetResponseBody_ContentExceedsLimit_ReturnsTruncatedContent(int excess)
{
// Arrange
var content = new string('C', MessageSizeLimit + excess);
var cancellationToken = CancellationToken.None;

using var httpResponse = new HttpResponseMessage
{
Content = new StringContent(content),
};

// Act
var actual = OtlpExportClient.TryGetResponseBody(httpResponse, cancellationToken);

// Assert
Assert.NotNull(actual);
Assert.Equal(MessageSizeLimit, actual.Length);
Assert.Equal(new string('C', MessageSizeLimit), actual);
}

Comment thread
martincostello marked this conversation as resolved.
[Fact]
public void TryGetResponseBody_EmptyContent_ReturnsEmptyString()
{
// Arrange
var cancellationToken = CancellationToken.None;

using var httpResponse = new HttpResponseMessage
{
Content = new StringContent(string.Empty),
};

// Act
var actual = OtlpExportClient.TryGetResponseBody(httpResponse, cancellationToken);

// Assert
Assert.Equal(string.Empty, actual);
}

[Fact]
public void TryGetResponseBody_ExceptionDuringRead_ReturnsNull()
{
// Arrange
var cancellationToken = CancellationToken.None;

using var httpResponse = new HttpResponseMessage
{
Content = new ThrowingHttpContent(),
};

// Act
var actual = OtlpExportClient.TryGetResponseBody(httpResponse, cancellationToken);

// Assert
Assert.Null(actual);
}

private sealed class ThrowingHttpContent : HttpContent
{
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context)
=> throw new InvalidOperationException("Test exception");

#if NET
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken)
=> throw new InvalidOperationException("Test exception");
#endif

protected override bool TryComputeLength(out long length)
{
length = 0;
return false;
}
}
}
Loading