Skip to content
Open
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
3 changes: 3 additions & 0 deletions src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Log error when a metric's dimensions exceed the serialization buffer length.
([#TODO](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/TODO))

* Updated OpenTelemetry core component version(s) to `1.15.1`.
([#4020](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4020))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal sealed class ExporterEventSource : EventSource
private const int EVENT_ID_TRANSPORT_EXCEPTION = 8; // Transport exception
private const int EVENT_ID_TRANSPORT_INFO = 9; // Transport info
private const int EVENT_ID_AFD_CORRELATION_ID = 10; // Failed to get AFD correlation ID
private const int EVENT_ID_METRIC_BUFFER_OVERFLOW = 11; // Metric serialization buffer overflow

[NonEvent]
public void FailedToSendTraceData(Exception ex)
Expand Down Expand Up @@ -157,4 +158,10 @@ public void FailedToGetAFDCorrelationId(string error)
{
this.WriteEvent(EVENT_ID_AFD_CORRELATION_ID, error);
}

[Event(EVENT_ID_METRIC_BUFFER_OVERFLOW, Message = "Failed to export '{0}' metric: the {1}-byte serialization buffer was exceeded. Reduce the number or size of metric dimensions.", Level = EventLevel.Error)]
public void MetricSerializationBufferFull(string metricName, int bufferSizeBytes)
{
this.WriteEvent(EVENT_ID_METRIC_BUFFER_OVERFLOW, metricName, bufferSizeBytes);
}
}
14 changes: 14 additions & 0 deletions src/OpenTelemetry.Exporter.Geneva/Metrics/TlvMetricExporter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Diagnostics.Tracing;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
Expand Down Expand Up @@ -243,6 +244,19 @@ internal ExportResult Export(in Batch<Metric> batch)
break;
}
}
catch (Exception ex) when (ex is IndexOutOfRangeException || ex is ArgumentException)
{
// The fixed-size serialization buffer's bounds were exceeded.
// Note: SerializeByte/SerializeUInt16/etc. throw IndexOutOfRangeException when
// the buffer index exceeds the array length; Encoding.UTF8.GetBytes throws
// ArgumentException on .NET 5+ when the destination span is too small.
if (ExporterEventSource.Log.IsEnabled(EventLevel.Error, EventKeywords.All))
{
ExporterEventSource.Log.MetricSerializationBufferFull(metric.Name, GenevaMetricExporter.BufferSize);
}

result = ExportResult.Failure;
}
Comment on lines +247 to +259
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception filter catches all ArgumentException-derived exceptions, which includes ArgumentOutOfRangeException thrown by Socket.Send (and potentially other argument validation failures). That can misclassify transport/logic bugs as a "serialization buffer exceeded" condition and drops the original exception details. Consider narrowing this catch to only the specific encoding destination-too-small case (e.g., exclude ArgumentOutOfRangeException) or refactor serialization to throw a dedicated/consistent exception for buffer exhaustion and catch only that.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open to changing this, but it needs a bunch of refactoring to make the call sites that get detected for this more specific.

catch (Exception ex)
{
ExporterEventSource.Log.FailedToSendMetricData(monitoringAccount, metricNamespace, metric.Name, ex); // TODO: preallocate exception or no exception
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.Diagnostics.Tracing;
using System.Globalization;
using System.Net.Sockets;
using System.Reflection;
Expand Down Expand Up @@ -1411,5 +1412,82 @@ private static UserdataV2 GetSerializedData(Metric metric, TlvMetricExporter exp

return result;
}

[Fact]
public void TlvMetricExporter_BufferOverflow_LogsBufferFullEvent()
{
var capturedEvents = new List<EventWrittenEventArgs>();
var path = string.Empty;
Socket server = null;

using var listener = new BufferOverflowEventListener(capturedEvents);
try
Comment on lines +1419 to +1424
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EventListener callbacks can be raised on background threads while the test thread is asserting. Using a shared List here (and adding to it in OnEventWritten) is not thread-safe and can lead to intermittent failures (e.g., collection modified during enumeration). Use a thread-safe collection (ConcurrentQueue/ConcurrentBag) or protect accesses with a lock, and consider waiting until the expected event arrives before asserting.

Copilot uses AI. Check for mistakes.
{
string connectionString;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
connectionString = "Account=OTelMonitoringAccount;Namespace=OTelMetricNamespace";
}
else
{
path = GenerateTempFilePath();
connectionString = $"Endpoint=unix:{path};Account=OTelMonitoringAccount;Namespace=OTelMetricNamespace";
var endpoint = new UnixDomainSocketEndPoint(path);
server = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
server.Bind(endpoint);
server.Listen(1);
}

using var meter = new Meter("BufferOverflowTest", "0.0.1");
var counter = meter.CreateCounter<long>("overflowCounter");

using (var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter("BufferOverflowTest")
.AddGenevaMetricExporter(options => options.ConnectionString = connectionString)
.Build())
{
// Record a metric with a tag value large enough to overflow the fixed-size serialization buffer.
counter.Add(1, new KeyValuePair<string, object>("bigTag", new string('x', GenevaMetricExporter.BufferSize + 100)));
}

// Verify that the buffer overflow was logged with the dedicated event (ID 11).
Assert.Contains(capturedEvents, e => e.EventId == 11);
}
finally
{
server?.Dispose();
if (!string.IsNullOrEmpty(path))
{
try
{
File.Delete(path);
}
catch
{
}
}
}
}

private sealed class BufferOverflowEventListener : EventListener
{
private readonly List<EventWrittenEventArgs> capturedEvents;

public BufferOverflowEventListener(List<EventWrittenEventArgs> capturedEvents)
{
this.capturedEvents = capturedEvents;
}

protected override void OnEventSourceCreated(EventSource eventSource)
{
if (eventSource.Name == "OpenTelemetry-Exporter-Geneva")
{
this.EnableEvents(eventSource, EventLevel.Error, EventKeywords.All);
}
}

protected override void OnEventWritten(EventWrittenEventArgs eventData)
=> this.capturedEvents.Add(eventData);
}
}
#pragma warning restore CA1861 // // Prefer 'static readonly' fields over constant array arguments if the called method is called repeatedly and is not mutating the passed array
Loading