Skip to content
Merged
7 changes: 7 additions & 0 deletions src/OpenTelemetry.Exporter.Geneva/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## Unreleased

* **Experimental**: Added an option for configuring a custom string size limit in
the MessagePack serializer to improve ETW buffer space efficiency. The maximum
string length, in characters, can be set using the
`PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount=<CharCount>`
connection string parameter.
([#2813](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/2813))

## 1.12.0

Released 2025-May-06
Expand Down
1 change: 1 addition & 0 deletions src/OpenTelemetry.Exporter.Geneva/GenevaLogExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public GenevaLogExporter(GenevaExporterOptions options)

if (useMsgPackExporter)
{
MessagePackSerializer.StringSizeLimitCharCount = connectionStringBuilder.PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount;
var msgPackLogExporter = new MsgPackLogExporter(options);
this.IsUsingUnixDomainSocket = msgPackLogExporter.IsUsingUnixDomainSocket;
this.exportLogRecord = msgPackLogExporter.Export;
Expand Down
1 change: 1 addition & 0 deletions src/OpenTelemetry.Exporter.Geneva/GenevaTraceExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public GenevaTraceExporter(GenevaExporterOptions options)

if (useMsgPackExporter)
{
MessagePackSerializer.StringSizeLimitCharCount = connectionStringBuilder.PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount;
var msgPackTraceExporter = new MsgPackTraceExporter(options);
this.IsUsingUnixDomainSocket = msgPackTraceExporter.IsUsingUnixDomainSocket;
this.exportActivity = msgPackTraceExporter.Export;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using OpenTelemetry.Exporter.Geneva.MsgPack;
using OpenTelemetry.Exporter.Geneva.Transports;
using OpenTelemetry.Internal;

Expand Down Expand Up @@ -81,6 +82,9 @@ public string EtwSession
public bool PrivatePreviewEnableAFDCorrelationIdEnrichment => this.parts.TryGetValue(nameof(this.PrivatePreviewEnableAFDCorrelationIdEnrichment), out var value)
&& bool.TrueString.Equals(value, StringComparison.OrdinalIgnoreCase);

public int PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount => this.parts.TryGetValue(nameof(this.PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount), out var value)
&& int.TryParse(value, out var intValue) ? intValue : MessagePackSerializer.DEFAULT_STRING_SIZE_LIMIT_CHAR_COUNT;

public string Endpoint
{
get => this.ThrowIfNotExists<string>(nameof(this.Endpoint));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,35 @@ internal static class MessagePackSerializer
public const byte MAP32 = 0xDF;
public const byte EXT_DATE_TIME = 0xFF;

internal const int DEFAULT_STRING_SIZE_LIMIT_CHAR_COUNT = (1 << 14) - 1; // 16 * 1024 - 1 = 16383

private const int LIMIT_MIN_FIX_NEGATIVE_INT = -32;
private const int LIMIT_MAX_FIX_STRING_LENGTH_IN_BYTES = 31;
private const int LIMIT_MAX_STR8_LENGTH_IN_BYTES = (1 << 8) - 1; // str8 stores 2^8 - 1 bytes
private const int LIMIT_MAX_FIX_MAP_COUNT = 15;
private const int LIMIT_MAX_FIX_ARRAY_LENGTH = 15;
private const int STRING_SIZE_LIMIT_CHAR_COUNT = (1 << 14) - 1; // 16 * 1024 - 1 = 16383

#if NET
private const int MAX_STACK_ALLOC_SIZE_IN_BYTES = 256;
#endif

private const int MAX_ETW_PAYLOAD = 65360; // the maximum ETW payload (inclusive)
private static int sStringSizeLimitCharCount = DEFAULT_STRING_SIZE_LIMIT_CHAR_COUNT; // custom size limit for strings

internal static int StringSizeLimitCharCount
{
get => sStringSizeLimitCharCount;
set
{
if (value < 0 || value > MAX_ETW_PAYLOAD)
{
throw new ArgumentOutOfRangeException(nameof(value), $"String size limit must be between 0 and {MAX_ETW_PAYLOAD} characters.");
}

sStringSizeLimitCharCount = value;
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int SerializeNull(byte[] buffer, int cursor)
{
Expand Down Expand Up @@ -375,14 +393,14 @@ public static int SerializeAsciiString(byte[] buffer, int cursor, string? value)
}

cursor += 3;
if (cch <= STRING_SIZE_LIMIT_CHAR_COUNT)
if (cch <= sStringSizeLimitCharCount)
{
cb = Encoding.ASCII.GetBytes(value, 0, cch, buffer, cursor);
cursor += cb;
}
else
{
cb = Encoding.ASCII.GetBytes(value, 0, STRING_SIZE_LIMIT_CHAR_COUNT - 3, buffer, cursor);
cb = Encoding.ASCII.GetBytes(value, 0, sStringSizeLimitCharCount - 3, buffer, cursor);
cursor += cb;
cb += 3;

Expand Down Expand Up @@ -413,14 +431,14 @@ public static int SerializeUnicodeString(byte[] buffer, int cursor, ReadOnlySpan
var cch = value.Length;
int cb;
cursor += 3;
if (cch <= STRING_SIZE_LIMIT_CHAR_COUNT)
if (cch <= sStringSizeLimitCharCount)
{
cb = Encoding.UTF8.GetBytes(value.Slice(0, cch), buffer.AsSpan(cursor));
cursor += cb;
}
else
{
cb = Encoding.UTF8.GetBytes(value.Slice(0, STRING_SIZE_LIMIT_CHAR_COUNT - 3), buffer.AsSpan(cursor));
cb = Encoding.UTF8.GetBytes(value.Slice(0, sStringSizeLimitCharCount - 3), buffer.AsSpan(cursor));
cursor += cb;
cb += 3;

Expand Down Expand Up @@ -450,14 +468,14 @@ public static int SerializeUnicodeString(byte[] buffer, int cursor, string? valu
var cch = value.Length;
int cb;
cursor += 3;
if (cch <= STRING_SIZE_LIMIT_CHAR_COUNT)
if (cch <= sStringSizeLimitCharCount)
{
cb = Encoding.UTF8.GetBytes(value, 0, cch, buffer, cursor);
cursor += cb;
}
else
{
cb = Encoding.UTF8.GetBytes(value, 0, STRING_SIZE_LIMIT_CHAR_COUNT - 3, buffer, cursor);
cb = Encoding.UTF8.GetBytes(value, 0, sStringSizeLimitCharCount - 3, buffer, cursor);
cursor += cb;
cb += 3;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,15 @@ public void ConnectionStringBuilder_PrivatePreviewEnableUserEvents_No_Default_Va
builder = new ConnectionStringBuilder("PrivatePreviewEnableAFDCorrelationIdEnrichment=false");
Assert.False(builder.PrivatePreviewEnableAFDCorrelationIdEnrichment);
}

[Fact]
public void ConnectionStringBuilder_PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount_No_Default_Value()
{
var builder = new ConnectionStringBuilder("key1=value1");
Assert.Equal(16383, builder.PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount);
builder = new ConnectionStringBuilder("PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount=1024");
Assert.Equal(1024, builder.PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount);
builder = new ConnectionStringBuilder("PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount=32767");
Assert.Equal(32767, builder.PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount);
}
}
27 changes: 27 additions & 0 deletions test/OpenTelemetry.Exporter.Geneva.Tests/GenevaLogExporterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,33 @@ public void IncompatibleConnectionString_Linux()
Assert.Equal("ETW cannot be used on non-Windows operating systems.", exception.Message);
}

[SkipUnlessPlatformMatchesFact(TestPlatform.Windows)]
public void ConnectionString_CustomStringSizeLimit()
{
var exporterOptions = new GenevaExporterOptions() { ConnectionString = "EtwSession=OpenTelemetry" };
using var exporter1 = new GenevaLogExporter(exporterOptions);
var defaultStringSizeLimit = (1 << 14) - 1;
Assert.Equal(defaultStringSizeLimit, MessagePackSerializer.StringSizeLimitCharCount);

var exporterOptions2 = new GenevaExporterOptions() { ConnectionString = "EtwSession=OpenTelemetry;PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount=65360" };
using var exporter2 = new GenevaLogExporter(exporterOptions2);
Assert.Equal(65360, MessagePackSerializer.StringSizeLimitCharCount);

var exporterOptions3 = new GenevaExporterOptions() { ConnectionString = "EtwSession=OpenTelemetry;PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount=-1" };
var exception3 = Assert.Throws<ArgumentOutOfRangeException>(() =>
{
using var exporter3 = new GenevaLogExporter(exporterOptions3);
});
Assert.StartsWith("String size limit must be between 0 and 65360 characters.", exception3.Message);

var exporterOptions4 = new GenevaExporterOptions() { ConnectionString = "EtwSession=OpenTelemetry;PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount=65365" };
var exception4 = Assert.Throws<ArgumentOutOfRangeException>(() =>
{
using var exporter4 = new GenevaLogExporter(exporterOptions4);
});
Assert.StartsWith("String size limit must be between 0 and 65360 characters.", exception4.Message);
}

[Theory]
[InlineData("categoryA", "TableA")]
[InlineData("categoryB", "TableB")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,33 @@ public void GenevaTraceExporter_TableNameMappings_SpecialCharacters()
});
}

[SkipUnlessPlatformMatchesFact(TestPlatform.Windows)]
public void ConnectionString_CustomStringSizeLimit()
{
var exporterOptions = new GenevaExporterOptions() { ConnectionString = "EtwSession=OpenTelemetry" };
using var exporter1 = new GenevaTraceExporter(exporterOptions);
var defaultStringSizeLimit = (1 << 14) - 1;
Assert.Equal(defaultStringSizeLimit, MessagePackSerializer.StringSizeLimitCharCount);

var exporterOptions2 = new GenevaExporterOptions() { ConnectionString = "EtwSession=OpenTelemetry;PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount=65360" };
using var exporter2 = new GenevaTraceExporter(exporterOptions2);
Assert.Equal(65360, MessagePackSerializer.StringSizeLimitCharCount);

var exporterOptions3 = new GenevaExporterOptions() { ConnectionString = "EtwSession=OpenTelemetry;PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount=-1" };
var exception3 = Assert.Throws<ArgumentOutOfRangeException>(() =>
{
using var exporter3 = new GenevaTraceExporter(exporterOptions3);
});
Assert.StartsWith("String size limit must be between 0 and 65360 characters.", exception3.Message);

var exporterOptions4 = new GenevaExporterOptions() { ConnectionString = "EtwSession=OpenTelemetry;PrivatePreviewCustomMessagePackStringSizeLimitCharacterCount=65365" };
var exception4 = Assert.Throws<ArgumentOutOfRangeException>(() =>
{
using var exporter4 = new GenevaTraceExporter(exporterOptions4);
});
Assert.StartsWith("String size limit must be between 0 and 65360 characters.", exception4.Message);
}

[SkipUnlessPlatformMatchesFact(TestPlatform.Windows)]
public void GenevaTraceExporter_Success_Windows()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,16 @@ private void MessagePackSerializer_TestSerialization<T>(T value)
this.AssertBytes(MessagePack.MessagePackSerializer.Serialize(value), buffer, length);
}

private void MessagePackSerializer_TestASCIIStringSerialization(string input)
private void MessagePackSerializer_TestASCIIStringSerialization(string input, int sizeLimit = (1 << 14) - 1)
{
var sizeLimit = (1 << 14) - 1; // // Max length of string allowed
// sizeLimit is the max length of string allowed
var buffer = new byte[64 * 1024];
MessagePackSerializer.StringSizeLimitCharCount = sizeLimit;
var length = MessagePackSerializer.SerializeAsciiString(buffer, 0, input);
var deserializedString = MessagePack.MessagePackSerializer.Deserialize<string>(buffer);
if (!string.IsNullOrEmpty(input) && input.Length > sizeLimit)
{
// We truncate the string using `.` in the last three characters which takes 3 bytes of memort
// We truncate the string using `.` in the last three characters which takes 3 bytes of memory
#pragma warning disable CA1846 // Prefer 'AsSpan' over 'Substring'
var byteCount = Encoding.ASCII.GetByteCount(input.Substring(0, sizeLimit - 3)) + 3;
#pragma warning restore CA1846 // Prefer 'AsSpan' over 'Substring'
Expand Down Expand Up @@ -88,10 +89,12 @@ private void MessagePackSerializer_TestASCIIStringSerialization(string input)
}
}

private void MessagePackSerializer_TestUnicodeStringSerialization(string input)
private void MessagePackSerializer_TestUnicodeStringSerialization(string input, int sizeLimit = (1 << 14) - 1)
{
var sizeLimit = (1 << 14) - 1; // // Max length of string allowed
// sizeLimit is the max length of string allowed
// var sizeLimit = (1 << 14) - 1; // // Max length of string allowed
var buffer = new byte[64 * 1024];
MessagePackSerializer.StringSizeLimitCharCount = sizeLimit;
var length = MessagePackSerializer.SerializeUnicodeString(buffer, 0, input);

var deserializedString = MessagePack.MessagePackSerializer.Deserialize<string>(buffer);
Expand Down Expand Up @@ -292,6 +295,9 @@ public void MessagePackSerializer_SerializeAsciiString()
this.MessagePackSerializer_TestASCIIStringSerialization(new string('Z', (1 << 14) - 1));
this.MessagePackSerializer_TestASCIIStringSerialization(new string('Z', 1 << 14));

this.MessagePackSerializer_TestASCIIStringSerialization(new string('Z', (1 << 15) - 1));
this.MessagePackSerializer_TestASCIIStringSerialization(new string('Z', 1 << 15));

// Unicode special characters
// SerializeAsciiString will encode non-ASCII characters with '?'
Assert.Throws<EqualException>(() => this.MessagePackSerializer_TestASCIIStringSerialization("\u0418"));
Expand Down Expand Up @@ -328,6 +334,9 @@ public void MessagePackSerializer_SerializeUnicodeString()
this.MessagePackSerializer_TestUnicodeStringSerialization(new string('\u0418', (1 << 14) - 1));
this.MessagePackSerializer_TestUnicodeStringSerialization(new string('\u0418', 1 << 14));

this.MessagePackSerializer_TestUnicodeStringSerialization(new string('\u0418', (1 << 15) - 1));
this.MessagePackSerializer_TestUnicodeStringSerialization(new string('\u0418', 1 << 15));

// Unicode regular and special characters
this.MessagePackSerializer_TestUnicodeStringSerialization("\u0418TestString");
this.MessagePackSerializer_TestUnicodeStringSerialization("TestString\u0418");
Expand Down
Loading