Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 @@ -37,6 +37,10 @@ Notes](../../RELEASENOTES.md).
* Do not enable the integration with `IHttpClientFactory` when mTLS is enabled.
([#7305](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7305))

* Cached pre-serialized metric metadata (`Name` / `Description` / `Unit`) to avoid
re-encoding on every OTLP metric export.
([#7307](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7307))

## 1.15.3

Released 2026-Apr-21
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using OpenTelemetry.Metrics;

namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer;
Expand All @@ -11,6 +13,10 @@ internal static class ProtobufOtlpMetricSerializer
private const int ReserveSizeForLength = 4;
private const int TraceIdSize = 16;
private const int SpanIdSize = 8;
private const int StackallocByteThreshold = 256;
private const int InitialPoolBufferSize = 512;

private static readonly ConditionalWeakTable<Metric, byte[]> CachedMetricMetadata = new();

[ThreadStatic]
private static Stack<List<Metric>>? metricListPool;
Expand Down Expand Up @@ -172,17 +178,9 @@ private static int WriteMetric(byte[] buffer, int writePosition, Metric metric)
var metricLengthPosition = writePosition;
writePosition += ReserveSizeForLength;

writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Name, metric.Name);

if (metric.Description != null)
{
writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Description, metric.Description);
}

if (metric.Unit != null)
{
writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Unit, metric.Unit);
}
var cachedMetadata = CachedMetricMetadata.GetOrAdd(metric, SerializeMetricMetadataToBytes);
Buffer.BlockCopy(cachedMetadata, 0, buffer, writePosition, cachedMetadata.Length);
writePosition += cachedMetadata.Length;

var aggregationValue = metric.Temporality == AggregationTemporality.Cumulative
? ProtobufOtlpMetricFieldNumberConstants.Aggregation_Temporality_Cumulative
Expand Down Expand Up @@ -592,4 +590,62 @@ static int WritePackedLength(byte[] buffer, int writePosition, int length, int f
ProtobufWireType.LEN);
}
}

private static byte[] SerializeMetricMetadataToBytes(Metric metric)
{
Span<byte> stackBuffer = stackalloc byte[StackallocByteThreshold];
try
{
var length = WriteMetricMetadataCore(stackBuffer, 0, metric);
return stackBuffer.Slice(0, length).ToArray();
}
catch (Exception ex) when (ex is IndexOutOfRangeException or ArgumentException)
{
return SerializeMetricMetadataToBytesFromPool(metric);
}
}

private static byte[] SerializeMetricMetadataToBytesFromPool(Metric metric)
{
var pool = ArrayPool<byte>.Shared;

var buffer = pool.Rent(InitialPoolBufferSize);
try
{
while (true)
{
try
{
var length = WriteMetricMetadataCore(buffer, 0, metric);
return buffer.AsSpan(0, length).ToArray();
}
catch (Exception ex) when (ex is IndexOutOfRangeException or ArgumentException)
{
pool.Return(buffer);
buffer = pool.Rent(buffer.Length * 2);
}
}
}
finally
{
pool.Return(buffer);
}
}

private static int WriteMetricMetadataCore(Span<byte> buffer, int writePosition, Metric metric)
{
writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Name, metric.Name);

if (metric.Description != null)
{
writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Description, metric.Description);
}

if (metric.Unit != null)
{
writePosition = ProtobufSerializer.WriteStringWithTag(buffer, writePosition, ProtobufOtlpMetricFieldNumberConstants.Metric_Unit, metric.Unit);
}

return writePosition;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,7 @@ internal static int WriteResource(byte[] buffer, int writePosition, Resource? re
return writePosition + EmptyResourceBytes.Length;
}

#if NET10_0_OR_GREATER
var cached = CachedResourceBytes.GetOrAdd(resource, SerializeResourceToBytes);
#else
var cached = CachedResourceBytes.GetValue(resource, SerializeResourceToBytes);
#endif

Buffer.BlockCopy(cached, 0, buffer, writePosition, cached.Length);
return writePosition + cached.Length;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.CompilerServices;
#if NETFRAMEWORK || NETSTANDARD2_0
using System.Runtime.InteropServices;
#endif
using System.Text;

namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer;
Expand Down Expand Up @@ -257,19 +254,7 @@ internal static int WriteStringWithTag(byte[] buffer, int writePosition, int fie
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int GetNumberOfUtf8CharsInString(ReadOnlySpan<char> value)
{
#if NETFRAMEWORK || NETSTANDARD2_0
int numberOfUtf8CharsInString;
unsafe
{
fixed (char* strPtr = &GetNonNullPinnableReference(value))
{
numberOfUtf8CharsInString = Utf8Encoding.GetByteCount(strPtr, value.Length);
}
}
#else
var numberOfUtf8CharsInString = Utf8Encoding.GetByteCount(value);
#endif
return numberOfUtf8CharsInString;
return Utf8Encoding.GetByteCount(value);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand All @@ -285,31 +270,55 @@ internal static int WriteStringWithTag(byte[] buffer, int writePosition, int fie
writePosition = WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.LEN);
writePosition = WriteLength(buffer, writePosition, numberOfUtf8CharsInString);

#if NETFRAMEWORK || NETSTANDARD2_0
if (buffer.Length - writePosition < numberOfUtf8CharsInString)
{
// Note: Validate there is enough space in the buffer to hold the
// string otherwise throw to trigger a resize of the buffer.
#pragma warning disable CA2201 // Do not raise reserved exception types
throw new IndexOutOfRangeException();
#pragma warning restore CA2201 // Do not raise reserved exception types
}
var bytesWritten = Utf8Encoding.GetBytes(value, buffer.AsSpan(writePosition));
Debug.Assert(bytesWritten == numberOfUtf8CharsInString, "bytesWritten did not match numberOfUtf8CharsInString");

unsafe
writePosition += numberOfUtf8CharsInString;
return writePosition;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int WriteVarInt32(Span<byte> buffer, int writePosition, uint value)
{
while (value >= UInt128)
{
fixed (char* strPtr = &GetNonNullPinnableReference(value))
{
fixed (byte* bufferPtr = buffer)
{
var bytesWritten = Utf8Encoding.GetBytes(strPtr, value.Length, bufferPtr + writePosition, numberOfUtf8CharsInString);
Debug.Assert(bytesWritten == numberOfUtf8CharsInString, "bytesWritten did not match numberOfUtf8CharsInString");
}
}
buffer[writePosition++] = (byte)(MaskBitHigh | (value & MaskBitsLow));
value >>= 7;
}
#else
var bytesWritten = Utf8Encoding.GetBytes(value, buffer.AsSpan(writePosition));

buffer[writePosition++] = (byte)value;
return writePosition;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int WriteTag(Span<byte> buffer, int writePosition, int fieldNumber, ProtobufWireType type) => WriteVarInt32(buffer, writePosition, GetTagValue(fieldNumber, type));

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int WriteLength(Span<byte> buffer, int writePosition, int length) => WriteVarInt32(buffer, writePosition, (uint)length);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int WriteStringWithTag(Span<byte> buffer, int writePosition, int fieldNumber, string value)
{
Debug.Assert(value != null, "value was null");

return WriteStringWithTag(buffer, writePosition, fieldNumber, value.AsSpan());
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int WriteStringWithTag(Span<byte> buffer, int writePosition, int fieldNumber, ReadOnlySpan<char> value)
{
var numberOfUtf8CharsInString = GetNumberOfUtf8CharsInString(value);
return WriteStringWithTag(buffer, writePosition, fieldNumber, numberOfUtf8CharsInString, value);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int WriteStringWithTag(Span<byte> buffer, int writePosition, int fieldNumber, int numberOfUtf8CharsInString, ReadOnlySpan<char> value)
{
writePosition = WriteTag(buffer, writePosition, fieldNumber, ProtobufWireType.LEN);
writePosition = WriteLength(buffer, writePosition, numberOfUtf8CharsInString);

var bytesWritten = Utf8Encoding.GetBytes(value, buffer.Slice(writePosition));
Debug.Assert(bytesWritten == numberOfUtf8CharsInString, "bytesWritten did not match numberOfUtf8CharsInString");
#endif

writePosition += numberOfUtf8CharsInString;
return writePosition;
Expand All @@ -335,10 +344,4 @@ internal static bool IncreaseBufferSize(ref byte[] buffer, OtlpSignalType otlpSi
return false;
}
}

#if NETFRAMEWORK || NETSTANDARD2_0
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe ref T GetNonNullPinnableReference<T>(ReadOnlySpan<T> span)
=> ref (span.Length != 0) ? ref Unsafe.AsRef(in MemoryMarshal.GetReference(span)) : ref Unsafe.AsRef<T>((void*)1);
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
</ItemGroup>

<ItemGroup>
<Compile Include="$(RepoRoot)\src\Shared\ConditionalWeakTableExtensions.cs" Link="Includes\ConditionalWeakTableExtensions.cs" />
<Compile Include="$(RepoRoot)\src\Shared\DynamicallyAccessedMembersAttribute.cs" Link="Includes\DynamicallyAccessedMembersAttribute.cs" />
<Compile Include="$(RepoRoot)\src\Shared\DynamicallyAccessedMemberTypes.cs" Link="Includes\DynamicallyAccessedMemberTypes.cs" />
<Compile Include="$(RepoRoot)\src\Shared\EncodingExtensions.cs" Link="Includes\EncodingExtensions.cs" />
<Compile Include="$(RepoRoot)\src\Shared\HttpClientHelpers.cs" Link="Includes\HttpClientHelpers.cs" />
<Compile Include="$(RepoRoot)\src\Shared\PeriodicExportingMetricReaderHelper.cs" Link="Includes\PeriodicExportingMetricReaderHelper.cs" />
<Compile Include="$(RepoRoot)\src\Shared\TagWriter\ArrayTagWriter.cs" Link="Includes\TagWriter\ArrayTagWriter.cs" />
Expand Down
26 changes: 26 additions & 0 deletions src/Shared/ConditionalWeakTableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

#if !NET10_0_OR_GREATER

using System.Diagnostics.CodeAnalysis;

namespace System.Runtime.CompilerServices;

/// <summary>
/// Polyfills the GetOrAdd APIs added to <see cref="ConditionalWeakTable{TKey, TValue}"/> in .NET 10.
/// See https://github.com/dotnet/runtime/pull/111204.
/// </summary>
internal static class ConditionalWeakTableExtensions
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TValue GetOrAdd<TKey, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TValue>(
this ConditionalWeakTable<TKey, TValue> table,
TKey key,
ConditionalWeakTable<TKey, TValue>.CreateValueCallback valueFactory)
where TKey : class
where TValue : class
=> table.GetValue(key, valueFactory);
}

#endif
102 changes: 102 additions & 0 deletions src/Shared/DynamicallyAccessedMemberTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// <auto-generated/>
#pragma warning disable
#nullable enable annotations

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#if !NET

namespace System.Diagnostics.CodeAnalysis;

/// <summary>
/// Specifies the types of members that are dynamically accessed.
///
/// This enumeration has a <see cref="global::System.FlagsAttribute"/> attribute that allows a
/// bitwise combination of its member values.
/// </summary>
[global::System.Flags]
internal enum DynamicallyAccessedMemberTypes
{
/// <summary>
/// Specifies no members.
/// </summary>
None = 0,

/// <summary>
/// Specifies the default, parameterless public constructor.
/// </summary>
PublicParameterlessConstructor = 0x0001,

/// <summary>
/// Specifies all public constructors.
/// </summary>
PublicConstructors = 0x0002 | global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor,

/// <summary>
/// Specifies all non-public constructors.
/// </summary>
NonPublicConstructors = 0x0004,

/// <summary>
/// Specifies all public methods.
/// </summary>
PublicMethods = 0x0008,

/// <summary>
/// Specifies all non-public methods.
/// </summary>
NonPublicMethods = 0x0010,

/// <summary>
/// Specifies all public fields.
/// </summary>
PublicFields = 0x0020,

/// <summary>
/// Specifies all non-public fields.
/// </summary>
NonPublicFields = 0x0040,

/// <summary>
/// Specifies all public nested types.
/// </summary>
PublicNestedTypes = 0x0080,

/// <summary>
/// Specifies all non-public nested types.
/// </summary>
NonPublicNestedTypes = 0x0100,

/// <summary>
/// Specifies all public properties.
/// </summary>
PublicProperties = 0x0200,

/// <summary>
/// Specifies all non-public properties.
/// </summary>
NonPublicProperties = 0x0400,

/// <summary>
/// Specifies all public events.
/// </summary>
PublicEvents = 0x0800,

/// <summary>
/// Specifies all non-public events.
/// </summary>
NonPublicEvents = 0x1000,

/// <summary>
/// Specifies all interfaces implemented by the type.
/// </summary>
Interfaces = 0x2000,

/// <summary>
/// Specifies all members.
/// </summary>
All = ~global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None
}

#endif
Loading
Loading