diff --git a/OpenTelemetry.slnx b/OpenTelemetry.slnx
index b7ec1962f59..f78584e3e4c 100644
--- a/OpenTelemetry.slnx
+++ b/OpenTelemetry.slnx
@@ -219,6 +219,7 @@
+
@@ -228,6 +229,7 @@
+
diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md
index c99355a3900..621c0a54d31 100644
--- a/src/OpenTelemetry.Api/CHANGELOG.md
+++ b/src/OpenTelemetry.Api/CHANGELOG.md
@@ -6,6 +6,12 @@ Notes](../../RELEASENOTES.md).
## Unreleased
+* Fix baggage and trace headers not respecting the maximum length in some cases.
+ ([#7061](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7061))
+
+* Improve efficiency of parsing of baggage and B3 propagation headers.
+ ([#7061](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7061))
+
## 1.15.2
Released 2026-Apr-08
diff --git a/src/OpenTelemetry.Api/Context/Propagation/B3Propagator.cs b/src/OpenTelemetry.Api/Context/Propagation/B3Propagator.cs
index 5367d42794f..75504cae164 100644
--- a/src/OpenTelemetry.Api/Context/Propagation/B3Propagator.cs
+++ b/src/OpenTelemetry.Api/Context/Propagation/B3Propagator.cs
@@ -195,65 +195,111 @@ private static PropagationContext ExtractFromSingleHeader(PropagationContext
{
try
{
- var header = getter(carrier, XB3Combined)?.FirstOrDefault();
- if (string.IsNullOrWhiteSpace(header))
+ var headers = getter(carrier, XB3Combined);
+ if (headers == null)
{
return context;
}
- var parts =
-#if NET
- header.Split(XB3CombinedDelimiter);
-#else
- header!.Split(XB3CombinedDelimiter);
-#endif
+ var header = headers.FirstOrDefault();
- if (parts.Length is < 2 or > 4)
- {
- return context;
- }
+ return string.IsNullOrWhiteSpace(header)
+ ? context
+ : !TryExtractSingleHeaderContext(header, out var traceId, out var spanId, out var traceOptions)
+ ? context
+ : new PropagationContext(
+ new ActivityContext(traceId, spanId, traceOptions, isRemote: true),
+ context.Baggage);
+ }
+ catch (Exception e)
+ {
+ OpenTelemetryApiEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e);
+ return context;
+ }
+ }
- var traceIdStr = parts[0];
- if (string.IsNullOrWhiteSpace(traceIdStr))
- {
- return context;
- }
+ private static bool TryExtractSingleHeaderContext(
+ string header,
+ out ActivityTraceId traceId,
+ out ActivitySpanId spanId,
+ out ActivityTraceFlags traceOptions)
+ {
+ traceId = default;
+ spanId = default;
+ traceOptions = ActivityTraceFlags.None;
+
+ var headerValue = header.AsSpan();
+ var position = 0;
+ var traceIdStr = ReadNextPart(headerValue, position, out position);
+ if (position >= headerValue.Length || traceIdStr.IsEmpty)
+ {
+ return false;
+ }
+
+ var spanIdStr = ReadNextPart(headerValue, position, out position);
+ if (spanIdStr.IsEmpty)
+ {
+ return false;
+ }
- if (traceIdStr.Length == 16)
+ ReadOnlySpan traceFlagsStr = default;
+ if (position < headerValue.Length)
+ {
+ traceFlagsStr = ReadNextPart(headerValue, position, out position);
+ if (position < headerValue.Length)
{
- // This is an 8-byte traceID.
- traceIdStr = UpperTraceId + traceIdStr;
+ _ = ReadNextPart(headerValue, position, out position);
+ if (position < headerValue.Length)
+ {
+ return false;
+ }
}
+ }
- var traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan());
+ traceId = CreateTraceId(traceIdStr);
+ spanId = ActivitySpanId.CreateFromString(spanIdStr);
- var spanIdStr = parts[1];
- if (string.IsNullOrWhiteSpace(spanIdStr))
- {
- return context;
- }
+ if (IsSampledValue(traceFlagsStr) ||
+ traceFlagsStr.Equals(FlagsValue.AsSpan(), StringComparison.Ordinal))
+ {
+ traceOptions |= ActivityTraceFlags.Recorded;
+ }
- var spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan());
+ return true;
+ }
- var traceOptions = ActivityTraceFlags.None;
- if (parts.Length > 2)
- {
- var traceFlagsStr = parts[2];
- if (SampledValues.Contains(traceFlagsStr)
- || FlagsValue.Equals(traceFlagsStr, StringComparison.Ordinal))
- {
- traceOptions |= ActivityTraceFlags.Recorded;
- }
- }
+ private static bool IsSampledValue(ReadOnlySpan value) =>
+ value.Equals(SampledValue.AsSpan(), StringComparison.Ordinal) ||
+ value.Equals(LegacySampledValue.AsSpan(), StringComparison.Ordinal);
- return new PropagationContext(
- new ActivityContext(traceId, spanId, traceOptions, isRemote: true),
- context.Baggage);
+ private static ActivityTraceId CreateTraceId(ReadOnlySpan traceId)
+ {
+ if (traceId.Length == 16)
+ {
+ Span fullTraceId = stackalloc char[UpperTraceId.Length + 16];
+
+ UpperTraceId.AsSpan().CopyTo(fullTraceId);
+ traceId.CopyTo(fullTraceId.Slice(UpperTraceId.Length));
+
+ return ActivityTraceId.CreateFromString(fullTraceId);
}
- catch (Exception e)
+
+ return ActivityTraceId.CreateFromString(traceId);
+ }
+
+ private static ReadOnlySpan ReadNextPart(ReadOnlySpan header, int position, out int nextPosition)
+ {
+ var remaining = header.Slice(position);
+ var separatorIndex = remaining.IndexOf(XB3CombinedDelimiter);
+ if (separatorIndex < 0)
{
- OpenTelemetryApiEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e);
- return context;
+ nextPosition = header.Length;
+ var part = remaining;
+ return part;
}
+
+ var result = remaining.Slice(0, separatorIndex);
+ nextPosition = position + separatorIndex + 1;
+ return result;
}
}
diff --git a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs
index be150770fe6..8f354ecd3b4 100644
--- a/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs
+++ b/src/OpenTelemetry.Api/Context/Propagation/BaggagePropagator.cs
@@ -2,6 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
#if NET
+#if NET9_0_OR_GREATER
+using System.Buffers;
+#endif
using System.Diagnostics.CodeAnalysis;
#endif
using System.Net;
@@ -20,8 +23,9 @@ public class BaggagePropagator : TextMapPropagator
private const int MaxBaggageLength = 8192;
private const int MaxBaggageItems = 180;
- private static readonly char[] EqualSignSeparator = ['='];
- private static readonly char[] CommaSignSeparator = [','];
+#if NET9_0_OR_GREATER
+ private static readonly SearchValues DecodeHints = SearchValues.Create('%', '+');
+#endif
///
public override ISet Fields => new HashSet { BaggageHeaderName };
@@ -50,9 +54,9 @@ public override PropagationContext Extract(PropagationContext context, T carr
try
{
var baggageCollection = getter(carrier, BaggageHeaderName);
- if (baggageCollection?.Any() ?? false)
+ if (baggageCollection is not null)
{
- if (TryExtractBaggage([.. baggageCollection], out var baggageItems))
+ if (TryExtractBaggage(baggageCollection, out var baggageItems))
{
Baggage baggage =
#if NET
@@ -104,16 +108,40 @@ public override void Inject(PropagationContext context, T carrier, Action 0)
+ {
+ baggageItemLength++;
+ }
+
+ if (baggage.Length + baggageItemLength > MaxBaggageLength)
+ {
+ break;
+ }
+
+ if (baggage.Length > 0)
+ {
+ baggage.Append(',');
+ }
+
+ baggage.Append(encodedKey)
+ .Append('=')
+ .Append(encodedValue);
+ }
+ while (e.MoveNext() && ++itemCount < MaxBaggageItems);
+
+ if (baggage.Length > 0)
+ {
+ setter(carrier, BaggageHeaderName, baggage.ToString());
}
- 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,
+ IEnumerable baggageCollection,
#if NET
[NotNullWhen(true)]
#endif
@@ -135,8 +163,10 @@ internal static bool TryExtractBaggage(
continue;
}
- foreach (var pair in item.Split(CommaSignSeparator))
+ var remaining = item.AsSpan();
+ while (!remaining.IsEmpty)
{
+ var pair = ReadNextSegment(ref remaining, ',');
baggageLength += pair.Length + 1; // pair and comma
if (baggageLength >= MaxBaggageLength || baggageDictionary?.Count >= MaxBaggageItems)
@@ -145,31 +175,21 @@ internal static bool TryExtractBaggage(
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)
+ var separatorIndex = pair.IndexOf('=');
+ if (separatorIndex < 0)
{
continue;
}
- var key = WebUtility.UrlDecode(parts[0]);
- var value = WebUtility.UrlDecode(parts[1]);
+ var key = DecodeIfNeeded(pair.Slice(0, separatorIndex));
+ var value = DecodeIfNeeded(pair.Slice(separatorIndex + 1));
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value))
{
continue;
}
- baggageDictionary ??= [];
-
+ baggageDictionary ??= new(StringComparer.Ordinal);
baggageDictionary[key] = value;
}
}
@@ -177,4 +197,26 @@ internal static bool TryExtractBaggage(
baggage = baggageDictionary;
return baggageDictionary != null;
}
+
+ private static ReadOnlySpan ReadNextSegment(ref ReadOnlySpan remaining, char separator)
+ {
+ var separatorIndex = remaining.IndexOf(separator);
+ if (separatorIndex < 0)
+ {
+ var segment = remaining;
+ remaining = [];
+ return segment;
+ }
+
+ var result = remaining.Slice(0, separatorIndex);
+ remaining = remaining.Slice(separatorIndex + 1);
+ return result;
+ }
+
+ private static string DecodeIfNeeded(ReadOnlySpan value) =>
+#if NET9_0_OR_GREATER
+ value.ContainsAny(DecodeHints) ? WebUtility.UrlDecode(value.ToString()) : value.ToString();
+#else
+ value.IndexOfAny('%', '+') < 0 ? value.ToString() : WebUtility.UrlDecode(value.ToString());
+#endif
}
diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs
index 66ddc493807..111f5caec09 100644
--- a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs
+++ b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs
@@ -125,7 +125,15 @@ public override void Inject(PropagationContext context, T carrier, Action 0)
{
- setter(carrier, TraceState, tracestateStr);
+ var tracestateEntries = new List>();
+ if (TraceStateUtils.AppendTraceState(tracestateStr, tracestateEntries))
+ {
+ var normalizedTraceState = TraceStateUtils.GetString(tracestateEntries);
+ if (normalizedTraceState.Length > 0)
+ {
+ setter(carrier, TraceState, normalizedTraceState);
+ }
+ }
}
}
diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceStateUtils.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceStateUtils.cs
index 255c072bc8d..9ecc5c93b6d 100644
--- a/src/OpenTelemetry.Api/Context/Propagation/TraceStateUtils.cs
+++ b/src/OpenTelemetry.Api/Context/Propagation/TraceStateUtils.cs
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
using System.Diagnostics;
-using System.Text;
using OpenTelemetry.Internal;
namespace OpenTelemetry.Context.Propagation;
@@ -15,6 +14,8 @@ internal static class TraceStateUtils
private const int KeyMaxSize = 256;
private const int ValueMaxSize = 256;
private const int MaxKeyValuePairsCount = 32;
+ private const int MaxTraceStateLength = 512;
+ private const int LargeEntryLength = 128;
///
/// Extracts tracestate pairs from the given string and appends it to provided tracestate list.
@@ -106,37 +107,57 @@ internal static bool AppendTraceState(string traceStateString, List>? traceState)
{
-#pragma warning disable CA1851 // Possible multiple enumerations of 'IEnumerable' collection
- if (traceState == null || !traceState.Any())
+ if (traceState == null)
+ {
+ return string.Empty;
+ }
+
+ var entries = new List();
+ var pairsCount = 0;
+ foreach (var entry in traceState)
+ {
+ pairsCount++;
+ if (entries.Count < MaxKeyValuePairsCount)
+ {
+ // Take the first MaxKeyValuePairsCount pairs and ignore older pairs after that.
+ entries.Add(string.Concat(entry.Key, "=", entry.Value));
+ }
+ }
+
+ if (entries.Count == 0)
{
return string.Empty;
}
- // it's supposedly cheaper to iterate over very short collection a couple of times
- // than to convert it to array.
- var pairsCount = traceState.Count();
if (pairsCount > MaxKeyValuePairsCount)
{
OpenTelemetryApiEventSource.Log.TooManyItemsInTracestate();
}
- var sb = new StringBuilder();
+ TruncateEntries(entries);
- var ind = 0;
- foreach (var entry in traceState)
+ return string.Join(",", entries);
+ }
+
+ private static void TruncateEntries(List entries)
+ {
+ if (GetCombinedLength(entries) <= MaxTraceStateLength)
{
- if (ind++ < MaxKeyValuePairsCount)
+ return;
+ }
+
+ for (var i = entries.Count - 1; i >= 0 && GetCombinedLength(entries) > MaxTraceStateLength; i--)
+ {
+ if (entries[i].Length > LargeEntryLength)
{
- // take last MaxKeyValuePairsCount pairs, ignore last (oldest) pairs
- sb.Append(entry.Key)
- .Append('=')
- .Append(entry.Value)
- .Append(',');
+ entries.RemoveAt(i);
}
}
-#pragma warning restore CA1851 // Possible multiple enumerations of 'IEnumerable' collection
- return sb.Remove(sb.Length - 1, 1).ToString();
+ while (entries.Count > 0 && GetCombinedLength(entries) > MaxTraceStateLength)
+ {
+ entries.RemoveAt(entries.Count - 1);
+ }
}
private static bool TryParseKeyValue(ReadOnlySpan pair, out ReadOnlySpan key, out ReadOnlySpan value)
@@ -259,4 +280,21 @@ private static bool ValidateValue(ReadOnlySpan value)
return true;
}
+
+ private static int GetCombinedLength(List entries)
+ {
+ var combinedLength = 0;
+
+ for (var i = 0; i < entries.Count; i++)
+ {
+ if (i > 0)
+ {
+ combinedLength++;
+ }
+
+ combinedLength += entries[i].Length;
+ }
+
+ return combinedLength;
+ }
}
diff --git a/src/OpenTelemetry.Extensions.Propagators/B3Propagator.cs b/src/OpenTelemetry.Extensions.Propagators/B3Propagator.cs
index ddc737caaa7..0c3a7d1c70c 100644
--- a/src/OpenTelemetry.Extensions.Propagators/B3Propagator.cs
+++ b/src/OpenTelemetry.Extensions.Propagators/B3Propagator.cs
@@ -195,58 +195,103 @@ private static PropagationContext ExtractFromSingleHeader(PropagationContext
var header = headers.FirstOrDefault();
- if (string.IsNullOrWhiteSpace(header))
- {
- return context;
- }
+ return string.IsNullOrWhiteSpace(header)
+ ? context
+ : !TryExtractSingleHeaderContext(header, out var traceId, out var spanId, out var traceOptions)
+ ? context
+ : new PropagationContext(
+ new ActivityContext(traceId, spanId, traceOptions, isRemote: true),
+ context.Baggage);
+ }
+ catch (Exception e)
+ {
+ OpenTelemetryPropagatorsEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e);
+ return context;
+ }
+ }
- var parts = header.Split(XB3CombinedDelimiter);
- if (parts.Length is < 2 or > 4)
- {
- return context;
- }
+ private static bool TryExtractSingleHeaderContext(
+ string header,
+ out ActivityTraceId traceId,
+ out ActivitySpanId spanId,
+ out ActivityTraceFlags traceOptions)
+ {
+ traceId = default;
+ spanId = default;
+ traceOptions = ActivityTraceFlags.None;
+
+ var headerValue = header.AsSpan();
+ var position = 0;
+ var traceIdStr = ReadNextPart(headerValue, position, out position);
+ if (position >= headerValue.Length || traceIdStr.IsEmpty)
+ {
+ return false;
+ }
- var traceIdStr = parts[0];
- if (string.IsNullOrWhiteSpace(traceIdStr))
- {
- return context;
- }
+ var spanIdStr = ReadNextPart(headerValue, position, out position);
+ if (spanIdStr.IsEmpty)
+ {
+ return false;
+ }
- if (traceIdStr.Length == 16)
+ ReadOnlySpan traceFlagsStr = default;
+ if (position < headerValue.Length)
+ {
+ traceFlagsStr = ReadNextPart(headerValue, position, out position);
+ if (position < headerValue.Length)
{
- // This is an 8-byte traceID.
- traceIdStr = UpperTraceId + traceIdStr;
+ _ = ReadNextPart(headerValue, position, out position);
+ if (position < headerValue.Length)
+ {
+ return false;
+ }
}
+ }
- var traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan());
+ traceId = CreateTraceId(traceIdStr);
+ spanId = ActivitySpanId.CreateFromString(spanIdStr);
- var spanIdStr = parts[1];
- if (string.IsNullOrWhiteSpace(spanIdStr))
- {
- return context;
- }
+ if (IsSampledValue(traceFlagsStr) ||
+ traceFlagsStr.Equals(FlagsValue.AsSpan(), StringComparison.Ordinal))
+ {
+ traceOptions |= ActivityTraceFlags.Recorded;
+ }
- var spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan());
+ return true;
+ }
- var traceOptions = ActivityTraceFlags.None;
- if (parts.Length > 2)
- {
- var traceFlagsStr = parts[2];
- if (SampledValues.Contains(traceFlagsStr)
- || FlagsValue.Equals(traceFlagsStr, StringComparison.Ordinal))
- {
- traceOptions |= ActivityTraceFlags.Recorded;
- }
- }
+ private static bool IsSampledValue(ReadOnlySpan value) =>
+ value.Equals(SampledValue.AsSpan(), StringComparison.Ordinal) ||
+ value.Equals(LegacySampledValue.AsSpan(), StringComparison.Ordinal);
- return new PropagationContext(
- new ActivityContext(traceId, spanId, traceOptions, isRemote: true),
- context.Baggage);
+ private static ActivityTraceId CreateTraceId(ReadOnlySpan traceId)
+ {
+ if (traceId.Length == 16)
+ {
+ Span fullTraceId = stackalloc char[UpperTraceId.Length + 16];
+
+ UpperTraceId.AsSpan().CopyTo(fullTraceId);
+ traceId.CopyTo(fullTraceId.Slice(UpperTraceId.Length));
+
+ return ActivityTraceId.CreateFromString(fullTraceId);
}
- catch (Exception e)
+
+ return ActivityTraceId.CreateFromString(traceId);
+ }
+
+ private static ReadOnlySpan ReadNextPart(ReadOnlySpan header, int position, out int nextPosition)
+ {
+ var remaining = header.Slice(position);
+ var separatorIndex = remaining.IndexOf(XB3CombinedDelimiter);
+ if (separatorIndex < 0)
{
- OpenTelemetryPropagatorsEventSource.Log.ActivityContextExtractException(nameof(B3Propagator), e);
- return context;
+ nextPosition = header.Length;
+ var part = remaining;
+ return part;
}
+
+ var result = remaining.Slice(0, separatorIndex);
+ nextPosition = position + separatorIndex + 1;
+ return result;
}
}
diff --git a/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md b/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md
index 71d3dde8bc2..26990be7cdb 100644
--- a/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md
+++ b/src/OpenTelemetry.Extensions.Propagators/CHANGELOG.md
@@ -6,6 +6,9 @@ covering all components see: [Release Notes](../../RELEASENOTES.md).
## Unreleased
+* Improve efficiency of parsing of baggage, B3 and Jaeger propagation headers.
+ ([#7061](https://github.com/open-telemetry/opentelemetry-dotnet/pull/7061))
+
## 1.15.2
Released 2026-Apr-08
diff --git a/src/OpenTelemetry.Extensions.Propagators/JaegerPropagator.cs b/src/OpenTelemetry.Extensions.Propagators/JaegerPropagator.cs
index 38193de9717..001e5fa4ea2 100644
--- a/src/OpenTelemetry.Extensions.Propagators/JaegerPropagator.cs
+++ b/src/OpenTelemetry.Extensions.Propagators/JaegerPropagator.cs
@@ -71,14 +71,11 @@ public override PropagationContext Extract(PropagationContext context, T carr
var jaegerHeaderParsed = TryExtractTraceContext(jaegerHeader, out var traceId, out var spanId, out var traceOptions);
- if (!jaegerHeaderParsed)
- {
- return context;
- }
-
- return new PropagationContext(
- new ActivityContext(traceId, spanId, traceOptions, isRemote: true),
- context.Baggage);
+ return !jaegerHeaderParsed
+ ? context
+ : new PropagationContext(
+ new ActivityContext(traceId, spanId, traceOptions, isRemote: true),
+ context.Baggage);
}
catch (Exception ex)
{
@@ -147,13 +144,20 @@ internal static bool TryExtractTraceContext(string jaegerHeader, out ActivityTra
return false;
}
- var traceComponents = jaegerHeader.Split(JaegerDelimiters, StringSplitOptions.RemoveEmptyEntries);
- if (traceComponents.Length != 4)
+#if NET
+ var headerValue = jaegerHeader;
+#else
+ var headerValue = jaegerHeader!;
+#endif
+ if (!TryExtractTraceParts(
+ headerValue,
+ out var traceIdStr,
+ out var spanIdStr,
+ out var traceFlagsStr))
{
return false;
}
- var traceIdStr = traceComponents[0];
if (traceIdStr.Length < TraceId128BitLength)
{
traceIdStr = traceIdStr.PadLeft(TraceId128BitLength, '0');
@@ -161,7 +165,6 @@ internal static bool TryExtractTraceContext(string jaegerHeader, out ActivityTra
traceId = ActivityTraceId.CreateFromString(traceIdStr.AsSpan());
- var spanIdStr = traceComponents[1];
if (spanIdStr.Length < SpanIdLength)
{
spanIdStr = spanIdStr.PadLeft(SpanIdLength, '0');
@@ -169,7 +172,6 @@ internal static bool TryExtractTraceContext(string jaegerHeader, out ActivityTra
spanId = ActivitySpanId.CreateFromString(spanIdStr.AsSpan());
- var traceFlagsStr = traceComponents[3];
if (SampledValue.Equals(traceFlagsStr, StringComparison.Ordinal))
{
traceOptions |= ActivityTraceFlags.Recorded;
@@ -177,4 +179,93 @@ internal static bool TryExtractTraceContext(string jaegerHeader, out ActivityTra
return true;
}
+
+ private static bool TryExtractTraceParts(
+ string jaegerHeader,
+ out string traceId,
+ out string spanId,
+ out string traceFlags)
+ {
+ traceId = string.Empty;
+ spanId = string.Empty;
+ traceFlags = string.Empty;
+
+ var position = 0;
+ var componentCount = 0;
+
+ while (position <= jaegerHeader.Length)
+ {
+ var component = ReadNextComponent(jaegerHeader, ref position);
+ if (component.IsEmpty)
+ {
+ if (position >= jaegerHeader.Length)
+ {
+ break;
+ }
+
+ continue;
+ }
+
+ switch (componentCount)
+ {
+ case 0:
+ traceId = component.ToString();
+ break;
+
+ case 1:
+ spanId = component.ToString();
+ break;
+
+ case 2:
+ break;
+
+ case 3:
+ traceFlags = component.ToString();
+ break;
+
+ default:
+ return false;
+ }
+
+ componentCount++;
+
+ if (position >= jaegerHeader.Length)
+ {
+ break;
+ }
+ }
+
+ return componentCount == 4;
+ }
+
+ private static ReadOnlySpan ReadNextComponent(string header, ref int position)
+ {
+ var colonIndex = header.IndexOf(JaegerDelimiter, position, StringComparison.Ordinal);
+ var encodedIndex = header.IndexOf(JaegerDelimiterEncoded, position, StringComparison.Ordinal);
+
+ var nextIndex = -1;
+ var delimiterLength = 0;
+
+ if (colonIndex >= 0 && (encodedIndex < 0 || colonIndex < encodedIndex))
+ {
+ nextIndex = colonIndex;
+ delimiterLength = JaegerDelimiter.Length;
+ }
+ else if (encodedIndex >= 0)
+ {
+ nextIndex = encodedIndex;
+ delimiterLength = JaegerDelimiterEncoded.Length;
+ }
+
+ if (nextIndex < 0)
+ {
+ var result = header.AsSpan(position);
+ position = header.Length;
+ return result;
+ }
+
+ var component = header.AsSpan(position, nextIndex - position);
+ position = nextIndex + delimiterLength;
+ return component;
+ }
}
diff --git a/test/Benchmarks/Benchmarks.csproj b/test/Benchmarks/Benchmarks.csproj
index c820c9e0d14..cbd57153824 100644
--- a/test/Benchmarks/Benchmarks.csproj
+++ b/test/Benchmarks/Benchmarks.csproj
@@ -26,6 +26,7 @@
+
diff --git a/test/Benchmarks/Context/Propagation/B3PropagatorBenchmarks.cs b/test/Benchmarks/Context/Propagation/B3PropagatorBenchmarks.cs
new file mode 100644
index 00000000000..387dd359c0a
--- /dev/null
+++ b/test/Benchmarks/Context/Propagation/B3PropagatorBenchmarks.cs
@@ -0,0 +1,123 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Diagnostics;
+using BenchmarkDotNet.Attributes;
+using OpenTelemetry.Context.Propagation;
+using ApiB3Propagator = OpenTelemetry.Context.Propagation.B3Propagator;
+using ExtensionsB3Propagator = OpenTelemetry.Extensions.Propagators.B3Propagator;
+
+namespace Benchmarks.Context.Propagation;
+
+[MemoryDiagnoser]
+[Obsolete("Intentional coverage for obsolete API.")]
+public class B3PropagatorBenchmarks
+{
+ private const string TraceIdBase16 = "ff000000000000000000000000000041";
+ private const string SpanIdBase16 = "ff00000000000041";
+ private const string B3TraceId = "X-B3-TraceId";
+ private const string B3SpanId = "X-B3-SpanId";
+ private const string B3Sampled = "X-B3-Sampled";
+ private const string B3Combined = "b3";
+ private const string SampledValue = "1";
+
+ private static readonly ActivityTraceId TraceId = ActivityTraceId.CreateFromString(TraceIdBase16.AsSpan());
+ private static readonly ActivitySpanId SpanId = ActivitySpanId.CreateFromString(SpanIdBase16.AsSpan());
+
+ private static readonly ApiB3Propagator ApiMultiHeaderPropagator = new();
+ private static readonly ApiB3Propagator ApiSingleHeaderPropagator = new(true);
+ private static readonly ExtensionsB3Propagator ExtensionsMultiHeaderPropagator = new();
+ private static readonly ExtensionsB3Propagator ExtensionsSingleHeaderPropagator = new(true);
+
+ private static readonly Func, string, IEnumerable> Getter =
+ static (carrier, name) => carrier.TryGetValue(name, out var value) ? [value] : [];
+
+ private static readonly Action, string, string> Setter =
+ static (carrier, name, value) => carrier[name] = value;
+
+ [Params(false, true)]
+ public bool SingleHeader { get; set; }
+
+ [Params(false, true)]
+ public bool Sampled { get; set; }
+
+ public Dictionary ExtractCarrier { get; private set; } = [];
+
+ public Dictionary ApiInjectCarrier { get; private set; } = [];
+
+ public Dictionary ExtensionsInjectCarrier { get; private set; } = [];
+
+ public PropagationContext InjectContext { get; private set; }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ this.ExtractCarrier = this.CreateExtractCarrier();
+ this.InjectContext = new PropagationContext(
+ new ActivityContext(
+ TraceId,
+ SpanId,
+ this.Sampled ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None),
+ default);
+
+ this.ApiInjectCarrier = [];
+ this.ExtensionsInjectCarrier = [];
+ }
+
+ [Benchmark(Baseline = true)]
+ public PropagationContext ApiExtract() =>
+ this.GetApiPropagator().Extract(default, this.ExtractCarrier, Getter);
+
+ [Benchmark]
+ public PropagationContext ExtensionsExtract() =>
+ this.GetExtensionsPropagator().Extract(default, this.ExtractCarrier, Getter);
+
+ [Benchmark]
+ public void ApiInject()
+ {
+ this.ApiInjectCarrier.Clear();
+ this.GetApiPropagator().Inject(this.InjectContext, this.ApiInjectCarrier, Setter);
+ }
+
+ [Benchmark]
+ public void ExtensionsInject()
+ {
+ this.ExtensionsInjectCarrier.Clear();
+ this.GetExtensionsPropagator().Inject(this.InjectContext, this.ExtensionsInjectCarrier, Setter);
+ }
+
+ private static Dictionary CreateMultiHeaderCarrier(bool sampled)
+ {
+ var carrier = new Dictionary
+ {
+ [B3TraceId] = TraceIdBase16,
+ [B3SpanId] = SpanIdBase16,
+ };
+
+ if (sampled)
+ {
+ carrier[B3Sampled] = SampledValue;
+ }
+
+ return carrier;
+ }
+
+ private static Dictionary CreateSingleHeaderCarrier(bool sampled) =>
+ new()
+ {
+ [B3Combined] = sampled
+ ? $"{TraceIdBase16}-{SpanIdBase16}-{SampledValue}"
+ : $"{TraceIdBase16}-{SpanIdBase16}",
+ };
+
+ private Dictionary CreateExtractCarrier() =>
+ this.SingleHeader
+ ? CreateSingleHeaderCarrier(this.Sampled)
+ : CreateMultiHeaderCarrier(this.Sampled);
+
+ private ApiB3Propagator GetApiPropagator() =>
+ this.SingleHeader ? ApiSingleHeaderPropagator : ApiMultiHeaderPropagator;
+
+ private ExtensionsB3Propagator GetExtensionsPropagator() =>
+ this.SingleHeader ? ExtensionsSingleHeaderPropagator : ExtensionsMultiHeaderPropagator;
+}
diff --git a/test/Benchmarks/Context/Propagation/JaegerPropagatorBenchmarks.cs b/test/Benchmarks/Context/Propagation/JaegerPropagatorBenchmarks.cs
new file mode 100644
index 00000000000..e4ee2ee6316
--- /dev/null
+++ b/test/Benchmarks/Context/Propagation/JaegerPropagatorBenchmarks.cs
@@ -0,0 +1,79 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Diagnostics;
+using BenchmarkDotNet.Attributes;
+using OpenTelemetry.Context.Propagation;
+using OpenTelemetry.Extensions.Propagators;
+
+namespace Benchmarks.Context.Propagation;
+
+[MemoryDiagnoser]
+[Obsolete("Intentional coverage for obsolete API.")]
+public class JaegerPropagatorBenchmarks
+{
+ private const string TraceIdBase16 = "0007651916cd43dd8448eb211c803177";
+ private const string SpanIdBase16 = "0007c989f9791877";
+ private const string ParentSpanId = "0";
+ private const string JaegerHeader = "uber-trace-id";
+ private const string JaegerDelimiter = ":";
+ private const string JaegerDelimiterEncoded = "%3A";
+ private const string SampledValue = "1";
+
+ private static readonly ActivityTraceId TraceId = ActivityTraceId.CreateFromString(TraceIdBase16.AsSpan());
+ private static readonly ActivitySpanId SpanId = ActivitySpanId.CreateFromString(SpanIdBase16.AsSpan());
+ private static readonly JaegerPropagator Propagator = new();
+
+ private static readonly Func, string, IEnumerable> Getter =
+ static (carrier, name) => carrier.TryGetValue(name, out var values) ? values : [];
+
+ private static readonly Action, string, string> Setter =
+ static (carrier, name, value) => carrier[name] = value;
+
+ [Params(false, true)]
+ public bool Sampled { get; set; }
+
+ [Params(false, true)]
+ public bool UseEncodedDelimiter { get; set; }
+
+ public Dictionary ExtractCarrier { get; private set; } = [];
+
+ public Dictionary InjectCarrier { get; private set; } = [];
+
+ public PropagationContext InjectContext { get; private set; }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ var delimiter = this.UseEncodedDelimiter
+ ? JaegerDelimiterEncoded
+ : JaegerDelimiter;
+ var flags = this.Sampled ? SampledValue : "0";
+ var headerValue = string.Join(delimiter, TraceIdBase16, SpanIdBase16, ParentSpanId, flags);
+
+ this.ExtractCarrier = new Dictionary
+ {
+ [JaegerHeader] = [headerValue],
+ };
+
+ this.InjectContext = new PropagationContext(
+ new ActivityContext(
+ TraceId,
+ SpanId,
+ this.Sampled ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None),
+ default);
+
+ this.InjectCarrier = [];
+ }
+
+ [Benchmark(Baseline = true)]
+ public PropagationContext Extract() =>
+ Propagator.Extract(default, this.ExtractCarrier, Getter);
+
+ [Benchmark]
+ public void Inject()
+ {
+ this.InjectCarrier.Clear();
+ Propagator.Inject(this.InjectContext, this.InjectCarrier, Setter);
+ }
+}
diff --git a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/B3PropagatorFuzzTests.cs b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/B3PropagatorFuzzTests.cs
new file mode 100644
index 00000000000..0aa68e8e792
--- /dev/null
+++ b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/B3PropagatorFuzzTests.cs
@@ -0,0 +1,140 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Diagnostics;
+using FsCheck;
+using FsCheck.Fluent;
+using FsCheck.Xunit;
+using OpenTelemetry.Context.Propagation;
+
+namespace OpenTelemetry.Api.FuzzTests;
+
+[Obsolete("B3Propagator is obsolete but intentionally fuzz tested.")]
+public class B3PropagatorFuzzTests
+{
+ private const int MaxTests = 200;
+ private const int DelimiterFloodTests = 25;
+
+ private const string CombinedHeader = "b3";
+ private const string TraceIdHeader = "X-B3-TraceId";
+ private const string SpanIdHeader = "X-B3-SpanId";
+ private const string SampledHeader = "X-B3-Sampled";
+
+ [Property(MaxTest = MaxTests)]
+ public Property MultipleHeaderInjectExtractRoundTripPreservesTraceContext() => Prop.ForAll(Generators.ActivityContextArbitrary(), (activityContext) =>
+ {
+ try
+ {
+ var propagator = new B3Propagator();
+ var carrier = new Dictionary(StringComparer.Ordinal);
+
+ propagator.Inject(new PropagationContext(activityContext, default), carrier, FuzzTestHelpers.Setter);
+
+ var extracted = propagator.Extract(default, carrier, FuzzTestHelpers.Getter);
+
+ return
+ carrier.TryGetValue(TraceIdHeader, out var traceId) &&
+ carrier.TryGetValue(SpanIdHeader, out var spanId) &&
+ traceId.Length == 32 &&
+ spanId.Length == 16 &&
+ (!carrier.TryGetValue(SampledHeader, out var sampled) || sampled == "1") &&
+ HaveEquivalentB3State(activityContext, extracted.ActivityContext);
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+
+ [Property(MaxTest = MaxTests)]
+ public Property SingleHeaderInjectExtractRoundTripPreservesTraceContext() => Prop.ForAll(Generators.ActivityContextArbitrary(), (activityContext) =>
+ {
+ try
+ {
+ var propagator = new B3Propagator(singleHeader: true);
+ var carrier = new Dictionary(StringComparer.Ordinal);
+
+ propagator.Inject(new PropagationContext(activityContext, default), carrier, FuzzTestHelpers.Setter);
+
+ var extracted = propagator.Extract(default, carrier, FuzzTestHelpers.Getter);
+
+ return
+ carrier.TryGetValue(CombinedHeader, out var headerValue) &&
+ headerValue.Split('-').Length is 2 or 3 &&
+ HaveEquivalentB3State(activityContext, extracted.ActivityContext);
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+
+ [Property(MaxTest = MaxTests)]
+ public Property MultipleHeaderExtractIsDeterministicForArbitraryHeaders() => Prop.ForAll(Generators.B3MultipleHeaderCarrierArbitrary(), (carrier) =>
+ {
+ try
+ {
+ var propagator = new B3Propagator();
+ var original = FuzzTestHelpers.CloneCarrier(carrier);
+
+ var first = propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
+ var second = propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
+
+ return
+ FuzzTestHelpers.CarriersEqual(original, carrier) &&
+ HaveEquivalentB3State(first.ActivityContext, second.ActivityContext);
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+
+ [Property(MaxTest = MaxTests)]
+ public Property SingleHeaderExtractIsDeterministicForArbitraryHeaders() => Prop.ForAll(Generators.B3SingleHeaderCarrierArbitrary(), (carrier) =>
+ {
+ try
+ {
+ var propagator = new B3Propagator(singleHeader: true);
+ var original = FuzzTestHelpers.CloneCarrier(carrier);
+
+ var first = propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
+ var second = propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
+
+ return
+ FuzzTestHelpers.CarriersEqual(original, carrier) &&
+ HaveEquivalentB3State(first.ActivityContext, second.ActivityContext);
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+
+ [Property(MaxTest = DelimiterFloodTests)]
+ public Property SingleHeaderDelimiterFloodReturnsDefault() => Prop.ForAll(Generators.DelimiterFloodArbitrary('-'), (headerValue) =>
+ {
+ try
+ {
+ var propagator = new B3Propagator(singleHeader: true);
+ var carrier = new Dictionary(StringComparer.Ordinal)
+ {
+ [CombinedHeader] = [headerValue],
+ };
+ var original = FuzzTestHelpers.CloneCarrier(carrier);
+
+ var extracted = propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
+
+ return extracted.Equals(default) && FuzzTestHelpers.CarriersEqual(original, carrier);
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+
+ private static bool HaveEquivalentB3State(ActivityContext expected, ActivityContext actual) =>
+ expected.TraceId == actual.TraceId &&
+ expected.SpanId == actual.SpanId &&
+ expected.TraceFlags == actual.TraceFlags;
+}
diff --git a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs
new file mode 100644
index 00000000000..d1008b63d13
--- /dev/null
+++ b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/BaggagePropagatorFuzzTests.cs
@@ -0,0 +1,147 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using FsCheck;
+using FsCheck.Fluent;
+using FsCheck.Xunit;
+using OpenTelemetry.Context.Propagation;
+
+namespace OpenTelemetry.Api.FuzzTests;
+
+public class BaggagePropagatorFuzzTests
+{
+ private const string BaggageHeader = "baggage";
+ private const int MaxTests = 200;
+ private const int MaxBaggageLength = 8192;
+ private const int MaxBaggageItems = 180;
+
+ [Property(MaxTest = MaxTests)]
+ public Property InjectExtractRoundTripPreservesSafeBaggage() => Prop.ForAll(Generators.SafeBaggageDictionaryArbitrary(), (baggageItems) =>
+ {
+ try
+ {
+ var propagator = new BaggagePropagator();
+ var carrier = new Dictionary(StringComparer.Ordinal);
+ var propagationContext = new PropagationContext(default, Baggage.Create(baggageItems));
+
+ propagator.Inject(propagationContext, carrier, FuzzTestHelpers.Setter);
+
+ var extracted = propagator.Extract(default, carrier, FuzzTestHelpers.Getter);
+
+ return DictionariesEqual(baggageItems, extracted.Baggage.GetBaggage());
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+
+ [Property(MaxTest = MaxTests)]
+ public Property InjectedHeadersStayWithinConfiguredLimits() => Prop.ForAll(Generators.BaggageDictionaryArbitrary(), (baggageItems) =>
+ {
+ try
+ {
+ var propagator = new BaggagePropagator();
+ var carrier = new Dictionary(StringComparer.Ordinal);
+ var propagationContext = new PropagationContext(default, Baggage.Create(baggageItems));
+
+ propagator.Inject(propagationContext, carrier, FuzzTestHelpers.Setter);
+
+ return
+ !carrier.TryGetValue(BaggageHeader, out var headerValue) ||
+ (headerValue.Length <= 8192 && headerValue.Split(',').Length <= 180);
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+
+ [Property(MaxTest = MaxTests)]
+ public Property ExtractIsDeterministicForArbitraryHeaders() => Prop.ForAll(Generators.BaggageCarrierArbitrary(), (carrier) =>
+ {
+ try
+ {
+ var propagator = new BaggagePropagator();
+ var original = FuzzTestHelpers.CloneCarrier(carrier);
+
+ var first = propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
+ var second = propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
+
+ return
+ FuzzTestHelpers.CarriersEqual(original, carrier) &&
+ DictionariesEqual(first.Baggage.GetBaggage(), second.Baggage.GetBaggage());
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+
+ [Property(MaxTest = MaxTests)]
+ public Property ExtractMatchesReplayableGetterForSinglePassEnumerables() => Prop.ForAll(Generators.BaggageCarrierArbitrary(), (carrier) =>
+ {
+ try
+ {
+ var propagator = new BaggagePropagator();
+ var original = FuzzTestHelpers.CloneCarrier(carrier);
+
+ var replayable = propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
+ var singlePass = propagator.Extract(default, carrier, FuzzTestHelpers.SinglePassArrayGetter);
+
+ return
+ FuzzTestHelpers.CarriersEqual(original, carrier) &&
+ DictionariesEqual(replayable.Baggage.GetBaggage(), singlePass.Baggage.GetBaggage());
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+
+ [Property(MaxTest = MaxTests)]
+ public Property OversizedExtractionHonorsConfiguredLimits() => Prop.ForAll(Generators.OversizedBaggageValuesArbitrary(), (values) =>
+ {
+ try
+ {
+ var propagator = new BaggagePropagator();
+ var carrier = new Dictionary(StringComparer.Ordinal)
+ {
+ [BaggageHeader] = [string.Join(",", values.Select((value, index) => $"k{index:D4}={value}"))],
+ };
+
+ var extracted = propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
+
+ return DictionariesEqual(ExpectedBaggageForOversizedHeader(values), extracted.Baggage.GetBaggage());
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+
+ private static bool DictionariesEqual(IReadOnlyDictionary expected, IReadOnlyDictionary actual) =>
+ expected.Count == actual.Count &&
+ expected.All(pair => actual.TryGetValue(pair.Key, out var value) && value == pair.Value);
+
+ private static Dictionary ExpectedBaggageForOversizedHeader(string[] values)
+ {
+ var expected = new Dictionary(StringComparer.Ordinal);
+ var baggageLength = -1;
+
+ for (var i = 0; i < values.Length; i++)
+ {
+ var key = $"k{i:D4}";
+ baggageLength += key.Length + values[i].Length + 2; // key, equals sign, value, and comma.
+
+ if (baggageLength >= MaxBaggageLength || expected.Count >= MaxBaggageItems)
+ {
+ break;
+ }
+
+ expected[key] = values[i];
+ }
+
+ return expected;
+ }
+}
diff --git a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/FuzzTestHelpers.cs b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/FuzzTestHelpers.cs
new file mode 100644
index 00000000000..36a238e1c2c
--- /dev/null
+++ b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/FuzzTestHelpers.cs
@@ -0,0 +1,45 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+namespace OpenTelemetry.Api.FuzzTests;
+
+internal static class FuzzTestHelpers
+{
+ public static readonly Func, string, IEnumerable> Getter =
+ static (carrier, name) => carrier.TryGetValue(name, out var value) ? [value] : [];
+
+ public static readonly Func, string, IEnumerable> ArrayGetter =
+ static (carrier, name) => carrier.TryGetValue(name, out var value) ? value : [];
+
+ public static readonly Func, string, IEnumerable> SinglePassArrayGetter =
+ static (carrier, name) => carrier.TryGetValue(name, out var value) ? EnumerateOnce(value) : [];
+
+ public static readonly Action, string, string> Setter =
+ static (carrier, name, value) => carrier[name] = value;
+
+ public static Dictionary CloneCarrier(Dictionary carrier)
+ {
+ var clone = new Dictionary(carrier.Count, StringComparer.Ordinal);
+
+ foreach (var pair in carrier)
+ {
+ clone[pair.Key] = [.. pair.Value];
+ }
+
+ return clone;
+ }
+
+ public static bool CarriersEqual(Dictionary left, Dictionary right) =>
+ left.Count == right.Count &&
+ left.All(pair => right.TryGetValue(pair.Key, out var value) && pair.Value.SequenceEqual(value));
+
+ public static bool IsAllowedException(Exception ex) => ex is ArgumentException;
+
+ private static IEnumerable EnumerateOnce(IEnumerable values)
+ {
+ foreach (var value in values)
+ {
+ yield return value;
+ }
+ }
+}
diff --git a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs
new file mode 100644
index 00000000000..0f04abdb56a
--- /dev/null
+++ b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/Generators.cs
@@ -0,0 +1,243 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Diagnostics;
+using FsCheck;
+using FsCheck.Fluent;
+
+namespace OpenTelemetry.Api.FuzzTests;
+
+internal static class Generators
+{
+ private static readonly Gen LowerAlphaNumericChar = Gen.Elements("abcdefghijklmnopqrstuvwxyz0123456789".ToCharArray());
+ private static readonly Gen TraceStateKeyChar = Gen.Elements("abcdefghijklmnopqrstuvwxyz0123456789_-*/".ToCharArray());
+ private static readonly Gen TraceStateValueChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!#$%&'*+-.^_`|~:/".ToCharArray());
+ private static readonly Gen BaggageChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -_./:!$&'()*+;@?=,".ToCharArray());
+ private static readonly Gen CompactBaggageValueChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-".ToCharArray());
+ private static readonly Gen HeaderValueChar = Gen.Elements("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_=,;: ./@".ToCharArray());
+
+ public static Arbitrary ActivityContextArbitrary()
+ {
+ var gen =
+ from traceFlags in Gen.Elements(default, ActivityTraceFlags.Recorded)
+ from traceState in Gen.OneOf(
+ Gen.Constant((string?)null),
+ ValidTraceStateArbitrary().Generator.Select(static value => (string?)value))
+ select new ActivityContext(
+ ActivityTraceId.CreateRandom(),
+ ActivitySpanId.CreateRandom(),
+ traceFlags,
+ traceState);
+
+ return gen.ToArbitrary();
+ }
+
+ public static Arbitrary> SafeBaggageDictionaryArbitrary()
+ {
+ var pairGen =
+ from key in CreateString(BaggageChar, 1, 12)
+ from value in CreateString(BaggageChar, 1, 24)
+ select new KeyValuePair(key, value);
+
+ var gen = Gen.Sized(size =>
+ {
+ var maxCount = Math.Min(Math.Max(size + 1, 1), 20);
+
+ return
+ from count in Gen.Choose(0, maxCount)
+ from pairs in Gen.ArrayOf(pairGen, count)
+ select ToDictionary(pairs);
+ });
+
+ return gen.ToArbitrary();
+ }
+
+ public static Arbitrary> BaggageDictionaryArbitrary()
+ {
+ var pairGen =
+ from key in CreateString(BaggageChar, 0, 12)
+ from value in CreateString(BaggageChar, 0, 32)
+ select new KeyValuePair(key, value);
+
+ var gen = Gen.Sized(size =>
+ {
+ var maxCount = Math.Min(Math.Max(size + 1, 1), 256);
+
+ return
+ from count in Gen.Choose(0, maxCount)
+ from pairs in Gen.ArrayOf(pairGen, count)
+ select ToDictionary(pairs);
+ });
+
+ return gen.ToArbitrary();
+ }
+
+ public static Arbitrary> TraceContextCarrierArbitrary()
+ {
+ var gen =
+ from includeTraceParent in Gen.Elements(true, false)
+ from includeTraceState in Gen.Elements(true, false)
+ from traceParentValues in HeaderValuesArbitrary(4, 96).Generator
+ from traceStateValues in HeaderValuesArbitrary(4, 256).Generator
+ select CreateCarrier(
+ ("traceparent", includeTraceParent, traceParentValues),
+ ("tracestate", includeTraceState, traceStateValues));
+
+ return gen.ToArbitrary();
+ }
+
+ public static Arbitrary> BaggageCarrierArbitrary()
+ {
+ var gen =
+ from includeBaggage in Gen.Elements(true, false)
+ from baggageValues in HeaderValuesArbitrary(6, 512).Generator
+ select CreateCarrier(("baggage", includeBaggage, baggageValues));
+
+ return gen.ToArbitrary();
+ }
+
+ public static Arbitrary OversizedBaggageValuesArbitrary()
+ {
+ var valueGen = CreateString(CompactBaggageValueChar, 1, 64);
+ var gen = Gen.Sized(size =>
+ {
+ var maxCount = Math.Min(Math.Max((size * 4) + 181, 181), 512);
+
+ return
+ from count in Gen.Choose(181, maxCount)
+ from values in Gen.ArrayOf(valueGen, count)
+ select values;
+ });
+
+ return gen.ToArbitrary();
+ }
+
+ public static Arbitrary> B3MultipleHeaderCarrierArbitrary()
+ {
+ var gen =
+ from includeTraceId in Gen.Elements(true, false)
+ from includeSpanId in Gen.Elements(true, false)
+ from includeSampled in Gen.Elements(true, false)
+ from includeFlags in Gen.Elements(true, false)
+ from traceIdValues in HeaderValuesArbitrary(4, 64).Generator
+ from spanIdValues in HeaderValuesArbitrary(4, 32).Generator
+ from sampledValues in HeaderValuesArbitrary(4, 8).Generator
+ from flagsValues in HeaderValuesArbitrary(4, 8).Generator
+ select CreateCarrier(
+ ("X-B3-TraceId", includeTraceId, traceIdValues),
+ ("X-B3-SpanId", includeSpanId, spanIdValues),
+ ("X-B3-Sampled", includeSampled, sampledValues),
+ ("X-B3-Flags", includeFlags, flagsValues));
+
+ return gen.ToArbitrary();
+ }
+
+ public static Arbitrary> B3SingleHeaderCarrierArbitrary()
+ {
+ var gen =
+ from includeCombined in Gen.Elements(true, false)
+ from combinedValues in HeaderValuesArbitrary(4, 128).Generator
+ select CreateCarrier(("b3", includeCombined, combinedValues));
+
+ return gen.ToArbitrary();
+ }
+
+ public static Arbitrary DelimiterFloodArbitrary(char delimiter, int minLength = 1024, int maxLength = 50_000)
+ {
+ var gen =
+ from length in Gen.Choose(minLength, maxLength)
+ select new string(delimiter, length);
+
+ return gen.ToArbitrary();
+ }
+
+ public static Arbitrary ValidTraceStateArbitrary()
+ {
+ var memberGen =
+ from key in ValidTraceStateKey()
+ from value in CreateString(TraceStateValueChar, 1, 24)
+ select (key, value);
+
+ var gen = Gen.Sized(size =>
+ {
+ var maxCount = Math.Min(Math.Max(size + 1, 1), 16);
+
+ return
+ from count in Gen.Choose(1, maxCount)
+ from members in Gen.ArrayOf(memberGen, count)
+ select string.Join(
+ ",",
+ members.Select(static (member, index) => $"{member.key}{index}={member.value}"));
+ });
+
+ return gen.ToArbitrary();
+ }
+
+ private static Arbitrary HeaderValuesArbitrary(int maxCount, int maxLength)
+ {
+ var valueGen = CreateString(HeaderValueChar, 0, maxLength);
+ var gen = Gen.Sized(size =>
+ {
+ var count = Math.Min(Math.Max(size + 1, 1), maxCount);
+
+ return
+ from actualCount in Gen.Choose(0, count)
+ from values in Gen.ArrayOf(valueGen, actualCount)
+ select values;
+ });
+
+ return gen.ToArbitrary();
+ }
+
+ private static Gen CreateString(Gen charGen, int minLength, int maxLength) =>
+ from length in Gen.Choose(minLength, maxLength)
+ from chars in Gen.ArrayOf(charGen, length)
+ select new string(chars);
+
+ private static Gen ValidTraceStateKey()
+ {
+ var simpleKey =
+ from length in Gen.Choose(1, 12)
+ from first in LowerAlphaNumericChar
+ from rest in Gen.ArrayOf(TraceStateKeyChar, length - 1)
+ select $"{first}{new string(rest)}";
+
+ var vendorKey =
+ from tenantLength in Gen.Choose(1, 8)
+ from vendorLength in Gen.Choose(1, 6)
+ from tenantFirst in LowerAlphaNumericChar
+ from tenantRest in Gen.ArrayOf(TraceStateKeyChar, tenantLength - 1)
+ from vendorFirst in LowerAlphaNumericChar
+ from vendorRest in Gen.ArrayOf(TraceStateKeyChar, vendorLength - 1)
+ select $"{tenantFirst}{new string(tenantRest)}@{vendorFirst}{new string(vendorRest)}";
+
+ return Gen.OneOf(simpleKey, vendorKey);
+ }
+
+ private static Dictionary ToDictionary(IEnumerable> pairs)
+ {
+ var dictionary = new Dictionary(StringComparer.Ordinal);
+
+ foreach (var pair in pairs)
+ {
+ dictionary[pair.Key] = pair.Value;
+ }
+
+ return dictionary;
+ }
+
+ private static Dictionary CreateCarrier(params (string Key, bool Include, string[] Values)[] entries)
+ {
+ var carrier = new Dictionary(StringComparer.Ordinal);
+
+ foreach (var (key, include, values) in entries)
+ {
+ if (include)
+ {
+ carrier[key] = [.. values];
+ }
+ }
+
+ return carrier;
+ }
+}
diff --git a/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/TraceContextPropagatorFuzzTests.cs b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/TraceContextPropagatorFuzzTests.cs
new file mode 100644
index 00000000000..86f4d0ef50e
--- /dev/null
+++ b/test/OpenTelemetry.Api.FuzzTests/Context/Propagation/TraceContextPropagatorFuzzTests.cs
@@ -0,0 +1,90 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Diagnostics;
+using FsCheck;
+using FsCheck.Fluent;
+using FsCheck.Xunit;
+using OpenTelemetry.Context.Propagation;
+
+namespace OpenTelemetry.Api.FuzzTests;
+
+public class TraceContextPropagatorFuzzTests
+{
+ private const int MaxTests = 200;
+ private const string TraceParent = "traceparent";
+
+ [Property(MaxTest = MaxTests)]
+ public Property InjectExtractRoundTripPreservesValidContexts() => Prop.ForAll(Generators.ActivityContextArbitrary(), (activityContext) =>
+ {
+ try
+ {
+ var carrier = new Dictionary(StringComparer.Ordinal);
+ var propagator = new TraceContextPropagator();
+
+ propagator.Inject(new PropagationContext(activityContext, default), carrier, FuzzTestHelpers.Setter);
+
+ var extracted = propagator.Extract(default, carrier, FuzzTestHelpers.Getter);
+
+ return
+ carrier.TryGetValue(TraceParent, out var traceParent) &&
+ traceParent is not null &&
+ traceParent.Length == 55 &&
+ AreEquivalent(activityContext, extracted.ActivityContext);
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+
+ [Property(MaxTest = MaxTests)]
+ public Property ExtractIsDeterministicForArbitraryHeaders() => Prop.ForAll(Generators.TraceContextCarrierArbitrary(), (carrier) =>
+ {
+ try
+ {
+ var propagator = new TraceContextPropagator();
+ var original = FuzzTestHelpers.CloneCarrier(carrier);
+
+ var first = propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
+ var second = propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
+
+ return
+ FuzzTestHelpers.CarriersEqual(original, carrier) &&
+ AreEquivalent(first.ActivityContext, second.ActivityContext);
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+
+ [Property(MaxTest = MaxTests)]
+ public Property MultipleInjectionCallsAreConsistent() => Prop.ForAll(Generators.ActivityContextArbitrary(), activityContext =>
+ {
+ try
+ {
+ var propagator = new TraceContextPropagator();
+ var firstCarrier = new Dictionary(StringComparer.Ordinal);
+ var secondCarrier = new Dictionary(StringComparer.Ordinal);
+ var propagationContext = new PropagationContext(activityContext, default);
+
+ propagator.Inject(propagationContext, firstCarrier, FuzzTestHelpers.Setter);
+ propagator.Inject(propagationContext, secondCarrier, FuzzTestHelpers.Setter);
+
+ return
+ firstCarrier.Count == secondCarrier.Count &&
+ firstCarrier.All(pair => secondCarrier.TryGetValue(pair.Key, out var value) && value == pair.Value);
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+
+ private static bool AreEquivalent(ActivityContext expected, ActivityContext actual) =>
+ expected.TraceId == actual.TraceId &&
+ expected.SpanId == actual.SpanId &&
+ expected.TraceFlags == actual.TraceFlags &&
+ expected.TraceState == actual.TraceState;
+}
diff --git a/test/OpenTelemetry.Api.FuzzTests/OpenTelemetry.Api.FuzzTests.csproj b/test/OpenTelemetry.Api.FuzzTests/OpenTelemetry.Api.FuzzTests.csproj
new file mode 100644
index 00000000000..e4b8259f77b
--- /dev/null
+++ b/test/OpenTelemetry.Api.FuzzTests/OpenTelemetry.Api.FuzzTests.csproj
@@ -0,0 +1,15 @@
+
+
+
+ $(TargetFrameworksForTests)
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/B3PropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/B3PropagatorTests.cs
index e7f93d78848..5c8dd499623 100644
--- a/test/OpenTelemetry.Api.Tests/Context/Propagation/B3PropagatorTests.cs
+++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/B3PropagatorTests.cs
@@ -20,17 +20,9 @@ public class B3PropagatorTests
private static readonly ActivityTraceId TraceIdEightBytes = ActivityTraceId.CreateFromString(("0000000000000000" + TraceIdBase16EightBytes).AsSpan());
private static readonly ActivitySpanId SpanId = ActivitySpanId.CreateFromString(SpanIdBase16.AsSpan());
- private static readonly Action, string, string> Setter = (d, k, v) => d[k] = v;
+ private static readonly Action, string, string> Setter = static (d, k, v) => d[k] = v;
private static readonly Func, string, IEnumerable> Getter =
- (d, k) =>
- {
- if (d.TryGetValue(k, out var v))
- {
- return [v];
- }
-
- return [];
- };
+ static (d, k) => d.TryGetValue(k, out var v) ? [v] : [];
private readonly B3Propagator b3propagator = new();
private readonly B3Propagator b3PropagatorSingleHeader = new(true);
@@ -355,11 +347,23 @@ public void ParseMissingSpanId_SingleHeader()
}
[Fact]
- public void Fields_list()
- {
+ public void Fields_list() =>
ContainsExactly(
this.b3propagator.Fields,
[B3Propagator.XB3TraceId, B3Propagator.XB3SpanId, B3Propagator.XB3ParentSpanId, B3Propagator.XB3Sampled, B3Propagator.XB3Flags]);
+
+ [Fact]
+ public void ParseSingleHeaderWithManyDelimitersReturnsDefault()
+ {
+ var headerValue = new string('-', 50_000);
+ var headers = new Dictionary
+ {
+ { B3Propagator.XB3Combined, headerValue },
+ };
+
+ var result = this.b3PropagatorSingleHeader.Extract(default, headers, Getter);
+
+ Assert.Equal(default, result);
}
private static void ContainsExactly(ISet list, List items)
diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs
index b6633bdb53b..66423e91413 100644
--- a/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs
+++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/BaggagePropagatorTests.cs
@@ -8,27 +8,16 @@ namespace OpenTelemetry.Context.Propagation.Tests;
public class BaggagePropagatorTests
{
- private static readonly Func, string, IEnumerable> Getter =
- (d, k) =>
- {
- if (d.TryGetValue(k, out var v))
- {
- return [v];
- }
+ private const int MaxBaggageLength = 8192;
- return [];
- };
+ private static readonly Func, string, IEnumerable> Getter =
+ static (d, k) => d.TryGetValue(k, out var v) ? [v] : [];
private static readonly Func>, string, IEnumerable> GetterList =
- (d, k) =>
- {
- return d.Where(i => i.Key == k).Select(i => i.Value);
- };
+ static (d, k) => d.Where(i => i.Key == k).Select(i => i.Value);
- private static readonly Action, string, string> Setter = (carrier, name, value) =>
- {
- carrier[name] = value;
- };
+ private static readonly Action, string, string> Setter =
+ static (carrier, name, value) => carrier[name] = value;
private readonly BaggagePropagator baggage = new();
@@ -85,9 +74,9 @@ public void ValidateMultipleBaggageExtraction()
{
var carrier = new List>
{
- new KeyValuePair(BaggagePropagator.BaggageHeaderName, "name1=test1"),
- new KeyValuePair(BaggagePropagator.BaggageHeaderName, "name2=test2"),
- new KeyValuePair(BaggagePropagator.BaggageHeaderName, "name2=test2"),
+ new(BaggagePropagator.BaggageHeaderName, "name1=test1"),
+ new(BaggagePropagator.BaggageHeaderName, "name2=test2"),
+ new(BaggagePropagator.BaggageHeaderName, "name2=test2"),
};
var propagationContext = this.baggage.Extract(default, carrier, GetterList);
@@ -140,7 +129,7 @@ public void ValidateSpecialCharsBaggageExtraction()
var initialBaggage = $"key+1=value+1,{encodedKey}={encodedValue},{escapedKey}={escapedValue}";
var carrier = new List>
{
- new KeyValuePair(BaggagePropagator.BaggageHeaderName, initialBaggage),
+ new(BaggagePropagator.BaggageHeaderName, initialBaggage),
};
var propagationContext = this.baggage.Extract(default, carrier, GetterList);
@@ -356,7 +345,7 @@ public void ValidateInjectionOfSixtyFourEntries()
}
[Fact]
- public void ValidateInjectionOf8192Bytes()
+ public void ValidateInjectionOfMaximumLength()
{
var longValue = new string('0', 8190);
@@ -375,7 +364,59 @@ public void ValidateInjectionOf8192Bytes()
var baggageHeader = carrier[BaggagePropagator.BaggageHeaderName];
- Assert.Equal(8192, baggageHeader.Length);
+ Assert.Equal(MaxBaggageLength, baggageHeader.Length);
+ }
+
+ [Theory]
+ [InlineData(8187)]
+ [InlineData(8188)]
+ [InlineData(8189)]
+ [InlineData(8190)]
+ public void ValidateInjectionStopsBeforeExceedingMaximumLength(int length)
+ {
+ var longValue = new string('0', length);
+
+ var propagationContext = new PropagationContext(
+ default,
+ new Baggage(new Dictionary
+ {
+ ["a"] = longValue,
+ ["b"] = "c",
+ }));
+
+ var carrier = new Dictionary();
+
+ this.baggage.Inject(propagationContext, carrier, Setter);
+
+ var item = Assert.Single(carrier);
+
+ Assert.Equal(BaggagePropagator.BaggageHeaderName, item.Key);
+
+ var baggageHeader = item.Value;
+ Assert.True(baggageHeader.Length <= MaxBaggageLength, $"Baggage length {baggageHeader.Length} exceeds maximum allowed length of {MaxBaggageLength}");
+ Assert.Equal($"a={longValue}", baggageHeader);
+ }
+
+ [Theory]
+ [InlineData(8191)]
+ [InlineData(16384)]
+ public void ValidateInjectionDoesNotExceedMaximumLength(int length)
+ {
+ var longValue = new string('0', length);
+
+ var propagationContext = new PropagationContext(
+ default,
+ new Baggage(new Dictionary
+ {
+ ["a"] = longValue,
+ ["b"] = "c",
+ }));
+
+ var carrier = new Dictionary();
+
+ this.baggage.Inject(propagationContext, carrier, Setter);
+
+ Assert.Empty(carrier);
}
[Fact]
@@ -398,7 +439,7 @@ public void ValidateMaxByteManyEntriesInjection()
var baggageHeader = carrier[BaggagePropagator.BaggageHeaderName];
- Assert.True(baggageHeader.Length <= 8192);
+ Assert.True(baggageHeader.Length <= MaxBaggageLength);
}
[Fact]
@@ -446,6 +487,26 @@ public void ValidateValueWithMultipleEqualsPreservesEquals()
Assert.Equal("value=more=equals", extractedBaggage["key"]);
}
+ [Fact]
+ public void ValidateOversizedBaggageExtractionHonorsLimits()
+ {
+ var entries = Enumerable.Range(0, 5000).Select(i => $"k{i:D4}=v");
+ var headerValue = string.Join(",", entries);
+ var carrier = new Dictionary
+ {
+ { BaggagePropagator.BaggageHeaderName, headerValue },
+ };
+
+ var propagationContext = this.baggage.Extract(default, carrier, Getter);
+ var baggage = propagationContext.Baggage.GetBaggage();
+
+ Assert.NotEqual(default, propagationContext);
+ Assert.Equal(180, baggage.Count);
+ Assert.Equal("v", baggage["k0000"]);
+ Assert.Equal("v", baggage["k0179"]);
+ Assert.False(baggage.ContainsKey("k0180"));
+ }
+
[Fact(Skip = "Fails due to spec mismatch, tracked in https://github.com/open-telemetry/opentelemetry-dotnet/issues/5210")]
public void ValidateSpecialCharactersInjection()
{
diff --git a/test/OpenTelemetry.Api.Tests/Context/Propagation/TracestateUtilsTests.cs b/test/OpenTelemetry.Api.Tests/Context/Propagation/TracestateUtilsTests.cs
index ae9de6f0ff0..58f6aae4438 100644
--- a/test/OpenTelemetry.Api.Tests/Context/Propagation/TracestateUtilsTests.cs
+++ b/test/OpenTelemetry.Api.Tests/Context/Propagation/TracestateUtilsTests.cs
@@ -100,4 +100,31 @@ public void ValidPairs(string tracestate)
Assert.Equal("k1=v1,k2=v2", TraceStateUtils.GetString(tracestateEntries));
}
+
+ [Fact]
+ public void GetString_RemovesLargeEntriesFirstWhenTruncating()
+ {
+ var tracestateEntries = new List>
+ {
+ new("big", new string('a', 196)),
+ };
+
+ tracestateEntries.AddRange(Enumerable.Range(0, 17).Select(i => new KeyValuePair($"k{i:00}", new string('a', 15))));
+
+ Assert.Equal(
+ string.Join(",", Enumerable.Range(0, 17).Select(i => $"k{i:00}={new string('a', 15)}")),
+ TraceStateUtils.GetString(tracestateEntries));
+ }
+
+ [Fact]
+ public void GetString_RemovesEntriesFromEndWhenStillTooLong()
+ {
+ var tracestateEntries = Enumerable.Range(0, 32)
+ .Select(i => new KeyValuePair($"k{i:00}", new string('a', 20)))
+ .ToList();
+
+ Assert.Equal(
+ string.Join(",", Enumerable.Range(0, 20).Select(i => $"k{i:00}={new string('a', 20)}")),
+ TraceStateUtils.GetString(tracestateEntries));
+ }
}
diff --git a/test/OpenTelemetry.Extensions.Propagators.FuzzTests/Context/Propagation/B3PropagatorFuzzTests.cs b/test/OpenTelemetry.Extensions.Propagators.FuzzTests/Context/Propagation/B3PropagatorFuzzTests.cs
new file mode 100644
index 00000000000..4ea44f4fab7
--- /dev/null
+++ b/test/OpenTelemetry.Extensions.Propagators.FuzzTests/Context/Propagation/B3PropagatorFuzzTests.cs
@@ -0,0 +1,38 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using FsCheck;
+using FsCheck.Fluent;
+using FsCheck.Xunit;
+using ExtensionsB3Propagator = OpenTelemetry.Extensions.Propagators.B3Propagator;
+
+namespace OpenTelemetry.Extensions.Propagators.FuzzTests;
+
+[Obsolete("B3Propagator is obsolete but intentionally fuzz tested.")]
+public class B3PropagatorFuzzTests
+{
+ private const int MaxTests = 25;
+ private const string CombinedHeader = "b3";
+
+ [Property(MaxTest = MaxTests)]
+ public Property SingleHeaderDelimiterFloodReturnsDefault() => Prop.ForAll(Generators.DelimiterFloodArbitrary('-'), (headerValue) =>
+ {
+ try
+ {
+ var propagator = new ExtensionsB3Propagator(singleHeader: true);
+ var carrier = new Dictionary(StringComparer.Ordinal)
+ {
+ [CombinedHeader] = [headerValue],
+ };
+ var original = FuzzTestHelpers.CloneCarrier(carrier);
+
+ var extracted = propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
+
+ return extracted.Equals(default) && FuzzTestHelpers.CarriersEqual(original, carrier);
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+}
diff --git a/test/OpenTelemetry.Extensions.Propagators.FuzzTests/Context/Propagation/FuzzTestHelpers.cs b/test/OpenTelemetry.Extensions.Propagators.FuzzTests/Context/Propagation/FuzzTestHelpers.cs
new file mode 100644
index 00000000000..0968cc2beec
--- /dev/null
+++ b/test/OpenTelemetry.Extensions.Propagators.FuzzTests/Context/Propagation/FuzzTestHelpers.cs
@@ -0,0 +1,28 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+namespace OpenTelemetry.Extensions.Propagators.FuzzTests;
+
+internal static class FuzzTestHelpers
+{
+ public static readonly Func, string, IEnumerable> ArrayGetter =
+ static (carrier, name) => carrier.TryGetValue(name, out var value) ? value : [];
+
+ public static Dictionary CloneCarrier(Dictionary carrier)
+ {
+ var clone = new Dictionary(carrier.Count, StringComparer.Ordinal);
+
+ foreach (var pair in carrier)
+ {
+ clone[pair.Key] = [.. pair.Value];
+ }
+
+ return clone;
+ }
+
+ public static bool CarriersEqual(Dictionary left, Dictionary right) =>
+ left.Count == right.Count &&
+ left.All(pair => right.TryGetValue(pair.Key, out var value) && pair.Value.SequenceEqual(value));
+
+ public static bool IsAllowedException(Exception ex) => ex is ArgumentException;
+}
diff --git a/test/OpenTelemetry.Extensions.Propagators.FuzzTests/Context/Propagation/Generators.cs b/test/OpenTelemetry.Extensions.Propagators.FuzzTests/Context/Propagation/Generators.cs
new file mode 100644
index 00000000000..fdc2bfdc9a8
--- /dev/null
+++ b/test/OpenTelemetry.Extensions.Propagators.FuzzTests/Context/Propagation/Generators.cs
@@ -0,0 +1,29 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using FsCheck;
+using FsCheck.Fluent;
+
+namespace OpenTelemetry.Extensions.Propagators.FuzzTests;
+
+internal static class Generators
+{
+ public static Arbitrary DelimiterFloodArbitrary(char delimiter, int minLength = 1024, int maxLength = 50_000)
+ {
+ var gen =
+ from length in Gen.Choose(minLength, maxLength)
+ select new string(delimiter, length);
+
+ return gen.ToArbitrary();
+ }
+
+ public static Arbitrary JaegerManyComponentHeaderArbitrary(int minParts = 1024, int maxParts = 50_000)
+ {
+ var gen =
+ from delimiter in Gen.Elements(":", "%3A")
+ from count in Gen.Choose(minParts, maxParts)
+ select string.Join(delimiter, Enumerable.Repeat("part", count));
+
+ return gen.ToArbitrary();
+ }
+}
diff --git a/test/OpenTelemetry.Extensions.Propagators.FuzzTests/Context/Propagation/JaegerPropagatorFuzzTests.cs b/test/OpenTelemetry.Extensions.Propagators.FuzzTests/Context/Propagation/JaegerPropagatorFuzzTests.cs
new file mode 100644
index 00000000000..d6aa99936b5
--- /dev/null
+++ b/test/OpenTelemetry.Extensions.Propagators.FuzzTests/Context/Propagation/JaegerPropagatorFuzzTests.cs
@@ -0,0 +1,38 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+using FsCheck;
+using FsCheck.Fluent;
+using FsCheck.Xunit;
+using ExtensionsJaegerPropagator = OpenTelemetry.Extensions.Propagators.JaegerPropagator;
+
+namespace OpenTelemetry.Extensions.Propagators.FuzzTests;
+
+[Obsolete("JaegerPropagator is obsolete but intentionally fuzz tested.")]
+public class JaegerPropagatorFuzzTests
+{
+ private const int MaxTests = 25;
+ private const string JaegerHeader = "uber-trace-id";
+
+ [Property(MaxTest = MaxTests)]
+ public Property ManyComponentHeadersReturnDefault() => Prop.ForAll(Generators.JaegerManyComponentHeaderArbitrary(), (headerValue) =>
+ {
+ try
+ {
+ var propagator = new ExtensionsJaegerPropagator();
+ var carrier = new Dictionary(StringComparer.Ordinal)
+ {
+ [JaegerHeader] = [headerValue],
+ };
+ var original = FuzzTestHelpers.CloneCarrier(carrier);
+
+ var extracted = propagator.Extract(default, carrier, FuzzTestHelpers.ArrayGetter);
+
+ return extracted.Equals(default) && FuzzTestHelpers.CarriersEqual(original, carrier);
+ }
+ catch (Exception ex) when (FuzzTestHelpers.IsAllowedException(ex))
+ {
+ return true;
+ }
+ });
+}
diff --git a/test/OpenTelemetry.Extensions.Propagators.FuzzTests/OpenTelemetry.Extensions.Propagators.FuzzTests.csproj b/test/OpenTelemetry.Extensions.Propagators.FuzzTests/OpenTelemetry.Extensions.Propagators.FuzzTests.csproj
new file mode 100644
index 00000000000..678b0b3cc00
--- /dev/null
+++ b/test/OpenTelemetry.Extensions.Propagators.FuzzTests/OpenTelemetry.Extensions.Propagators.FuzzTests.csproj
@@ -0,0 +1,15 @@
+
+
+
+ $(TargetFrameworksForTests)
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTests.cs b/test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTests.cs
index b390e7d1d36..99691496610 100644
--- a/test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTests.cs
+++ b/test/OpenTelemetry.Extensions.Propagators.Tests/B3PropagatorTests.cs
@@ -21,17 +21,9 @@ public class B3PropagatorTests
private static readonly ActivityTraceId TraceIdEightBytes = ActivityTraceId.CreateFromString(("0000000000000000" + TraceIdBase16EightBytes).AsSpan());
private static readonly ActivitySpanId SpanId = ActivitySpanId.CreateFromString(SpanIdBase16.AsSpan());
- private static readonly Action, string, string> Setter = (d, k, v) => d[k] = v;
+ private static readonly Action, string, string> Setter = static (d, k, v) => d[k] = v;
private static readonly Func, string, IEnumerable> Getter =
- (d, k) =>
- {
- if (d.TryGetValue(k, out var v))
- {
- return [v];
- }
-
- return [];
- };
+ static (d, k) => d.TryGetValue(k, out var v) ? [v] : [];
private readonly B3Propagator b3propagator = new();
private readonly B3Propagator b3PropagatorSingleHeader = new(true);
@@ -243,6 +235,19 @@ public void ParseSampled_SingleHeader()
this.b3PropagatorSingleHeader.Extract(default, headersSampled, Getter));
}
+ [Fact]
+ public void ParseLegacySampled_SingleHeader()
+ {
+ var headersSampled = new Dictionary
+ {
+ { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-true" },
+ };
+
+ Assert.Equal(
+ new PropagationContext(new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true), default),
+ this.b3PropagatorSingleHeader.Extract(default, headersSampled, Getter));
+ }
+
[Fact]
public void ParseZeroSampled_SingleHeader()
{
@@ -328,6 +333,13 @@ public void ParseMissingTraceId_SingleHeader()
Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter));
}
+ [Fact]
+ public void ParseSingleHeaderWithoutDelimiterReturnsDefault()
+ {
+ var invalidHeaders = new Dictionary { { B3Propagator.XB3Combined, TraceIdBase16 } };
+ Assert.Equal(default, this.b3PropagatorSingleHeader.Extract(default, invalidHeaders, Getter));
+ }
+
[Fact]
public void ParseInvalidSpanId_SingleHeader()
{
@@ -364,6 +376,48 @@ public void Fields_list()
[B3Propagator.XB3TraceId, B3Propagator.XB3SpanId, B3Propagator.XB3ParentSpanId, B3Propagator.XB3Sampled, B3Propagator.XB3Flags]);
}
+ [Fact]
+ public void ParseSingleHeaderWithManyDelimitersReturnsDefault()
+ {
+ var headerValue = new string('-', 50_000);
+ var headers = new Dictionary
+ {
+ { B3Propagator.XB3Combined, headerValue },
+ };
+
+ var result = this.b3PropagatorSingleHeader.Extract(default, headers, Getter);
+
+ Assert.Equal(default, result);
+ }
+
+ [Fact]
+ public void ParseSingleHeaderWithFourthPartReturnsContext()
+ {
+ var headers = new Dictionary
+ {
+ { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1-parent" },
+ };
+
+ var result = this.b3PropagatorSingleHeader.Extract(default, headers, Getter);
+
+ Assert.Equal(
+ new PropagationContext(new ActivityContext(TraceId, SpanId, TraceOptions, isRemote: true), default),
+ result);
+ }
+
+ [Fact]
+ public void ParseSingleHeaderWithTooManyPartsReturnsDefault()
+ {
+ var headers = new Dictionary
+ {
+ { B3Propagator.XB3Combined, $"{TraceIdBase16}-{SpanIdBase16}-1-parent-extra" },
+ };
+
+ var result = this.b3PropagatorSingleHeader.Extract(default, headers, Getter);
+
+ Assert.Equal(default, result);
+ }
+
private static void ContainsExactly(ISet list, List items)
{
Assert.Equal(items.Count, list.Count);
diff --git a/test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTests.cs b/test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTests.cs
index ebcb914f0aa..0e00823d267 100644
--- a/test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTests.cs
+++ b/test/OpenTelemetry.Extensions.Propagators.Tests/JaegerPropagatorTests.cs
@@ -7,6 +7,7 @@
namespace OpenTelemetry.Extensions.Propagators.Tests;
+[Obsolete("We still want to test obsolete APIs.")]
public class JaegerPropagatorTests
{
private const string JaegerHeader = "uber-trace-id";
@@ -21,15 +22,11 @@ public class JaegerPropagatorTests
private const string FlagSampled = "1";
private const string FlagNotSampled = "0";
- private static readonly Func, string, IEnumerable> Getter = (headers, name) =>
- {
- return headers.TryGetValue(name, out var value) ? value : [];
- };
+ private static readonly Func, string, IEnumerable> Getter =
+ static (headers, name) => headers.TryGetValue(name, out var value) ? value : [];
- private static readonly Action, string, string> Setter = (carrier, name, value) =>
- {
- carrier[name] = value;
- };
+ private static readonly Action, string, string> Setter =
+ static (carrier, name, value) => carrier[name] = value;
[Fact]
public void ExtractReturnsOriginalContextIfContextIsAlreadyValid()
@@ -44,9 +41,7 @@ public void ExtractReturnsOriginalContextIfContextIsAlreadyValid()
var headers = new Dictionary();
// act
-#pragma warning disable CS0618 // Type or member is obsolete
var result = new JaegerPropagator().Extract(propagationContext, headers, Getter);
-#pragma warning restore CS0618
// assert
Assert.Equal(propagationContext, result);
@@ -59,9 +54,7 @@ public void ExtractReturnsOriginalContextIfCarrierIsNull()
var propagationContext = default(PropagationContext);
// act
-#pragma warning disable CS0618 // Type or member is obsolete
var result = new JaegerPropagator().Extract(propagationContext, null, Getter!);
-#pragma warning restore CS0618
// assert
Assert.Equal(propagationContext, result);
@@ -76,9 +69,7 @@ public void ExtractReturnsOriginalContextIfGetterIsNull()
var headers = new Dictionary();
// act
-#pragma warning disable CS0618 // Type or member is obsolete
var result = new JaegerPropagator().Extract(propagationContext, headers, null!);
-#pragma warning restore CS0618
// assert
Assert.Equal(propagationContext, result);
@@ -108,9 +99,7 @@ public void ExtractReturnsOriginalContextIfHeaderIsNotValid(string traceId, stri
var headers = new Dictionary { { JaegerHeader, [formattedHeader] } };
// act
-#pragma warning disable CS0618 // Type or member is obsolete
var result = new JaegerPropagator().Extract(propagationContext, headers, Getter);
-#pragma warning restore CS0618
// assert
Assert.Equal(propagationContext, result);
@@ -149,9 +138,7 @@ public void ExtractReturnsNewContextIfHeaderIsValid(string traceId, string spanI
var headers = new Dictionary { { JaegerHeader, [formattedHeader] } };
// act
-#pragma warning disable CS0618 // Type or member is obsolete
var result = new JaegerPropagator().Extract(propagationContext, headers, Getter);
-#pragma warning restore CS0618
// assert
Assert.Equal(traceId.PadLeft(TraceId.Length, '0'), result.ActivityContext.TraceId.ToString());
@@ -159,6 +146,22 @@ public void ExtractReturnsNewContextIfHeaderIsValid(string traceId, string spanI
Assert.Equal(flags == "1" ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None, result.ActivityContext.TraceFlags);
}
+ [Fact]
+ public void ExtractReturnsNewContextIfHeaderContainsEmptyComponent()
+ {
+ var formattedHeader = $"{TraceId}{JaegerDelimiter}{JaegerDelimiter}{SpanId}{JaegerDelimiter}{ParentSpanId}{JaegerDelimiter}{FlagSampled}";
+ var headers = new Dictionary
+ {
+ [JaegerHeader] = [formattedHeader],
+ };
+
+ var result = new JaegerPropagator().Extract(default, headers, Getter);
+
+ Assert.Equal(TraceId, result.ActivityContext.TraceId.ToString());
+ Assert.Equal(SpanId, result.ActivityContext.SpanId.ToString());
+ Assert.Equal(ActivityTraceFlags.Recorded, result.ActivityContext.TraceFlags);
+ }
+
[Fact]
public void InjectDoesNoopIfContextIsInvalid()
{
@@ -168,9 +171,7 @@ public void InjectDoesNoopIfContextIsInvalid()
var headers = new Dictionary();
// act
-#pragma warning disable CS0618 // Type or member is obsolete
new JaegerPropagator().Inject(propagationContext, headers, Setter);
-#pragma warning restore CS0618
// assert
Assert.Empty(headers);
@@ -187,9 +188,7 @@ public void InjectDoesNoopIfCarrierIsNull()
default);
// act
-#pragma warning disable CS0618 // Type or member is obsolete
new JaegerPropagator().Inject(propagationContext, null, Setter!);
-#pragma warning restore CS0618
// assert
}
@@ -207,9 +206,7 @@ public void InjectDoesNoopIfSetterIsNull()
var headers = new Dictionary();
// act
-#pragma warning disable CS0618 // Type or member is obsolete
new JaegerPropagator().Inject(propagationContext, headers, null!);
-#pragma warning restore CS0618
// assert
Assert.Empty(headers);
@@ -237,12 +234,21 @@ public void InjectWillAddJaegerFormattedTraceToCarrier(string sampledFlag)
var headers = new Dictionary();
// act
-#pragma warning disable CS0618 // Type or member is obsolete
new JaegerPropagator().Inject(propagationContext, headers, Setter);
-#pragma warning restore CS0618
// assert
Assert.Single(headers);
Assert.Equal(expectedValue, headers[JaegerHeader]);
}
+
+ [Fact]
+ public void ExtractHeaderWithManyDelimitersReturnsDefault()
+ {
+ var formattedHeader = string.Join(JaegerDelimiter, Enumerable.Repeat("part", 50_000));
+ var headers = new Dictionary { { JaegerHeader, [formattedHeader] } };
+
+ var result = new JaegerPropagator().Extract(default, headers, Getter);
+
+ Assert.Equal(default, result);
+ }
}
diff --git a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs
index bedc451b06e..5e1364989bf 100644
--- a/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs
+++ b/test/OpenTelemetry.Tests/Trace/Propagation/TraceContextPropagatorTests.cs
@@ -13,31 +13,14 @@ public class TraceContextPropagatorTests
private const string TraceId = "0af7651916cd43dd8448eb211c80319c";
private const string SpanId = "b9c7c989f97918e1";
- private static readonly string[] Empty = [];
- private static readonly Func, string, IEnumerable> Getter = (headers, name) =>
- {
- if (headers.TryGetValue(name, out var value))
- {
- return [value];
- }
-
- return Empty;
- };
-
- private static readonly Func, string, IEnumerable> ArrayGetter = (headers, name) =>
- {
- if (headers.TryGetValue(name, out var value))
- {
- return value;
- }
+ private static readonly Func, string, IEnumerable> Getter =
+ static (headers, name) => headers.TryGetValue(name, out var value) ? [value] : [];
- return [];
- };
+ private static readonly Func, string, IEnumerable> ArrayGetter =
+ static (headers, name) => headers.TryGetValue(name, out var value) ? value : [];
- private static readonly Action, string, string> Setter = (carrier, name, value) =>
- {
- carrier[name] = value;
- };
+ private static readonly Action, string, string> Setter =
+ static (carrier, name, value) => carrier[name] = value;
[Fact]
public void CanParseExampleFromSpec()
@@ -56,7 +39,7 @@ public void CanParseExampleFromSpec()
Assert.True(ctx.ActivityContext.IsRemote);
Assert.True(ctx.ActivityContext.IsValid());
- Assert.True((ctx.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) != 0);
+ Assert.NotEqual(0, (int)(ctx.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded));
Assert.Equal($"congo=lZWRzIHRoNhcm5hbCBwbGVhc3VyZS4,rojo=00-{TraceId}-00f067aa0ba902b7-01", ctx.ActivityContext.TraceState);
}
@@ -74,7 +57,7 @@ public void NotSampled()
Assert.Equal(ActivityTraceId.CreateFromString(TraceId.AsSpan()), ctx.ActivityContext.TraceId);
Assert.Equal(ActivitySpanId.CreateFromString(SpanId.AsSpan()), ctx.ActivityContext.SpanId);
- Assert.True((ctx.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded) == 0);
+ Assert.Equal(0, (int)(ctx.ActivityContext.TraceFlags & ActivityTraceFlags.Recorded));
Assert.True(ctx.ActivityContext.IsRemote);
Assert.True(ctx.ActivityContext.IsValid());
@@ -177,6 +160,24 @@ public void Inject_WithTracestate()
Assert.Equal(expectedHeaders, carrier);
}
+ [Fact]
+ public void Inject_TruncatesOversizedTracestate()
+ {
+ var traceId = ActivityTraceId.CreateRandom();
+ var spanId = ActivitySpanId.CreateRandom();
+ var expectedTraceState = string.Join(",", Enumerable.Range(0, 17).Select(i => $"k{i:00}={new string('a', 15)}"));
+ var oversizedTraceState = $"big={new string('a', 196)},{expectedTraceState}";
+
+ var activityContext = new ActivityContext(traceId, spanId, ActivityTraceFlags.Recorded, oversizedTraceState);
+ var propagationContext = new PropagationContext(activityContext, default);
+ var carrier = new Dictionary();
+ var f = new TraceContextPropagator();
+ f.Inject(propagationContext, carrier, Setter);
+
+ Assert.Equal($"00-{traceId}-{spanId}-01", carrier[TraceParent]);
+ Assert.Equal(expectedTraceState, carrier[TraceState]);
+ }
+
[Fact]
public void DuplicateKeys()
{