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);
var key = splitKeyValue[0].Trim();
var value = splitKeyValue[1].Trim();

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));
#endif

i += 2;
}
else if (baggageEncoded[i] == '%')
{
return baggageEncoded; // Bad percent triplet -> return original value
}
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());
}

#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);

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