diff --git a/src/libraries/Microsoft.Extensions.Logging.EventSource/src/EventSourceLogger.cs b/src/libraries/Microsoft.Extensions.Logging.EventSource/src/EventSourceLogger.cs index 8b002fbb966ba0..8a17156396df5f 100644 --- a/src/libraries/Microsoft.Extensions.Logging.EventSource/src/EventSourceLogger.cs +++ b/src/libraries/Microsoft.Extensions.Logging.EventSource/src/EventSourceLogger.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.Diagnostics.Tracing; using System.IO; using System.Text; @@ -87,9 +88,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except { activityTraceId = activity.TraceId.ToHexString(); activitySpanId = activity.SpanId.ToHexString(); - activityTraceFlags = activity.ActivityTraceFlags == ActivityTraceFlags.None - ? "0" - : "1"; + activityTraceFlags = ((int)activity.ActivityTraceFlags).ToString(CultureInfo.InvariantCulture); } else { diff --git a/src/libraries/Microsoft.Extensions.Logging.EventSource/tests/EventSourceLoggerTest.cs b/src/libraries/Microsoft.Extensions.Logging.EventSource/tests/EventSourceLoggerTest.cs index d7cdc0c52e2def..d7fd7ac5e3c552 100644 --- a/src/libraries/Microsoft.Extensions.Logging.EventSource/tests/EventSourceLoggerTest.cs +++ b/src/libraries/Microsoft.Extensions.Logging.EventSource/tests/EventSourceLoggerTest.cs @@ -244,7 +244,7 @@ public void Logs_TracingDetailsAsExpected_WithDefaults(bool hasTrace, bool useW3 { Assert.Contains($@"""ActivityTraceId"":""{activity.TraceId.ToHexString()}""", eventJson); Assert.Contains($@"""ActivitySpanId"":""{activity.SpanId.ToHexString()}""", eventJson); - Assert.Contains($@"""ActivityTraceFlags"":""{(activity.Recorded ? "1" : "0")}""", eventJson); + Assert.Contains($@"""ActivityTraceFlags"":""{((int)activity.ActivityTraceFlags).ToString(CultureInfo.InvariantCulture)}""", eventJson); } else { 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 991f4048a9da81..10a4af3bda126e 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs @@ -36,6 +36,7 @@ public string? Id public System.Diagnostics.Activity? Parent { get { throw null; } } public string? ParentId { get { throw null; } } public System.Diagnostics.ActivitySpanId ParentSpanId { get { throw null; } } + public bool HasRandomizedTraceId { get { throw null; } } public bool Recorded { get { throw null; } } public string? RootId { get { throw null; } } public System.Diagnostics.ActivitySpanId SpanId { get { throw null; } } @@ -184,6 +185,7 @@ public enum ActivityTraceFlags { None = 0, Recorded = 1, + RandomTraceId = 2, } public readonly partial struct ActivityTraceId : System.IEquatable { diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs index cd6da7a53f818d..850670ce83bb05 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs @@ -940,7 +940,12 @@ public ActivityTraceId TraceId } /// - /// True if the W3CIdFlags.Recorded flag is set. + /// True if the flag is set. + /// + public bool HasRandomizedTraceId { get => (ActivityTraceFlags & ActivityTraceFlags.RandomTraceId) != 0; } + + /// + /// True if the flag is set. /// public bool Recorded { get => (ActivityTraceFlags & ActivityTraceFlags.Recorded) != 0; } @@ -1289,9 +1294,31 @@ private void GenerateW3CId() if (!TrySetTraceIdFromParent()) { Func? traceIdGenerator = TraceIdGenerator; - ActivityTraceId id = traceIdGenerator == null ? ActivityTraceId.CreateRandom() : traceIdGenerator(); + ActivityTraceId id; + + if (traceIdGenerator == null) + { + id = ActivityTraceId.CreateRandom(); + // Set RandomTraceId flag when using the default random generator + ActivityTraceFlags |= ActivityTraceFlags.RandomTraceId; + } + else + { + // Using custom generator + id = traceIdGenerator(); + } + _traceId = id.ToHexString(); } + else + { + // When inheriting trace ID from parent, propagate the RandomTraceId flag + // so downstream participants know the trace ID has sufficient randomness + // for probabilistic sampling (W3C Trace Context Level 2). + // This is needed here because TrySetTraceFlagsFromParent() below may be + // skipped when W3CIdFlagsSet is already true (e.g., Recorded set by sampling). + TryPropagateRandomTraceIdFromParent(); + } } if (!W3CIdFlagsSet) @@ -1466,6 +1493,28 @@ private void TrySetTraceFlagsFromParent() } } + /// + /// Propagates the RandomTraceId flag from the parent when a child inherits its trace ID. + /// Unlike Recorded (which is set independently per-activity by sampling), RandomTraceId + /// reflects a property of the trace ID itself and should be inherited along with it. + /// + private void TryPropagateRandomTraceIdFromParent() + { + if (Parent is not null) + { + if ((Parent.ActivityTraceFlags & ActivityTraceFlags.RandomTraceId) != 0) + { + ActivityTraceFlags |= ActivityTraceFlags.RandomTraceId; + } + } + else if (_parentId is not null && IsW3CId(_parentId) + && HexConverter.IsHexLowerChar(_parentId[53]) && HexConverter.IsHexLowerChar(_parentId[54]) + && (ActivityTraceId.HexByteFromChars(_parentId[53], _parentId[54]) & (byte)ActivityTraceFlags.RandomTraceId) != 0) + { + ActivityTraceFlags |= ActivityTraceFlags.RandomTraceId; + } + } + private bool W3CIdFlagsSet { get => (_w3CIdFlags & ActivityTraceFlagsIsSet) != 0; @@ -1872,6 +1921,7 @@ public enum ActivityTraceFlags { None = 0b_0_0000000, Recorded = 0b_0_0000001, // The Activity (or more likely its parents) has been marked as useful to record + RandomTraceId = 0b_0_0000010, // The Activity has a randomized TraceId } /// diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityCreationOptions.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityCreationOptions.cs index 133f0ff69eb4fe..bb0ad3808688d9 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityCreationOptions.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityCreationOptions.cs @@ -133,10 +133,21 @@ public ActivityTraceId TraceId if (Parent is ActivityContext && IdFormat == ActivityIdFormat.W3C && _context == default) { Func? traceIdGenerator = Activity.TraceIdGenerator; - ActivityTraceId id = traceIdGenerator == null ? ActivityTraceId.CreateRandom() : traceIdGenerator(); + ActivityTraceId id; + ActivityTraceFlags activityTraceFlags = ActivityTraceFlags.None; + + if (traceIdGenerator is null) + { + id = ActivityTraceId.CreateRandom(); + activityTraceFlags = ActivityTraceFlags.RandomTraceId; + } + else + { + id = traceIdGenerator(); + } // Because the struct is readonly, we cannot directly assign _context. We have to workaround it by calling Unsafe.AsRef - Unsafe.AsRef(in _context) = new ActivityContext(id, default, ActivityTraceFlags.None); + Unsafe.AsRef(in _context) = new ActivityContext(id, default, activityTraceFlags); } return _context.TraceId; diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs index af9540f0203e18..0e86bc5f71ba18 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs @@ -1151,10 +1151,39 @@ public void ActivityTraceFlagsTests() Assert.Equal($"00-0123456789abcdef0123456789abcdef-{activity.SpanId.ToHexString()}-01", activity.Id); Assert.Equal(ActivityTraceFlags.Recorded, activity.ActivityTraceFlags); Assert.True(activity.Recorded); + Assert.False(activity.HasRandomizedTraceId); activity.Stop(); - // Set the 'Recorded' bit by using SetParentId by using the TraceId, SpanId, ActivityTraceFlags overload + // Set the 'RandomTraceId' bit by using SetParentId with the -02 flag. activity = new Activity("activity2"); + activity.SetParentId("00-0123456789abcdef0123456789abcdef-0123456789abcdef-02"); + activity.Start(); + Assert.Equal(ActivityIdFormat.W3C, activity.IdFormat); + Assert.Equal("0123456789abcdef0123456789abcdef", activity.TraceId.ToHexString()); + Assert.Equal("0123456789abcdef", activity.ParentSpanId.ToHexString()); + Assert.True(IdIsW3CFormat(activity.Id)); + Assert.Equal($"00-0123456789abcdef0123456789abcdef-{activity.SpanId.ToHexString()}-02", activity.Id); + Assert.Equal(ActivityTraceFlags.RandomTraceId, activity.ActivityTraceFlags); + Assert.False(activity.Recorded); + Assert.True(activity.HasRandomizedTraceId); + activity.Stop(); + + // Set the 'Recorded' and 'RandomTraceId' bits by using SetParentId with the -03 flag. + activity = new Activity("activity3"); + activity.SetParentId("00-0123456789abcdef0123456789abcdef-0123456789abcdef-03"); + activity.Start(); + Assert.Equal(ActivityIdFormat.W3C, activity.IdFormat); + Assert.Equal("0123456789abcdef0123456789abcdef", activity.TraceId.ToHexString()); + Assert.Equal("0123456789abcdef", activity.ParentSpanId.ToHexString()); + Assert.True(IdIsW3CFormat(activity.Id)); + Assert.Equal($"00-0123456789abcdef0123456789abcdef-{activity.SpanId.ToHexString()}-03", activity.Id); + Assert.Equal(ActivityTraceFlags.Recorded | ActivityTraceFlags.RandomTraceId, activity.ActivityTraceFlags); + Assert.True(activity.Recorded); + Assert.True(activity.HasRandomizedTraceId); + activity.Stop(); + + // Set the 'Recorded' bit by using SetParentId by using the TraceId, SpanId, ActivityTraceFlags overload + activity = new Activity("activity4"); ActivityTraceId activityTraceId = ActivityTraceId.CreateRandom(); activity.SetParentId(activityTraceId, ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded); activity.Start(); @@ -1164,11 +1193,41 @@ public void ActivityTraceFlagsTests() Assert.Equal($"00-{activity.TraceId.ToHexString()}-{activity.SpanId.ToHexString()}-01", activity.Id); Assert.Equal(ActivityTraceFlags.Recorded, activity.ActivityTraceFlags); Assert.True(activity.Recorded); + Assert.False(activity.HasRandomizedTraceId); + activity.Stop(); + + // Set the 'RandomTraceId' bit by using SetParentId by using the TraceId, SpanId, ActivityTraceFlags overload + activity = new Activity("activity5"); + activityTraceId = ActivityTraceId.CreateRandom(); + activity.SetParentId(activityTraceId, ActivitySpanId.CreateRandom(), ActivityTraceFlags.RandomTraceId); + activity.Start(); + Assert.Equal(ActivityIdFormat.W3C, activity.IdFormat); + Assert.Equal(activityTraceId.ToHexString(), activity.TraceId.ToHexString()); + Assert.True(IdIsW3CFormat(activity.Id)); + Assert.Equal($"00-{activity.TraceId.ToHexString()}-{activity.SpanId.ToHexString()}-02", activity.Id); + Assert.Equal(ActivityTraceFlags.RandomTraceId, activity.ActivityTraceFlags); + Assert.False(activity.Recorded); + Assert.True(activity.HasRandomizedTraceId); + activity.Stop(); + + + // Set the 'Recorded' and 'RandomTraceId' bits by using SetParentId by using the TraceId, SpanId, ActivityTraceFlags overload + activity = new Activity("activity6"); + activityTraceId = ActivityTraceId.CreateRandom(); + activity.SetParentId(activityTraceId, ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded | ActivityTraceFlags.RandomTraceId); + activity.Start(); + Assert.Equal(ActivityIdFormat.W3C, activity.IdFormat); + Assert.Equal(activityTraceId.ToHexString(), activity.TraceId.ToHexString()); + Assert.True(IdIsW3CFormat(activity.Id)); + Assert.Equal($"00-{activity.TraceId.ToHexString()}-{activity.SpanId.ToHexString()}-03", activity.Id); + Assert.Equal(ActivityTraceFlags.Recorded | ActivityTraceFlags.RandomTraceId, activity.ActivityTraceFlags); + Assert.True(activity.Recorded); + Assert.True(activity.HasRandomizedTraceId); activity.Stop(); /****************************************************/ - // Set the 'Recorded' bit explicitly after the fact. - activity = new Activity("activity3"); + // Set the 'Recorded' and 'RandomTraceId' bits explicitly after the fact. + activity = new Activity("activity7"); activity.SetParentId("00-0123456789abcdef0123456789abcdef-0123456789abcdef-00"); activity.Start(); Assert.Equal(ActivityIdFormat.W3C, activity.IdFormat); @@ -1178,37 +1237,52 @@ public void ActivityTraceFlagsTests() Assert.Equal($"00-{activity.TraceId.ToHexString()}-{activity.SpanId.ToHexString()}-00", activity.Id); Assert.Equal(ActivityTraceFlags.None, activity.ActivityTraceFlags); Assert.False(activity.Recorded); + Assert.False(activity.HasRandomizedTraceId); activity.ActivityTraceFlags = ActivityTraceFlags.Recorded; Assert.Equal(ActivityTraceFlags.Recorded, activity.ActivityTraceFlags); Assert.True(activity.Recorded); + Assert.False(activity.HasRandomizedTraceId); + + activity.ActivityTraceFlags = ActivityTraceFlags.RandomTraceId; + Assert.Equal(ActivityTraceFlags.RandomTraceId, activity.ActivityTraceFlags); + Assert.False(activity.Recorded); + Assert.True(activity.HasRandomizedTraceId); + + activity.ActivityTraceFlags = ActivityTraceFlags.Recorded | ActivityTraceFlags.RandomTraceId; + Assert.Equal(ActivityTraceFlags.Recorded | ActivityTraceFlags.RandomTraceId, activity.ActivityTraceFlags); + Assert.True(activity.Recorded); + Assert.True(activity.HasRandomizedTraceId); + activity.Stop(); /****************************************************/ // Confirm that the flags are propagated to children. - activity = new Activity("activity4"); - activity.SetParentId("00-0123456789abcdef0123456789abcdef-0123456789abcdef-01"); + activity = new Activity("activity8"); + activity.SetParentId("00-0123456789abcdef0123456789abcdef-0123456789abcdef-03"); activity.Start(); Assert.Equal(activity, Activity.Current); Assert.Equal(ActivityIdFormat.W3C, activity.IdFormat); Assert.Equal("0123456789abcdef0123456789abcdef", activity.TraceId.ToHexString()); Assert.Equal("0123456789abcdef", activity.ParentSpanId.ToHexString()); Assert.True(IdIsW3CFormat(activity.Id)); - Assert.Equal($"00-{activity.TraceId.ToHexString()}-{activity.SpanId.ToHexString()}-01", activity.Id); - Assert.Equal(ActivityTraceFlags.Recorded, activity.ActivityTraceFlags); + Assert.Equal($"00-{activity.TraceId.ToHexString()}-{activity.SpanId.ToHexString()}-03", activity.Id); + Assert.Equal(ActivityTraceFlags.Recorded | ActivityTraceFlags.RandomTraceId, activity.ActivityTraceFlags); Assert.True(activity.Recorded); + Assert.True(activity.HasRandomizedTraceId); // create a child - var childActivity = new Activity("activity4Child"); + var childActivity = new Activity("activity8Child"); childActivity.Start(); Assert.Equal(childActivity, Activity.Current); Assert.Equal("0123456789abcdef0123456789abcdef", childActivity.TraceId.ToHexString()); Assert.NotEqual(activity.SpanId.ToHexString(), childActivity.SpanId.ToHexString()); Assert.True(IdIsW3CFormat(childActivity.Id)); - Assert.Equal($"00-{childActivity.TraceId.ToHexString()}-{childActivity.SpanId.ToHexString()}-01", childActivity.Id); - Assert.Equal(ActivityTraceFlags.Recorded, childActivity.ActivityTraceFlags); + Assert.Equal($"00-{childActivity.TraceId.ToHexString()}-{childActivity.SpanId.ToHexString()}-03", childActivity.Id); + Assert.Equal(ActivityTraceFlags.Recorded | ActivityTraceFlags.RandomTraceId, childActivity.ActivityTraceFlags); Assert.True(childActivity.Recorded); + Assert.True(childActivity.HasRandomizedTraceId); childActivity.Stop(); activity.Stop(); @@ -2190,12 +2264,58 @@ public void TraceIdCustomGenerationTest() a.Start(); Assert.Equal(ActivityTraceId.CreateFromBytes(traceIdBytes), a.TraceId); + Assert.False(a.HasRandomizedTraceId); a.Stop(); } }).Dispose(); } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TraceIdDefaultGenerationSetsRandomFlag() + { + RemoteExecutor.Invoke(() => + { + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.TraceIdGenerator = null; // Ensure we're using the default generator + + Activity a = new Activity("DefaultRandomTraceId"); + a.Start(); + + // Default random generator should set RandomTraceId flag + Assert.True(a.HasRandomizedTraceId); + Assert.Equal(ActivityTraceFlags.RandomTraceId, a.ActivityTraceFlags & ActivityTraceFlags.RandomTraceId); + + // Verify TraceId is not all zeros + Assert.NotEqual("00000000000000000000000000000000", a.TraceId.ToHexString()); + + a.Stop(); + + ActivityTraceId sampledTraceId = default; + + using ActivitySource source = new ActivitySource(nameof(TraceIdDefaultGenerationSetsRandomFlag)); + using ActivityListener listener = new ActivityListener + { + ShouldListenTo = activitySource => activitySource.Name == source.Name, + Sample = (ref ActivityCreationOptions options) => + { + sampledTraceId = options.TraceId; + Assert.NotEqual("00000000000000000000000000000000", sampledTraceId.ToHexString()); + return ActivitySamplingResult.AllData; + } + }; + + ActivitySource.AddActivityListener(listener); + + using Activity? sourceActivity = source.StartActivity("DefaultRandomTraceIdFromSource"); + + Assert.NotNull(sourceActivity); + Assert.Equal(sampledTraceId, sourceActivity.TraceId); + Assert.True(sourceActivity.HasRandomizedTraceId); + Assert.Equal(ActivityTraceFlags.RandomTraceId, sourceActivity.ActivityTraceFlags); + }).Dispose(); + } + [Fact] public void EnumerateTagObjectsTest() { diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/DiagnosticSourceEventSourceBridgeTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/DiagnosticSourceEventSourceBridgeTests.cs index e07a6748e0430c..d1b4cb4f0a2832 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/DiagnosticSourceEventSourceBridgeTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/DiagnosticSourceEventSourceBridgeTests.cs @@ -58,7 +58,7 @@ public void TestEnableAllActivitySourcesAllEvents() Assert.Equal(++eventsCount, eventSourceListener.EventCount); ValidateActivityEvents(eventSourceListener, "ActivityStart", sources[i].Name, activities[i].OperationName); Assert.True(activities[i].IsAllDataRequested); - Assert.Equal(ActivityTraceFlags.Recorded, activities[i].ActivityTraceFlags); + Assert.Equal(ActivityTraceFlags.Recorded | ActivityTraceFlags.RandomTraceId, activities[i].ActivityTraceFlags); } for (int i = 0; i < 10; i++) @@ -106,7 +106,7 @@ public void TestEnableAllActivitySourcesWithOneEvent(string eventName) } Assert.Equal(eventsCount, eventSourceListener.EventCount); Assert.True(activities[i].IsAllDataRequested); - Assert.Equal(ActivityTraceFlags.Recorded, activities[i].ActivityTraceFlags); + Assert.Equal(ActivityTraceFlags.Recorded | ActivityTraceFlags.RandomTraceId, activities[i].ActivityTraceFlags); } for (int i = 0; i < 10; i++) @@ -126,12 +126,12 @@ public void TestEnableAllActivitySourcesWithOneEvent(string eventName) } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [InlineData("Propagate", false, ActivityTraceFlags.None)] - [InlineData("PROPAGATE", false, ActivityTraceFlags.None)] - [InlineData("Record", true, ActivityTraceFlags.None)] - [InlineData("recorD", true, ActivityTraceFlags.None)] - [InlineData("", true, ActivityTraceFlags.Recorded)] - public void TestEnableAllActivitySourcesWithSpeciifcSamplingResult(string samplingResult, bool alldataRequested, ActivityTraceFlags activityTraceFlags) + [InlineData("Propagate", false, ActivityTraceFlags.RandomTraceId)] + [InlineData("PROPAGATE", false, ActivityTraceFlags.RandomTraceId)] + [InlineData("Record", true, ActivityTraceFlags.RandomTraceId)] + [InlineData("recorD", true, ActivityTraceFlags.RandomTraceId)] + [InlineData("", true, ActivityTraceFlags.Recorded | ActivityTraceFlags.RandomTraceId)] + public void TestEnableAllActivitySourcesWithSpecificSamplingResult(string samplingResult, bool alldataRequested, ActivityTraceFlags activityTraceFlags) { RemoteExecutor.Invoke((result, dataRequested, traceFlags) => { @@ -181,8 +181,8 @@ public void TestDefaultActivitySource(string eventName, string samplingResult, b Assert.NotNull(a); Assert.Equal(bool.Parse(allDataRequested), a.IsAllDataRequested); - // All Activities created with "new Activity(...)" will have ActivityTraceFlags is `None`; - Assert.Equal(samplingResult.Length == 0 ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None, a.ActivityTraceFlags); + // Activities created via ActivitySource with the default random trace ID generator will have RandomTraceId set. + Assert.Equal(samplingResult.Length == 0 ? ActivityTraceFlags.Recorded | ActivityTraceFlags.RandomTraceId : ActivityTraceFlags.RandomTraceId, a.ActivityTraceFlags); a.Dispose();