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
78 changes: 3 additions & 75 deletions src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

#if NET
using System.Diagnostics.CodeAnalysis;
#endif
using System.Net;
using System.Text;
using OpenTelemetry.Internal;

Expand All @@ -20,9 +16,6 @@ public class BaggagePropagator : TextMapPropagator
private const int MaxBaggageLength = 8192;
private const int MaxBaggageItems = 180;

private static readonly char[] EqualSignSeparator = ['='];
private static readonly char[] CommaSignSeparator = [','];

/// <inheritdoc/>
public override ISet<string> Fields => new HashSet<string> { BaggageHeaderName };

Expand Down Expand Up @@ -52,7 +45,7 @@ public override PropagationContext Extract<T>(PropagationContext context, T carr
var baggageCollection = getter(carrier, BaggageHeaderName);
if (baggageCollection?.Any() ?? false)
{
if (TryExtractBaggage([.. baggageCollection], out var baggage))
if (PercentEncodingHelper.TryExtractBaggage([.. baggageCollection], out var baggage))
{
return new PropagationContext(context.ActivityContext, new Baggage(baggage!));
}
Expand Down Expand Up @@ -97,77 +90,12 @@ public override void Inject<T>(PropagationContext context, T carrier, Action<T,
continue;
}

baggage.Append(WebUtility.UrlEncode(item.Key)).Append('=').Append(WebUtility.UrlEncode(item.Value)).Append(',');
baggage.Append(PercentEncodingHelper.PercentEncodeBaggage(item.Key, item.Value));
baggage.Append(',');
}
while (e.MoveNext() && ++itemCount < MaxBaggageItems && baggage.Length < MaxBaggageLength);
baggage.Remove(baggage.Length - 1, 1);
setter(carrier, BaggageHeaderName, baggage.ToString());
}
}

internal static bool TryExtractBaggage(
string[] baggageCollection,
#if NET
[NotNullWhen(true)]
#endif
out Dictionary<string, string>? baggage)
{
int baggageLength = -1;
bool done = false;
Dictionary<string, string>? baggageDictionary = null;

foreach (var item in baggageCollection)
{
if (done)
{
break;
}

if (string.IsNullOrEmpty(item))
{
continue;
}

foreach (var pair in item.Split(CommaSignSeparator))
{
baggageLength += pair.Length + 1; // pair and comma

if (baggageLength >= MaxBaggageLength || baggageDictionary?.Count >= MaxBaggageItems)
{
done = true;
break;
}

#if NET
if (pair.IndexOf('=', StringComparison.Ordinal) < 0)
#else
if (pair.IndexOf('=') < 0)
#endif
{
continue;
}

var parts = pair.Split(EqualSignSeparator, 2);
if (parts.Length != 2)
{
continue;
}

var key = WebUtility.UrlDecode(parts[0]);
var value = WebUtility.UrlDecode(parts[1]);

if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value))
{
continue;
}

baggageDictionary ??= [];

baggageDictionary[key] = value;
}
}

baggage = baggageDictionary;
return baggageDictionary != null;
}
}
1 change: 1 addition & 0 deletions src/OpenTelemetry.Api/OpenTelemetry.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<Compile Include="$(RepoRoot)\src\Shared\DiagnosticDefinitions.cs" Link="Includes\DiagnosticDefinitions.cs" />
<Compile Include="$(RepoRoot)\src\Shared\ExceptionExtensions.cs" Link="Includes\ExceptionExtensions.cs" />
<Compile Include="$(RepoRoot)\src\Shared\Guard.cs" Link="Includes\Guard.cs" />
<Compile Include="$(RepoRoot)\src\Shared\PercentEncodingHelper.cs" Link="Includes\PercentEncodingHelper.cs" />
<Compile Include="$(RepoRoot)\src\Shared\SemanticConventions.cs" Link="Includes\SemanticConventions.cs" />
<Compile Include="$(RepoRoot)\src\Shared\Shims\ExperimentalAttribute.cs" Link="Includes\Shims\ExperimentalAttribute.cs" />
<Compile Include="$(RepoRoot)\src\Shared\Shims\Lock.cs" Link="Includes\Shims\Lock.cs" />
Expand Down
5 changes: 5 additions & 0 deletions src/OpenTelemetry/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ Released 2025-Oct-01
to a single `MeterProvider`, as required by the OpenTelemetry specification.
([#6458](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6458))

* Added decoding of the `OTEL_RESOURCE_ATTRIBUTES` variable according to the specification,
adhering to the [W3C Baggage](https://github.com/w3c/baggage/blob/main/baggage/HTTP_HEADER_FORMAT.md)
format.
([#6461](https://github.com/open-telemetry/opentelemetry-dotnet/pull/6461))

* Added `FormatMessage` configuration option to self-diagnostics feature. When
set to `true` (default is false), log messages will be formatted by replacing
placeholders with actual parameter values for improved readability.
Expand Down
13 changes: 4 additions & 9 deletions src/OpenTelemetry/Resources/OtelEnvResourceDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
// SPDX-License-Identifier: Apache-2.0

using Microsoft.Extensions.Configuration;
using OpenTelemetry.Internal;

namespace OpenTelemetry.Resources;

internal sealed class OtelEnvResourceDetector : IResourceDetector
{
public const string EnvVarKey = "OTEL_RESOURCE_ATTRIBUTES";
private const char AttributeListSplitter = ',';
private const char AttributeKeyValueSplitter = '=';

private readonly IConfiguration configuration;

Expand All @@ -35,16 +34,12 @@ private static List<KeyValuePair<string, object>> ParseResourceAttributes(string
{
var attributes = new List<KeyValuePair<string, object>>();

string[] rawAttributes = resourceAttributes.Split(AttributeListSplitter);
foreach (string rawKeyValuePair in rawAttributes)
if (PercentEncodingHelper.TryExtractBaggage([resourceAttributes], out var baggage) && baggage != null)
{
string[] keyValuePair = rawKeyValuePair.Split(AttributeKeyValueSplitter);
if (keyValuePair.Length != 2)
foreach (var kvp in baggage)
{
continue;
attributes.Add(new KeyValuePair<string, object>(kvp.Key, kvp.Value));
}

attributes.Add(new KeyValuePair<string, object>(keyValuePair[0].Trim(), keyValuePair[1].Trim()));
}

return attributes;
Expand Down
146 changes: 146 additions & 0 deletions src/Shared/PercentEncodingHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

#if NET
using System.Diagnostics.CodeAnalysis;
#endif
using System.Text;
using System.Text.RegularExpressions;

namespace OpenTelemetry.Internal;

/// <summary>
/// Helper methods for percent-encoding and decoding baggage values.
/// See https://w3c.github.io/baggage/.
/// </summary>
internal static partial class PercentEncodingHelper
{
private const int MaxBaggageLength = 8192;
private const int MaxBaggageItems = 180;
private const char KeyValueSplitter = '=';
private const char ListSplitter = ',';

internal static bool TryExtractBaggage(
string[] baggageCollection,
#if NET
[NotNullWhen(true)]
#endif
out Dictionary<string, string>? baggage)
{
Dictionary<string, string>? baggageDictionary = null;
int baggageLength = -1; // Start with -1 to account for no leading comma on first item

foreach (var baggageList in baggageCollection.Where(h => !string.IsNullOrEmpty(h)))
{
foreach (string keyValuePair in baggageList.Split(ListSplitter))
{
baggageLength += keyValuePair.Length + 1; // pair length + comma
if (ExceedsMaxBaggageLimits(baggageLength, baggageDictionary?.Count))
{
baggage = baggageDictionary;
return baggageDictionary != null;
}
#if NET
var indexOfFirstEquals = keyValuePair.IndexOf(KeyValueSplitter, StringComparison.Ordinal);
#else
var indexOfFirstEquals = keyValuePair.IndexOf(KeyValueSplitter);
#endif
if (indexOfFirstEquals < 0)
{
continue;
}

var splitKeyValue = keyValuePair.Split([KeyValueSplitter], 2);
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that indexOfFirstEquals is already found above, might as well just use that.

var key = splitKeyValue[0].Trim();
var value = splitKeyValue[1].Trim();
Copy link
Contributor

Choose a reason for hiding this comment

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

Use spans to save some allocations. Same as #6461 (comment)


if (!IsValidKeyValuePair(key, value))
{
continue;
}

var decodedValue = PercentDecodeBaggage(value);

baggageDictionary ??= [];
baggageDictionary[key] = decodedValue;
}
}

baggage = baggageDictionary;
return baggageDictionary != null;
}

/// <summary>
/// As per the specification, only the value is percent-encoded.
/// "Uri.EscapeDataString" encodes code points which are not required to be percent-encoded.
/// </summary>
/// <param name="key"> The baggage key. </param>
/// <param name="value"> The baggage value. </param>
/// <returns> The percent-encoded baggage item. </returns>
internal static string PercentEncodeBaggage(string key, string value) => $"{key.Trim()}={Uri.EscapeDataString(value.Trim())}";

private static string PercentDecodeBaggage(string baggageEncoded)
{
var bytes = new List<byte>();
for (int i = 0; i < baggageEncoded.Length; i++)
{
if (baggageEncoded[i] == '%' && i + 2 < baggageEncoded.Length && IsHex(baggageEncoded[i + 1]) && IsHex(baggageEncoded[i + 2]))
{
var hex = baggageEncoded.AsSpan(i + 1, 2);
#if NET
bytes.Add(byte.Parse(hex, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture));
#else
bytes.Add(Convert.ToByte(hex.ToString(), 16));
Copy link
Contributor

Choose a reason for hiding this comment

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

It will be a lot of string allocations to generate a string for each hex number in the hot path.

#endif

i += 2;
}
else if (baggageEncoded[i] == '%')
{
return baggageEncoded; // Bad percent triplet -> return original value
Copy link
Contributor

@xiang17 xiang17 Dec 3, 2025

Choose a reason for hiding this comment

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

The spec called out that it must be replaced with the replacement code point (U+FFFD). https://www.w3.org/TR/baggage/#value

When decoding the value, percent-encoded octet sequences that do not match the UTF-8 encoding scheme MUST be replaced with the replacement code point (U+FFFD).

Although it didn't say what to do for invalid encoded values, I personally I think it's better to keep non percent-encoded triplets as is. For reference, WebUtility.UrlDecode("123+abc%G1%") returns 123 abc%G1%.

}
else
{
if (!IsBaggageOctet(baggageEncoded[i]))
{
return baggageEncoded; // non-encoded character not baggage octet encoded -> return original value
}

bytes.Add((byte)baggageEncoded[i]);
}
}

return new UTF8Encoding(false, false).GetString(bytes.ToArray());
Copy link
Contributor

Choose a reason for hiding this comment

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

The baggage string has always been ASCII only before and after conversion. Why switching to List<byte> and UTF8Encoding, instead of keep using StringBuilder like in the original approach?

}

#if NET
[GeneratedRegex(@"^[!#$%&'*+\-\.^_`|~0-9A-Z]+$", RegexOptions.IgnoreCase)]
private static partial Regex TokenRegex();
#else

#pragma warning disable SA1201 // A field should not follow a method
private static readonly Regex TokenRegexField = new(
@"^[!#$%&'*+\-\.^_`|~0-9A-Z]+$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
#pragma warning restore SA1201 // A field should not follow a method

private static Regex TokenRegex() => TokenRegexField;
#endif

private static bool ExceedsMaxBaggageLimits(int currentLength, int? currentItemCount) =>
currentLength >= MaxBaggageLength || currentItemCount >= MaxBaggageItems;

private static bool IsValidKeyValuePair(string key, string value) =>
!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value) && TokenRegex().IsMatch(key);
Copy link
Contributor

Choose a reason for hiding this comment

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

The key is a token, which also has its limits:

Copy link
Contributor

Choose a reason for hiding this comment

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

Avoid regex on hot path.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is empty value not allowed?


private static bool IsHex(char c) =>
(c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F');

private static bool IsBaggageOctet(char c) =>
c == 0x21 ||
(c >= 0x23 && c <= 0x2B) ||
(c >= 0x2D && c <= 0x3A) ||
(c >= 0x3C && c <= 0x5B) ||
(c >= 0x5D && c <= 0x7E);
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,14 @@ public void ValidateSpecialCharsBaggageExtraction()

Assert.Equal(3, actualBaggage.Count);

Assert.True(actualBaggage.ContainsKey("key 1"));
Assert.Equal("value 1", actualBaggage["key 1"]);
Assert.True(actualBaggage.ContainsKey("key+1"));
Assert.Equal("value+1", actualBaggage["key+1"]);

Assert.True(actualBaggage.ContainsKey("key2"));
Assert.Equal("!x_x,x-x&x(x\");:", actualBaggage["key2"]);

Assert.True(actualBaggage.ContainsKey("key()3"));
Assert.Equal("value()!&;:", actualBaggage["key()3"]);
Assert.True(actualBaggage.ContainsKey("key%28%293"));
Assert.Equal("value()!&;:", actualBaggage["key%28%293"]);
}

[Fact]
Expand Down Expand Up @@ -204,6 +204,6 @@ public void ValidateSpecialCharsBaggageInjection()
this.baggage.Inject(propagationContext, carrier, Setter);

Assert.Single(carrier);
Assert.Equal("key+1=value+1,key2=!x_x%2Cx-x%26x(x%22)%3B%3A", carrier[BaggagePropagator.BaggageHeaderName]);
Assert.Equal("key 1=value%201,key2=%21x_x%2Cx-x%26x%28x%22%29%3B%3A", carrier[BaggagePropagator.BaggageHeaderName]);
}
}
Loading
Loading