Skip to content

Commit b23dea6

Browse files
authored
Adding Activity.AddException (#102905)
* Adding Activity.AddException * Feedback * Feedback 2 * Remove un-needed text from the comment
1 parent f9bcbcb commit b23dea6

File tree

6 files changed

+227
-2
lines changed

6 files changed

+227
-2
lines changed

src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs

+3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public string? Id
5050
public string? TraceStateString { get { throw null; } set { } }
5151
public System.Diagnostics.Activity AddBaggage(string key, string? value) { throw null; }
5252
public System.Diagnostics.Activity AddEvent(System.Diagnostics.ActivityEvent e) { throw null; }
53+
public System.Diagnostics.Activity AddException(System.Exception exception, System.Diagnostics.TagList tags = default, System.DateTimeOffset timestamp = default) { throw null; }
5354
public System.Diagnostics.Activity AddLink(System.Diagnostics.ActivityLink link) { throw null; }
5455
public System.Diagnostics.Activity AddTag(string key, string? value) { throw null; }
5556
public System.Diagnostics.Activity AddTag(string key, object? value) { throw null; }
@@ -276,11 +277,13 @@ public readonly struct ActivityCreationOptions<T>
276277
public string? TraceState { get { throw null; } init { throw null; } }
277278
}
278279
public delegate System.Diagnostics.ActivitySamplingResult SampleActivity<T>(ref System.Diagnostics.ActivityCreationOptions<T> options);
280+
public delegate void ExceptionRecorder(System.Diagnostics.Activity activity, System.Exception exception, ref System.Diagnostics.TagList tags);
279281
public sealed class ActivityListener : IDisposable
280282
{
281283
public ActivityListener() { throw null; }
282284
public System.Action<System.Diagnostics.Activity>? ActivityStarted { get { throw null; } set { throw null; } }
283285
public System.Action<System.Diagnostics.Activity>? ActivityStopped { get { throw null; } set { throw null; } }
286+
public System.Diagnostics.ExceptionRecorder? ExceptionRecorder { get { throw null; } set { throw null; } }
284287
public System.Func<System.Diagnostics.ActivitySource, bool>? ShouldListenTo { get { throw null; } set { throw null; } }
285288
public System.Diagnostics.SampleActivity<string>? SampleUsingParentId { get { throw null; } set { throw null; } }
286289
public System.Diagnostics.SampleActivity<ActivityContext>? Sample { get { throw null; } set { throw null; } }

src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs

+69
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,75 @@ public Activity AddEvent(ActivityEvent e)
517517
return this;
518518
}
519519

520+
/// <summary>
521+
/// Add an <see cref="ActivityEvent" /> object containing the exception information to the <see cref="Events" /> list.
522+
/// </summary>
523+
/// <param name="exception">The exception to add to the attached events list.</param>
524+
/// <param name="tags">The tags to add to the exception event.</param>
525+
/// <param name="timestamp">The timestamp to add to the exception event.</param>
526+
/// <returns><see langword="this" /> for convenient chaining.</returns>
527+
/// <remarks>
528+
/// <para>- The name of the event will be "exception", and it will include the tags "exception.message", "exception.stacktrace", and "exception.type",
529+
/// in addition to the tags provided in the <paramref name="tags"/> parameter.</para>
530+
/// <para>- Any registered <see cref="ActivityListener"/> with the <see cref="ActivityListener.ExceptionRecorder"/> callback will be notified about this exception addition
531+
/// before the <see cref="ActivityEvent" /> object is added to the <see cref="Events" /> list.</para>
532+
/// <para>- Any registered <see cref="ActivityListener"/> with the <see cref="ActivityListener.ExceptionRecorder"/> callback that adds "exception.message", "exception.stacktrace", or "exception.type" tags
533+
/// will not have these tags overwritten, except by any subsequent <see cref="ActivityListener"/> that explicitly overwrites them.</para>
534+
/// </remarks>
535+
public Activity AddException(Exception exception, TagList tags = default, DateTimeOffset timestamp = default)
536+
{
537+
if (exception == null)
538+
{
539+
throw new ArgumentNullException(nameof(exception));
540+
}
541+
542+
TagList exceptionTags = tags;
543+
544+
Source.NotifyActivityAddException(this, exception, ref exceptionTags);
545+
546+
const string ExceptionEventName = "exception";
547+
const string ExceptionMessageTag = "exception.message";
548+
const string ExceptionStackTraceTag = "exception.stacktrace";
549+
const string ExceptionTypeTag = "exception.type";
550+
551+
bool hasMessage = false;
552+
bool hasStackTrace = false;
553+
bool hasType = false;
554+
555+
for (int i = 0; i < exceptionTags.Count; i++)
556+
{
557+
if (exceptionTags[i].Key == ExceptionMessageTag)
558+
{
559+
hasMessage = true;
560+
}
561+
else if (exceptionTags[i].Key == ExceptionStackTraceTag)
562+
{
563+
hasStackTrace = true;
564+
}
565+
else if (exceptionTags[i].Key == ExceptionTypeTag)
566+
{
567+
hasType = true;
568+
}
569+
}
570+
571+
if (!hasMessage)
572+
{
573+
exceptionTags.Add(new KeyValuePair<string, object?>(ExceptionMessageTag, exception.Message));
574+
}
575+
576+
if (!hasStackTrace)
577+
{
578+
exceptionTags.Add(new KeyValuePair<string, object?>(ExceptionStackTraceTag, exception.ToString()));
579+
}
580+
581+
if (!hasType)
582+
{
583+
exceptionTags.Add(new KeyValuePair<string, object?>(ExceptionTypeTag, exception.GetType().ToString()));
584+
}
585+
586+
return AddEvent(new ActivityEvent(ExceptionEventName, timestamp, ref exceptionTags));
587+
}
588+
520589
/// <summary>
521590
/// Add an <see cref="ActivityLink"/> to the <see cref="Links"/> list.
522591
/// </summary>

src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityEvent.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,16 @@ public ActivityEvent(string name) : this(name, DateTimeOffset.UtcNow, tags: null
2828
/// <param name="name">Event name.</param>
2929
/// <param name="timestamp">Event timestamp. Timestamp MUST only be used for the events that happened in the past, not at the moment of this call.</param>
3030
/// <param name="tags">Event Tags.</param>
31-
public ActivityEvent(string name, DateTimeOffset timestamp = default, ActivityTagsCollection? tags = null)
31+
public ActivityEvent(string name, DateTimeOffset timestamp = default, ActivityTagsCollection? tags = null) : this(name, timestamp, tags, tags is null ? 0 : tags.Count) { }
32+
33+
internal ActivityEvent(string name, DateTimeOffset timestamp, ref TagList tags) : this(name, timestamp, tags, tags.Count) { }
34+
35+
private ActivityEvent(string name, DateTimeOffset timestamp, IEnumerable<KeyValuePair<string, object?>>? tags, int tagsCount)
3236
{
3337
Name = name ?? string.Empty;
3438
Timestamp = timestamp != default ? timestamp : DateTimeOffset.UtcNow;
3539

36-
_tags = tags?.Count > 0 ? new Activity.TagsLinkedList(tags) : null;
40+
_tags = tagsCount > 0 ? new Activity.TagsLinkedList(tags!) : null;
3741
}
3842

3943
/// <summary>

src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityListener.cs

+10
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ namespace System.Diagnostics
1010
/// </summary>
1111
public delegate ActivitySamplingResult SampleActivity<T>(ref ActivityCreationOptions<T> options);
1212

13+
/// <summary>
14+
/// Define the callback to be used in <see cref="ActivityListener"/> to receive notifications when exceptions are added to the <see cref="Activity"/>.
15+
/// </summary>
16+
public delegate void ExceptionRecorder(Activity activity, Exception exception, ref TagList tags);
17+
1318
/// <summary>
1419
/// ActivityListener allows listening to the start and stop Activity events and give the opportunity to decide creating the Activity for sampling scenarios.
1520
/// </summary>
@@ -32,6 +37,11 @@ public ActivityListener()
3237
/// </summary>
3338
public Action<Activity>? ActivityStopped { get; set; }
3439

40+
/// <summary>
41+
/// Set or get the callback used to listen to <see cref="Activity"/> events when exceptions are added.
42+
/// </summary>
43+
public ExceptionRecorder? ExceptionRecorder { get; set; }
44+
3545
/// <summary>
3646
/// Set or get the callback used to decide if want to listen to <see cref="Activity"/> objects events which created using <see cref="ActivitySource"/> object.
3747
/// </summary>

src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivitySource.cs

+43
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,18 @@ internal void NotifyActivityStop(Activity activity)
393393
listeners.EnumWithAction((listener, obj) => listener.ActivityStopped?.Invoke((Activity)obj), activity);
394394
}
395395
}
396+
397+
internal void NotifyActivityAddException(Activity activity, Exception exception, ref TagList tags)
398+
{
399+
Debug.Assert(activity != null);
400+
401+
// _listeners can get assigned to null in Dispose.
402+
SynchronizedList<ActivityListener>? listeners = _listeners;
403+
if (listeners != null && listeners.Count > 0)
404+
{
405+
listeners.EnumWithExceptionNotification(activity, exception, ref tags);
406+
}
407+
}
396408
}
397409

398410
// SynchronizedList<T> is a helper collection which ensure thread safety on the collection
@@ -498,5 +510,36 @@ public void EnumWithAction(Action<T, object> action, object arg)
498510
}
499511
}
500512

513+
public void EnumWithExceptionNotification(Activity activity, Exception exception, ref TagList tags)
514+
{
515+
if (typeof(T) != typeof(ActivityListener))
516+
{
517+
return;
518+
}
519+
520+
uint version = _version;
521+
int index = 0;
522+
523+
while (index < _list.Count)
524+
{
525+
T item;
526+
lock (_list)
527+
{
528+
if (version != _version)
529+
{
530+
version = _version;
531+
index = 0;
532+
continue;
533+
}
534+
535+
item = _list[index];
536+
index++;
537+
}
538+
539+
// Important to notify outside the lock.
540+
// This is the whole point we are having this wrapper class.
541+
(item as ActivityListener)!.ExceptionRecorder?.Invoke(activity, exception, ref tags);
542+
}
543+
}
501544
}
502545
}

src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs

+96
Original file line numberDiff line numberDiff line change
@@ -1626,6 +1626,102 @@ public void AddLinkTest()
16261626
Assert.Equal(99, tag.Value);
16271627
}
16281628

1629+
[Fact]
1630+
public void AddExceptionTest()
1631+
{
1632+
using ActivitySource aSource = new ActivitySource("AddExceptionTest");
1633+
1634+
ActivityListener listener = new ActivityListener();
1635+
listener.ShouldListenTo = (activitySource) => object.ReferenceEquals(activitySource, aSource);
1636+
listener.Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData;
1637+
ActivitySource.AddActivityListener(listener);
1638+
1639+
using Activity? activity = aSource.StartActivity("Activity1");
1640+
Assert.NotNull(activity);
1641+
Assert.Empty(activity.Events);
1642+
1643+
const string ExceptionEventName = "exception";
1644+
const string ExceptionMessageTag = "exception.message";
1645+
const string ExceptionStackTraceTag = "exception.stacktrace";
1646+
const string ExceptionTypeTag = "exception.type";
1647+
1648+
Exception exception = new ArgumentOutOfRangeException("Some message");
1649+
activity.AddException(exception);
1650+
List<ActivityEvent> events = activity.Events.ToList();
1651+
Assert.Equal(1, events.Count);
1652+
Assert.Equal(ExceptionEventName, events[0].Name);
1653+
Assert.Equal(new TagList { { ExceptionMessageTag, exception.Message}, { ExceptionStackTraceTag, exception.ToString()}, { ExceptionTypeTag, exception.GetType().ToString() } }, events[0].Tags);
1654+
1655+
try { throw new InvalidOperationException("Some other message"); } catch (Exception e) { exception = e; }
1656+
activity.AddException(exception);
1657+
events = activity.Events.ToList();
1658+
Assert.Equal(2, events.Count);
1659+
Assert.Equal(ExceptionEventName, events[1].Name);
1660+
Assert.Equal(new TagList { { ExceptionMessageTag, exception.Message}, { ExceptionStackTraceTag, exception.ToString()}, { ExceptionTypeTag, exception.GetType().ToString() } }, events[1].Tags);
1661+
1662+
listener.ExceptionRecorder = (Activity activity, Exception exception, ref TagList theTags) => theTags.Add("foo", "bar");
1663+
activity.AddException(exception, new TagList { { "hello", "world" } });
1664+
events = activity.Events.ToList();
1665+
Assert.Equal(3, events.Count);
1666+
Assert.Equal(ExceptionEventName, events[2].Name);
1667+
Assert.Equal(new TagList
1668+
{
1669+
{ "hello", "world" },
1670+
{ "foo", "bar" },
1671+
{ ExceptionMessageTag, exception.Message },
1672+
{ ExceptionStackTraceTag, exception.ToString() },
1673+
{ ExceptionTypeTag, exception.GetType().ToString() }
1674+
},
1675+
events[2].Tags);
1676+
1677+
listener.ExceptionRecorder = (Activity activity, Exception exception, ref TagList theTags) =>
1678+
{
1679+
theTags.Add("exception.escaped", "true");
1680+
theTags.Add("exception.message", "Overridden message");
1681+
theTags.Add("exception.stacktrace", "Overridden stacktrace");
1682+
theTags.Add("exception.type", "Overridden type");
1683+
};
1684+
activity.AddException(exception, new TagList { { "hello", "world" } });
1685+
events = activity.Events.ToList();
1686+
Assert.Equal(4, events.Count);
1687+
Assert.Equal(ExceptionEventName, events[3].Name);
1688+
Assert.Equal(new TagList
1689+
{
1690+
{ "hello", "world" },
1691+
{ "exception.escaped", "true" },
1692+
{ "exception.message", "Overridden message" },
1693+
{ "exception.stacktrace", "Overridden stacktrace" },
1694+
{ "exception.type", "Overridden type" }
1695+
},
1696+
events[3].Tags);
1697+
1698+
ActivityListener listener1 = new ActivityListener();
1699+
listener1.ShouldListenTo = (activitySource) => object.ReferenceEquals(activitySource, aSource);
1700+
listener1.Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData;
1701+
ActivitySource.AddActivityListener(listener1);
1702+
listener1.ExceptionRecorder = (Activity activity, Exception exception, ref TagList theTags) =>
1703+
{
1704+
theTags.Remove(new KeyValuePair<string, object?>("exception.message", "Overridden message"));
1705+
theTags.Remove(new KeyValuePair<string, object?>("exception.stacktrace", "Overridden stacktrace"));
1706+
theTags.Remove(new KeyValuePair<string, object?>("exception.type", "Overridden type"));
1707+
theTags.Add("secondListener", "win");
1708+
};
1709+
activity.AddException(exception, new TagList { { "hello", "world" } });
1710+
events = activity.Events.ToList();
1711+
Assert.Equal(5, events.Count);
1712+
Assert.Equal(ExceptionEventName, events[4].Name);
1713+
Assert.Equal(new TagList
1714+
{
1715+
{ "hello", "world" },
1716+
{ "exception.escaped", "true" },
1717+
{ "secondListener", "win" },
1718+
{ "exception.message", exception.Message },
1719+
{ "exception.stacktrace", exception.ToString() },
1720+
{ "exception.type", exception.GetType().ToString() },
1721+
},
1722+
events[4].Tags);
1723+
}
1724+
16291725
[Fact]
16301726
public void TestIsAllDataRequested()
16311727
{

0 commit comments

Comments
 (0)