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() {