diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs b/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs index 482a58bb414ea..7f9de8f30d4bf 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs @@ -253,7 +253,25 @@ public sealed class ActivityListener : IDisposable public System.Diagnostics.SampleActivity? SampleUsingParentId { get { throw null; } set { throw null; } } public System.Diagnostics.SampleActivity? Sample { get { throw null; } set { throw null; } } public void Dispose() { throw null; } - } + } + + + public abstract class TextMapPropagator + { + public delegate void PropagatorGetterCallback(object carrier, string fieldName, out string? value, out System.Collections.Generic.IEnumerable? values); + public abstract System.Collections.Generic.IReadOnlyCollection Fields { get; } + public abstract bool Inject(System.Diagnostics.Activity activity, object carrier, Action setter); + public abstract bool Inject(System.Diagnostics.ActivityContext context, object carrier, Action setter); + public abstract bool Inject(System.Collections.Generic.IEnumerable> baggage, object carrier, Action setter); + public abstract bool Extract(object carrier, PropagatorGetterCallback getter, out string? id, out string? state); + public abstract bool Extract(object carrier, PropagatorGetterCallback getter, out System.Diagnostics.ActivityContext context); + public abstract bool Extract(object carrier, PropagatorGetterCallback getter, out System.Collections.Generic.IEnumerable>? baggage); + public static TextMapPropagator Current { get; set; } + public static TextMapPropagator CreateLegacyPropagator() { throw null; } + public static TextMapPropagator CreatePassThroughPropagator() { throw null; } + public static TextMapPropagator CreateNoOutputPropagator() { throw null; } + public static TextMapPropagator CreateW3CPropagator() { throw null; } + } } namespace System.Diagnostics.Metrics diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj index 6c8fbb0203c4a..1d9c44bcdc927 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj @@ -51,6 +51,7 @@ + diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/TextMapPropagator.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/TextMapPropagator.cs new file mode 100644 index 0000000000000..334cf3bed94ec --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/TextMapPropagator.cs @@ -0,0 +1,564 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using System.Net; +using System.Collections.Generic; + +namespace System.Diagnostics +{ + public abstract class TextMapPropagator + { + public delegate void PropagatorGetterCallback(object carrier, string fieldName, out string? value, out IEnumerable? values); + + public abstract IReadOnlyCollection Fields { get; } + + // Inject + + public abstract bool Inject(Activity activity, object carrier, Action setter); + public abstract bool Inject(ActivityContext context, object carrier, Action setter); + public abstract bool Inject(IEnumerable> baggage, object carrier, Action setter); + + // Extract + + public abstract bool Extract(object carrier, PropagatorGetterCallback getter, out string? id, out string? state); + public abstract bool Extract(object carrier, PropagatorGetterCallback getter, out ActivityContext context); + public abstract bool Extract(object carrier, PropagatorGetterCallback getter, out IEnumerable>? baggage); + + // + // Static APIs + // + + public static TextMapPropagator Current { get; set; } = CreateLegacyPropagator(); + + // For Microsoft compatibility. e.g., it will propagate Baggage header name as "Correlation-Context" instead of "baggage". + public static TextMapPropagator CreateLegacyPropagator() => new LegacyTextMapPropagator(); + + // Suppress context propagation. + public static TextMapPropagator CreateNoOutputPropagator() => new OutputSuppressionPropagator(); + + // propagate only root parent context and ignore any intermediate created context. + public static TextMapPropagator CreatePassThroughPropagator() => new PassThroughPropagator(); + + // Conform to the W3C specs https://www.w3.org/TR/trace-context/ & https://www.w3.org/TR/2020/WD-baggage-20201020/ + public static TextMapPropagator CreateW3CPropagator() => new W3CPropagator(); + + // + // Internal + // + + internal const char Space = ' '; + internal const char Tab = (char)9; + internal const char Comma = ','; + internal const char Semicolon = ';'; + + internal const int MaxBaggageLength = 8192; + internal const int MaxKeyValueLength = 4096; + internal const int MaxBaggageItems = 180; + + internal const string TraceParent = "traceparent"; + internal const string RequestId = "Request-Id"; + internal const string TraceState = "tracestate"; + internal const string Baggage = "baggage"; + internal const string CorrelationContext = "Correlation-Context"; + + internal static readonly char [] s_trimmingSpaceCharacters = new char[] { Space, Tab }; + + internal static void InjectBaggage(object carrier, IEnumerable> baggage, Action setter, bool injectAsW3C = false) + { + using (IEnumerator> e = baggage.GetEnumerator()) + { + if (e.MoveNext()) + { + StringBuilder baggageList = new StringBuilder(); + do + { + KeyValuePair item = e.Current; + baggageList.Append(WebUtility.UrlEncode(item.Key)).Append('=').Append(WebUtility.UrlEncode(item.Value)).Append(Comma); + } + while (e.MoveNext()); + baggageList.Remove(baggageList.Length - 1, 1); + setter(carrier, injectAsW3C ? Baggage : CorrelationContext, baggageList.ToString()); + } + } + } + + internal static bool TryExtractBaggage(string baggagestring, out IEnumerable>? baggage) + { + baggage = null; + int baggageLength = -1; + List>? baggageList = null; + + if (string.IsNullOrEmpty(baggagestring)) + { + return true; + } + + int currentIndex = 0; + + do + { + // Skip spaces + while (currentIndex < baggagestring.Length && (baggagestring[currentIndex] == Space || baggagestring[currentIndex] == Tab)) { currentIndex++; } + + if (currentIndex >= baggagestring.Length) { break; } // No Key exist + + int keyStart = currentIndex; + + // Search end of the key + while (currentIndex < baggagestring.Length && baggagestring[currentIndex] != Space && baggagestring[currentIndex] != Tab && baggagestring[currentIndex] != '=') { currentIndex++; } + + if (currentIndex >= baggagestring.Length) { break; } + + int keyEnd = currentIndex; + + if (baggagestring[currentIndex] != '=') + { + // Skip Spaces + while (currentIndex < baggagestring.Length && (baggagestring[currentIndex] == Space || baggagestring[currentIndex] == Tab)) { currentIndex++; } + + if (currentIndex >= baggagestring.Length) { break; } // Wrong key format + } + + if (baggagestring[currentIndex] != '=') { break; } // wrong key format. + + currentIndex++; + + // Skip spaces + while (currentIndex < baggagestring.Length && (baggagestring[currentIndex] == Space || baggagestring[currentIndex] == Tab)) { currentIndex++; } + + if (currentIndex >= baggagestring.Length) { break; } // Wrong value format + + int valueStart = currentIndex; + + // Search end of the value + while (currentIndex < baggagestring.Length && baggagestring[currentIndex] != Space && baggagestring[currentIndex] != Tab && + baggagestring[currentIndex] != Comma && baggagestring[currentIndex] != Semicolon) + { currentIndex++; } + + if (keyStart < keyEnd && valueStart < currentIndex) + { + int keyValueLength = (keyEnd - keyStart) + (currentIndex - valueStart); + if (keyValueLength > MaxKeyValueLength || keyValueLength + baggageLength >= MaxBaggageLength) + { + break; + } + + if (baggageList is null) + { + baggageList = new(); + } + + baggageLength += keyValueLength; + + // Insert in reverse order for asp.net compatability. + baggageList.Insert(0, new KeyValuePair( + WebUtility.UrlDecode(baggagestring.Substring(keyStart, keyEnd - keyStart)).Trim(s_trimmingSpaceCharacters), + WebUtility.UrlDecode(baggagestring.Substring(valueStart, currentIndex - valueStart)).Trim(s_trimmingSpaceCharacters))); + + if (baggageList.Count >= MaxBaggageItems) + { + break; + } + } + + // Skip to end of values + while (currentIndex < baggagestring.Length && baggagestring[currentIndex] != Comma) { currentIndex++; } + + currentIndex++; // Move to next key-value entry + } while (currentIndex < baggagestring.Length); + + baggage = baggageList; + return baggageList != null; + } + + internal static bool InjectContext(ActivityContext context, object carrier, Action setter) + { + if (context == default || setter is null || context.TraceId == default || context.SpanId == default) + { + return false; + } + + Span traceParent = stackalloc char[55]; + traceParent[0] = '0'; + traceParent[1] = '0'; + traceParent[2] = '-'; + traceParent[35] = '-'; + traceParent[52] = '-'; + CopyStringToSpan(context.TraceId.ToHexString(), traceParent.Slice(3, 32)); + CopyStringToSpan(context.SpanId.ToHexString(), traceParent.Slice(36, 16)); + HexConverter.ToCharsBuffer((byte)(context.TraceFlags & ActivityTraceFlags.Recorded), traceParent.Slice(53, 2), 0, HexConverter.Casing.Lower); + + setter(carrier, TraceParent, traceParent.ToString()); + + string? tracestateStr = context.TraceState; + if (tracestateStr?.Length > 0) + { + setter(carrier, TraceState, tracestateStr); + } + + return true; + } + + internal static bool LegacyExtract(object carrier, PropagatorGetterCallback getter, out string? id, out string? state) + { + if (getter is null) + { + id = null; + state = null; + return false; + } + + getter(carrier, TraceParent, out id, out _); + if (id is null) + { + getter(carrier, RequestId, out id, out _); + } + + getter(carrier, TraceState, out state, out _); + return true; + } + + internal static bool LegacyExtract(object carrier, PropagatorGetterCallback getter, out ActivityContext context) + { + context = default; + + if (getter is null) + { + return false; + } + + getter(carrier, TraceParent, out string? traceParent, out _); + getter(carrier, TraceState, out string? traceState, out _); + + return ActivityContext.TryParse(traceParent, traceState, out context); + } + + internal static bool LegacyExtract(object carrier, PropagatorGetterCallback getter, out IEnumerable>? baggage) + { + baggage = null; + if (getter is null) + { + return false; + } + + getter(carrier, Baggage, out string? theBaggage, out _); + if (theBaggage is null || !TryExtractBaggage(theBaggage, out baggage)) + { + getter(carrier, CorrelationContext, out theBaggage, out _); + if (theBaggage is not null) + { + TryExtractBaggage(theBaggage, out baggage); + } + } + + return true; + } + + internal static void CopyStringToSpan(string s, Span span) + { + Debug.Assert(s is not null); + Debug.Assert(s.Length == span.Length); + + for (int i = 0; i < s.Length; i++) + { + span[i] = s[i]; + } + } + } + + internal class LegacyTextMapPropagator : TextMapPropagator + { + // + // Fields + // + + public override IReadOnlyCollection Fields { get; } = new HashSet() { TraceParent, RequestId, TraceState, Baggage, CorrelationContext }; + + // + // Inject + // + + public override bool Inject(Activity activity, object carrier, Action setter) + { + if (activity is null || setter == default) + { + return false; + } + + string? id = activity.Id; + if (id is null) + { + return false; + } + + if (activity.IdFormat == ActivityIdFormat.W3C) + { + setter(carrier, TraceParent, id); + if (activity.TraceStateString is not null) + { + setter(carrier, TraceState, activity.TraceStateString); + } + } + else + { + setter(carrier, RequestId, id); + } + + InjectBaggage(carrier, activity.Baggage, setter); + + return true; + } + + public override bool Inject(ActivityContext context, object carrier, Action setter) => + InjectContext(context, carrier, setter); + + public override bool Inject(IEnumerable> baggage, object carrier, Action setter) + { + if (setter is null) + { + return false; + } + + if (baggage is null) + { + return true; // nothing need to be done + } + + InjectBaggage(carrier, baggage, setter); + return true; + } + + // + // Extract + // + + public override bool Extract(object carrier, PropagatorGetterCallback getter, out string? id, out string? state) => + LegacyExtract(carrier, getter, out id, out state); + + public override bool Extract(object carrier, PropagatorGetterCallback getter, out ActivityContext context) => + LegacyExtract(carrier, getter, out context); + + public override bool Extract(object carrier, PropagatorGetterCallback getter, out IEnumerable>? baggage) => + LegacyExtract(carrier, getter, out baggage); + } + + internal class PassThroughPropagator : TextMapPropagator + { + // + // Fields + // + public override IReadOnlyCollection Fields { get; } = new HashSet() { TraceParent, RequestId, TraceState, Baggage, CorrelationContext }; + + // Inject + + public override bool Inject(Activity activity, object carrier, Action setter) + { + GetRootId(out string? parentId, out string? traceState, out bool isW3c); + if (parentId is null) + { + return true; + } + + setter(carrier, isW3c ? TraceParent : RequestId, parentId); + + if (traceState is not null) + { + setter(carrier, TraceState, traceState); + } + + return true; + } + + public override bool Inject(ActivityContext context, object carrier, Action setter) => + Inject((Activity) null!, carrier, setter); + + public override bool Inject(IEnumerable> baggage, object carrier, Action setter) + { + IEnumerable>? parentBaggage = GetRootBaggage(); + + if (parentBaggage is not null) + { + InjectBaggage(carrier, parentBaggage, setter); + } + + return true; + } + + // + // Extract + // + + public override bool Extract(object carrier, PropagatorGetterCallback getter, out string? id, out string? state) => + LegacyExtract(carrier, getter, out id, out state); + + public override bool Extract(object carrier, PropagatorGetterCallback getter, out ActivityContext context) => + LegacyExtract(carrier, getter, out context); + + public override bool Extract(object carrier, PropagatorGetterCallback getter, out IEnumerable>? baggage) => + LegacyExtract(carrier, getter, out baggage); + + private static void GetRootId(out string? parentId, out string? traceState, out bool isW3c) + { + Activity? activity = Activity.Current; + if (activity is null) + { + parentId = null; + traceState = null; + isW3c = false; + return; + } + + while (activity is not null && activity.Parent is not null) + { + activity = activity.Parent; + } + + traceState = activity?.TraceStateString; + parentId = activity?.ParentId ?? activity?.Id; + isW3c = activity?.IdFormat == ActivityIdFormat.W3C; + } + + private static IEnumerable>? GetRootBaggage() + { + Activity? activity = Activity.Current; + if (activity is null) + { + return null; + } + + while (activity is not null && activity.Parent is not null) + { + activity = activity.Parent; + } + + return activity?.Baggage; + } + } + + internal class OutputSuppressionPropagator : TextMapPropagator + { + // + // Fields + // + public override IReadOnlyCollection Fields { get; } = new HashSet() { TraceParent, RequestId, TraceState, Baggage, CorrelationContext }; + + // Inject + + public override bool Inject(Activity activity, object carrier, Action setter) => true; + public override bool Inject(ActivityContext context, object carrier, Action setter) => true; + public override bool Inject(IEnumerable> baggage, object carrier, Action setter) => true; + + // Extract + + public override bool Extract(object carrier, PropagatorGetterCallback getter, out string? id, out string? state) => + LegacyExtract(carrier, getter, out id, out state); + + public override bool Extract(object carrier, PropagatorGetterCallback getter, out ActivityContext context) => + LegacyExtract(carrier, getter, out context); + + public override bool Extract(object carrier, PropagatorGetterCallback getter, out IEnumerable>? baggage) => + LegacyExtract(carrier, getter, out baggage); + } + + internal class W3CPropagator : TextMapPropagator + { + // + // Fields + // + + public override IReadOnlyCollection Fields { get; } = new HashSet() { TraceParent, TraceState, Baggage }; + + // + // Inject + // + + public override bool Inject(Activity activity, object carrier, Action setter) + { + if (activity is null || setter == default || activity.IdFormat != ActivityIdFormat.W3C) + { + return false; + } + + string? id = activity.Id; + if (id is null) + { + return false; + } + + setter(carrier, TraceParent, id); + if (activity.TraceStateString is not null) + { + setter(carrier, TraceState, activity.TraceStateString); + } + + InjectBaggage(carrier, activity.Baggage, setter, injectAsW3C: true); + + return true; + } + + public override bool Inject(ActivityContext context, object carrier, Action setter) => + InjectContext(context, carrier, setter); + + public override bool Inject(IEnumerable> baggage, object carrier, Action setter) + { + if (setter is null) + { + return false; + } + + if (baggage is null) + { + return true; // nothing need to be done + } + + InjectBaggage(carrier, baggage, setter, true); + return true; + } + + // + // Extract + // + + public override bool Extract(object carrier, PropagatorGetterCallback getter, out string? id, out string? state) + { + if (getter is null) + { + id = null; + state = null; + return false; + } + + getter(carrier, TraceParent, out id, out _); + getter(carrier, TraceState, out state, out _); + + if (id is not null && !ActivityContext.TryParse(id, state, out ActivityContext context)) + { + return false; + } + + return true; + } + + public override bool Extract(object carrier, PropagatorGetterCallback getter, out ActivityContext context) => + LegacyExtract(carrier, getter, out context); + + public override bool Extract(object carrier, PropagatorGetterCallback getter, out IEnumerable>? baggage) + { + baggage = null; + if (getter is null) + { + return false; + } + + getter(carrier, Baggage, out string? theBaggage, out _); + + if (theBaggage is not null) + { + return TryExtractBaggage(theBaggage, out baggage); + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/PropagatorTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/PropagatorTests.cs new file mode 100644 index 0000000000000..82d18346e979f --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/PropagatorTests.cs @@ -0,0 +1,433 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Collections.Generic; +using Xunit; + +namespace System.Diagnostics.Tests +{ + public class PropagatorTests + { + private string ToString(ActivityContext context) + { + Span traceParent = stackalloc char[55]; + traceParent[0] = '0'; + traceParent[1] = '0'; + traceParent[2] = '-'; + traceParent[35] = '-'; + traceParent[52] = '-'; + CopyStringToSpan(context.TraceId.ToHexString(), traceParent.Slice(3, 32)); + CopyStringToSpan(context.SpanId.ToHexString(), traceParent.Slice(36, 16)); + traceParent[53] = '0'; + traceParent[54] = (context.TraceFlags & ActivityTraceFlags.Recorded) != 0 ? '1' : '0'; + return traceParent.ToString(); + } + + private string GetFormattedBaggage(Activity a) + { + string formattedBaggage = ""; + IEnumerator> enumerator = a.Baggage.GetEnumerator(); + while (enumerator.MoveNext()) + { + formattedBaggage += (formattedBaggage.Length > 0 ? "," : "") + enumerator.Current.Key + "=" + enumerator.Current.Value; + } + + return formattedBaggage; + } + + private static void CopyStringToSpan(string s, Span span) + { + Debug.Assert(s is not null); + Debug.Assert(s.Length == span.Length); + + for (int i = 0; i < s.Length; i++) + { + span[i] = s[i]; + } + } + + [Fact] + public void DifferentTests() + { + TextMapPropagator propagator = TextMapPropagator.Default; + + Assert.NotNull(propagator); + + Activity a = new Activity("SomeActivity"); + a.SetIdFormat(ActivityIdFormat.Hierarchical); + a.SetBaggage("B1", "v1"); + a.TraceStateString = "traceState"; + a.SetParentId("Hierarchical"); + a.Start(); + + propagator.Inject(a, null, (object carrier, string fieldName, string value) => + { + Assert.Null(carrier); + if (fieldName == "traceparent" || fieldName == "Request-Id") + { + Assert.Equal(a.Id, value); + } + else if (fieldName == "tracestate") + { + Assert.Equal(a.TraceStateString, value); + } + else if (fieldName == "Correlation-Context") + { + + string formattedBaggage = GetFormattedBaggage(a); + Assert.Equal(formattedBaggage, value); + } + else + { + Assert.False(true, $"{fieldName} Unexpected Field Name"); + } + }); + + propagator.Extract(null, (object carrier, string fieldName, out string? value) => + { + Assert.Null(carrier); + if (fieldName == "traceparent") + { + value = null; + } + else if (fieldName == "Request-Id") + { + value = a.Id; + } + else if (fieldName == "tracestate") + { + value = a.TraceStateString; + } + else + { + value = null; + Assert.False(true, $"{fieldName} Unexpected Field Name"); + } + + return true; + }, + out string? id, out string? state); + + Assert.Equal(a.Id, id); + Assert.Equal(a.TraceStateString, state); + + propagator.Extract(null, (object carrier, string fieldName, out string? value) => + { + Assert.Null(carrier); + if (fieldName == "baggage" || fieldName == "Correlation-Context") + { + value = GetFormattedBaggage(a); + } + else + { + value = null; + Assert.False(true, $"{fieldName} Unexpected Field Name"); + } + return true; + } , out IEnumerable>? baggage); + + List> list = new List>(baggage); + Assert.Equal(1, list.Count); + Assert.Equal(new KeyValuePair("B1", "v1"), list[0]); + + propagator.Extract(null, (object carrier, string fieldName, out string? value) => + { + Assert.Null(carrier); + if (fieldName == "baggage" || fieldName == "Correlation-Context") + { + value = $"{WebUtility.UrlEncode(" k1 ")} = {WebUtility.UrlEncode(" v1 ")}, {WebUtility.UrlEncode(" k2 ")} = {WebUtility.UrlEncode(" v2 ")}"; + } + else + { + value = null; + Assert.False(true, $"{fieldName} Unexpected Field Name"); + } + return true; + } , out baggage); + + list = new List>(baggage); + Assert.Equal(2, list.Count); + Assert.Equal(new KeyValuePair("k2", "v2"), list[0]); + Assert.Equal(new KeyValuePair("k1", "v1"), list[1]); + + propagator = TextMapPropagator.CreateW3CPropagator(); + propagator.Inject(a, null, (object carrier, string fieldName, string value) => + { + Assert.Null(carrier); + if (fieldName == "traceparent") + { + Assert.False(true, $"{fieldName} cannot be used with activity "); + } + else if (fieldName == "tracestate") + { + Assert.Equal(a.TraceStateString, value); + } + else if (fieldName == "baggage") + { + string formattedBaggage = GetFormattedBaggage(a); + Assert.Equal(formattedBaggage, value); + } + else + { + Assert.False(true, $"{fieldName} Unexpected Field Name Hierarchical activity Id"); + } + }); + + a = new Activity("W3CActivity"); + a.SetIdFormat(ActivityIdFormat.W3C); + a.SetBaggage("B2", "v2"); + a.TraceStateString = "W3CtraceState"; + a.Start(); + + propagator.Inject(a, null, (object carrier, string fieldName, string value) => + { + Assert.Null(carrier); + if (fieldName == "traceparent") + { + Assert.Equal(a.Id, value); + } + else if (fieldName == "tracestate") + { + Assert.Equal(a.TraceStateString, value); + } + else if (fieldName == "baggage") + { + string formattedBaggage = GetFormattedBaggage(a); + Assert.Equal(formattedBaggage, value); + } + else + { + Assert.False(true, $"{fieldName} Unexpected Field Name Hierarchical activity Id"); + } + }); + + propagator.Extract(null, (object carrier, string fieldName, out string? value) => + { + Assert.Null(carrier); + if (fieldName == "traceparent") + { + value = a.Id; + } + else if (fieldName == "tracestate") + { + value = a.TraceStateString; + } + else + { + value = null; + Assert.False(true, $"{fieldName} Unexpected Field Name"); + } + return true; + }, out id, out state); + + Assert.Equal(a.Id, id); + Assert.Equal(a.TraceStateString, state); + + propagator.Extract(null, (object carrier, string fieldName, out string? value) => + { + Assert.Null(carrier); + if (fieldName == "traceparent") + { + value = a.Id; + } + else if (fieldName == "tracestate") + { + value = a.TraceStateString; + } + else + { + value = null; + Assert.False(true, $"{fieldName} Unexpected Field Name"); + } + return true; + }, out ActivityContext context); + + Span traceParent = stackalloc char[55]; + traceParent[0] = '0'; + traceParent[1] = '0'; + traceParent[2] = '-'; + traceParent[35] = '-'; + traceParent[52] = '-'; + CopyStringToSpan(context.TraceId.ToHexString(), traceParent.Slice(3, 32)); + CopyStringToSpan(context.SpanId.ToHexString(), traceParent.Slice(36, 16)); + traceParent[53] = '0'; + traceParent[54] = a.Recorded ? '1' : '0'; + + Assert.Equal(a.Id, traceParent.ToString()); + Assert.Equal(a.TraceStateString, context.TraceState); + + propagator.Extract(null, (object carrier, string fieldName, out string? value) => + { + Assert.Null(carrier); + if (fieldName == "baggage") + { + value = GetFormattedBaggage(a); + } + else + { + value = null; + Assert.False(true, $"{fieldName} Unexpected Field Name"); + } + return true; + }, out baggage); + + list = new List>(baggage); + Assert.Equal(2, list.Count); + Assert.Equal(new KeyValuePair("B1", "v1"), list[0]); + Assert.Equal(new KeyValuePair("B2", "v2"), list[1]); + + // + // Test Inject Suppresssion propagator + // + propagator = TextMapPropagator.CreateOutputSuppressionPropagator(); + propagator.Inject(a, null, (object carrier, string fieldName, string value) => + { + Assert.False(true, $"Not expected to have the seeter callback be called."); + }); + + // Extract should continue work + propagator.Extract(null, (object carrier, string fieldName, out string? value) => + { + Assert.Null(carrier); + if (fieldName == "baggage") + { + value = GetFormattedBaggage(a); + } + else + { + value = null; + Assert.False(true, $"{fieldName} Unexpected Field Name"); + } + return true; + }, out baggage); + + list = new List>(baggage); + Assert.Equal(2, list.Count); + Assert.Equal(new KeyValuePair("B1", "v1"), list[0]); + Assert.Equal(new KeyValuePair("B2", "v2"), list[1]); + + // + // Test PassThroughPropagator + // + propagator = TextMapPropagator.CreatePassThroughPropagator(); + + Activity.Current = null; + + propagator.Inject(a, null, (object carrier, string fieldName, string value) => + { + Assert.False(true, $"Activity.Current is null. Extract shouldn't be called"); + }); + + using ActivitySource source = new ActivitySource("PropagatorTests"); + using ActivityListener listener = new ActivityListener(); + listener.ShouldListenTo = (activitySource) => object.ReferenceEquals(source, activitySource); + listener.SampleUsingParentId = (ref ActivityCreationOptions activityOptions) => ActivitySamplingResult.AllData; + listener.Sample = (ref ActivityCreationOptions activityOptions) => ActivitySamplingResult.AllData; + ActivitySource.AddActivityListener(listener); + + ActivityContext parentContext = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, "states"); + a = source.StartActivity("a", ActivityKind.Client, parentContext); + a.AddBaggage("B1", "v1"); + + propagator.Inject(a, null, (object carrier, string fieldName, string value) => + { + if (fieldName == "traceparent") + { + Assert.Equal(ToString(parentContext), value); + } + else if (fieldName == "tracestate") + { + Assert.Equal(parentContext.TraceState, value); + } + else if (fieldName == "Correlation-Context") + { + Assert.Equal(GetFormattedBaggage(a), value); + } + else + { + Assert.False(true, $"{fieldName} Unexpected Field Name"); + } + }); + + Activity b = source.StartActivity("b"); + b.AddBaggage("B2", "v2"); + + propagator.Inject(b, null, (object carrier, string fieldName, string value) => + { + if (fieldName == "traceparent") + { + Assert.Equal(ToString(parentContext), value); + } + else if (fieldName == "tracestate") + { + Assert.Equal(parentContext.TraceState, value); + } + else if (fieldName == "Correlation-Context") + { + Assert.Equal(GetFormattedBaggage(a), value); + } + else + { + Assert.False(true, $"{fieldName} Unexpected Field Name"); + } + }); + + Activity.Current = null; + a = new Activity("SomeActivity"); + a.SetIdFormat(ActivityIdFormat.Hierarchical); + a.SetBaggage("B1H", "v1H"); + a.TraceStateString = "traceStateH"; + a.SetParentId("HierarchicalH"); + a.Start(); + + propagator.Inject(b, null, (object carrier, string fieldName, string value) => + { + if (fieldName == "Request-Id") + { + Assert.Equal(a.ParentId, value); + } + else if (fieldName == "tracestate") + { + Assert.Equal(a.TraceStateString, value); + } + else if (fieldName == "Correlation-Context") + { + Assert.Equal(GetFormattedBaggage(a), value); + } + else + { + Assert.False(true, $"{fieldName} Unexpected Field Name"); + } + }); + + b = new Activity("SomeActivityb"); + b.SetIdFormat(ActivityIdFormat.Hierarchical); + b.SetBaggage("B2H", "v2H"); + b.TraceStateString = "traceStateW"; + b.Start(); + + string s1 = Activity.Current.Parent?.OperationName ?? "null"; + + propagator.Inject(b, null, (object carrier, string fieldName, string value) => + { + if (fieldName == "Request-Id") + { + Assert.Equal(a.ParentId, value); + } + else if (fieldName == "tracestate") + { + Assert.Equal(a.TraceStateString, value); + } + else if (fieldName == "Correlation-Context") + { + Assert.Equal(GetFormattedBaggage(a), value); + } + else + { + Assert.False(true, $"{fieldName} Unexpected Field Name with value {value}"); + } + }); + } + } +} diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj b/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj index a2f9daa3feec4..172ae4e8ac6ea 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/System.Diagnostics.DiagnosticSource.Tests.csproj @@ -12,6 +12,7 @@ +