From 515a0c99d8ba002fe79c5e71606af607b4b86100 Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Tue, 31 Mar 2020 20:51:55 -0700 Subject: [PATCH 1/2] System.Diagnostics Tracing APIs --- ...em.Diagnostics.DiagnosticSourceActivity.cs | 78 +++- .../src/Resources/Strings.resx | 60 ++-- ...System.Diagnostics.DiagnosticSource.csproj | 7 + .../src/System/Diagnostics/Activity.cs | 325 ++++++++++++++++- .../src/System/Diagnostics/ActivityContext.cs | 95 +++++ .../System/Diagnostics/ActivityDataRequest.cs | 37 ++ .../src/System/Diagnostics/ActivityEvent.cs | 74 ++++ .../src/System/Diagnostics/ActivityKind.cs | 47 +++ .../src/System/Diagnostics/ActivityLink.cs | 44 +++ .../System/Diagnostics/ActivityListener.cs | 55 +++ .../src/System/Diagnostics/ActivitySource.cs | 335 ++++++++++++++++++ .../System/Diagnostics/DiagnosticListener.cs | 2 +- .../tests/ActivitySourceTests.cs | 214 +++++++++++ .../tests/ActivityTests.cs | 112 ++++++ ....Diagnostics.DiagnosticSource.Tests.csproj | 1 + 15 files changed, 1443 insertions(+), 43 deletions(-) create mode 100644 src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityContext.cs create mode 100644 src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityDataRequest.cs create mode 100644 src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityEvent.cs create mode 100644 src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityKind.cs create mode 100644 src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.cs create mode 100644 src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityListener.cs create mode 100644 src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivitySource.cs create mode 100644 src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivitySourceTests.cs 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 8de9ac09bb0a7e..54f18d83d77250 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs @@ -7,7 +7,7 @@ namespace System.Diagnostics { - public partial class Activity + public partial class Activity : IDisposable { public Activity(string operationName) { } public System.Diagnostics.ActivityTraceFlags ActivityTraceFlags { get { throw null; } set { } } @@ -33,8 +33,13 @@ public string? Id #endif get { throw null; } } + + public bool IsAllDataRequested { get { throw null; } set { throw null; }} public System.Diagnostics.ActivityIdFormat IdFormat { get { throw null; } } + public System.Diagnostics.ActivityKind Kind { get; } public string OperationName { get { throw null; } } + public string DisplayName { get { throw null; } set { throw null; } } + public System.Diagnostics.ActivitySource Source { get { throw null; } } public System.Diagnostics.Activity? Parent { get { throw null; } } public string? ParentId { get { throw null; } } public System.Diagnostics.ActivitySpanId ParentSpanId { get { throw null; } } @@ -43,9 +48,13 @@ public string? Id public System.Diagnostics.ActivitySpanId SpanId { get { throw null; } } public System.DateTime StartTimeUtc { get { throw null; } } public System.Collections.Generic.IEnumerable> Tags { get { throw null; } } + public System.Collections.Generic.IEnumerable Events { get { throw null; } } + public System.Collections.Generic.IEnumerable Links { get { throw null; } } public System.Diagnostics.ActivityTraceId TraceId { get { throw null; } } public string? TraceStateString { get { throw null; } set { } } public System.Diagnostics.Activity AddBaggage(string key, string? value) { throw null; } + public System.Diagnostics.Activity AddEvent(System.Diagnostics.ActivityEvent e) { throw null; } + public System.Diagnostics.Activity AddLink(System.Diagnostics.ActivityLink link) { throw null; } public System.Diagnostics.Activity AddTag(string key, string? value) { throw null; } public string? GetBaggageItem(string key) { throw null; } public System.Diagnostics.Activity SetEndTime(System.DateTime endTimeUtc) { throw null; } @@ -55,6 +64,10 @@ public string? Id public System.Diagnostics.Activity SetStartTime(System.DateTime startTimeUtc) { throw null; } public System.Diagnostics.Activity Start() { throw null; } public void Stop() { } + public void Dispose() { } + public void SetCustomProperty(string propertyName, object? propertyValue) { } + public object? GetCustomProperty(string propertyName) { throw null; } + public ActivityContext Context { get { throw null; } } } public enum ActivityIdFormat { @@ -82,6 +95,23 @@ public void CopyTo(System.Span destination) { } public string ToHexString() { throw null; } public override string ToString() { throw null; } } + public sealed class ActivitySource : IDisposable + { + public ActivitySource(string name, System.Version version) { throw null; } + public string Name { get { throw null; } } + public System.Version Version { get { throw null; } } + public bool HasListeners { get { throw null; } } + public System.Diagnostics.Activity? StartActivity(string name, System.Diagnostics.ActivityKind kind = ActivityKind.Internal) { throw null; } + public System.Diagnostics.Activity? StartActivity(string name, System.Diagnostics.ActivityKind kind, System.Diagnostics.ActivityContext parentContext, System.Collections.Generic.IEnumerable>? tags = null, System.Collections.Generic.IEnumerable? links = null, System.DateTimeOffset startTime = default) { throw null; } + public System.Diagnostics.Activity? StartActivity(string name, System.Diagnostics.ActivityKind kind, string parentId, System.Collections.Generic.IEnumerable>? tags = null, System.Collections.Generic.IEnumerable? links = null, System.DateTimeOffset startTime = default) { throw null; } + public void Dispose() { throw null; } + public static IDisposable AddActivityListener( + System.Func listenToSource, + System.Func>?, System.Collections.Generic.IEnumerable?, System.Diagnostics.ActivityDataRequest> getActivityDataRequestUsingContext, + System.Func>?, System.Collections.Generic.IEnumerable?, System.Diagnostics.ActivityDataRequest> getActivityDataRequestUsingParentId, + System.Action onActivityStarted, + System.Action onActivityStopped) { throw null; } + } [System.FlagsAttribute] public enum ActivityTraceFlags { @@ -121,4 +151,50 @@ public virtual void OnActivityImport(System.Diagnostics.Activity activity, objec public System.Diagnostics.Activity StartActivity(System.Diagnostics.Activity activity, object? args) { throw null; } public void StopActivity(System.Diagnostics.Activity activity, object? args) { } } + public enum ActivityDataRequest + { + None, + PropagationData, + AllData, + AllDataAndRecorded + } + public enum ActivityKind + { + Internal = 1, + Server = 2, + Client = 3, + Producer = 4, + Consumer = 5, + } + public sealed class ActivityEvent + { + public ActivityEvent(string name) {throw null; } + public ActivityEvent(string name, System.DateTimeOffset timestamp) { throw null; } + public ActivityEvent(string name, System.DateTimeOffset timestamp, System.Collections.Generic.IReadOnlyDictionary attributes) { throw null; } + public ActivityEvent(string name, System.Collections.Generic.IReadOnlyDictionary attributes) { throw null; } + public string Name { get { throw null; } } + public System.DateTimeOffset Timestamp { get { throw null; } } + public System.Collections.Generic.IReadOnlyDictionary Attributes { get { throw null; } } + } + public readonly struct ActivityContext : System.IEquatable + { + public ActivityContext(System.Diagnostics.ActivityTraceId traceId, System.Diagnostics.ActivitySpanId spanId, System.Diagnostics.ActivityTraceFlags traceOptions, string? traceState = null) { throw null; } + public System.Diagnostics.ActivityTraceId TraceId { get; } + public System.Diagnostics.ActivitySpanId SpanId { get; } + public System.Diagnostics.ActivityTraceFlags TraceFlags { get; } + public string? TraceState { get; } + public static bool operator ==(System.Diagnostics.ActivityContext context1, System.Diagnostics.ActivityContext context2) { throw null; } + public static bool operator !=(System.Diagnostics.ActivityContext context1, System.Diagnostics.ActivityContext context2) { throw null; } + public bool Equals(System.Diagnostics.ActivityContext context) { throw null; } + public override bool Equals(object? obj) { throw null; } + public override int GetHashCode() { throw null; } + } + public readonly struct ActivityLink + { + public ActivityLink(System.Diagnostics.ActivityContext context) { throw null; } + public ActivityLink(System.Diagnostics.ActivityContext context, System.Collections.Generic.IReadOnlyDictionary? attributes) { throw null; } + public System.Diagnostics.ActivityContext Context { get; } + public System.Collections.Generic.IReadOnlyDictionary? Attributes { get; } + } } + diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx index 724068b2dc9e40..18036744326ffb 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx @@ -1,17 +1,17 @@  - @@ -144,9 +144,15 @@ "Can not change format for an activity that was already started" + + "Can not add link to activity after it has been started" + "Can not set ParentId on activity which has parent" + + "Invalid SpanId or TraceId" + "StartTime is not UTC" 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 eb0dc38fc393ab..068afdc4fa0a2d 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj @@ -32,6 +32,13 @@ Common\System\HexConverter.cs + + + + + + + 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 b98571feffb22d..6a376e0e6579e2 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs @@ -5,6 +5,7 @@ using System.Buffers.Binary; using System.Buffers.Text; using System.Collections.Generic; +using System.Collections.Concurrent; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -29,11 +30,14 @@ namespace System.Diagnostics /// but the exception is suppressed, and the operation does something reasonable (typically /// doing nothing). /// - public partial class Activity + public partial class Activity : IDisposable { #pragma warning disable CA1825 // Array.Empty() doesn't exist in all configurations private static readonly IEnumerable> s_emptyBaggageTags = new KeyValuePair[0]; + private static readonly IEnumerable s_emptyLinks = new ActivityLink[0]; + private static readonly IEnumerable s_emptyEvents = new ActivityEvent[0]; #pragma warning restore CA1825 + private static ActivitySource s_defaultSource = new ActivitySource(string.Empty, new Version()); private const byte ActivityTraceFlagsIsSet = 0b_1_0000000; // Internal flag to indicate if flags have been set private const int RequestIdMaxLength = 1024; @@ -70,8 +74,17 @@ public partial class Activity private byte _w3CIdFlags; - private KeyValueListNode? _tags; - private KeyValueListNode? _baggage; + private LinkedListNode>? _tags; + private LinkedListNode>? _baggage; + private LinkedListNode? _links; + private LinkedListNode? _events; + private ConcurrentDictionary? _customProperties; + private string? _displayName; + + /// + /// Kind describes the relationship between the Activity, its parents, and its children in a Trace. + /// + public ActivityKind Kind { get; private set; } = ActivityKind.Internal; /// /// An operation name is a COARSEST name that is useful grouping/filtering. @@ -81,6 +94,25 @@ public partial class Activity /// public string OperationName { get; } = null!; + /// + /// An operation name is a COARSEST name that is useful grouping/filtering. + /// The name is typically a compile-time constant. Names of Rest APIs are + /// reasonable, but arguments (e.g. specific accounts etc), should not be in + /// the name but rather in the tags. + /// + public string DisplayName + { + get => _displayName ?? OperationName; + set => _displayName = value; + } + + /// + /// Get the ActivitySource object associated with this Activity. + /// All Activities created from constructors will have a singelton source. + /// Otherwise, the source will be holding the object created the Activity through ActivitySource.StartActivity. + /// + public ActivitySource Source { get; private set; } + /// /// If the Activity that created this activity is from the same process you can get /// that Activity with Parent. However, this can be null if the Activity has no @@ -216,12 +248,12 @@ public string? RootId { get { - KeyValueListNode? tags = _tags; + LinkedListNode>? tags = _tags; return tags != null ? Iterate(tags) : s_emptyBaggageTags; - static IEnumerable> Iterate(KeyValueListNode? tags) + static IEnumerable> Iterate(LinkedListNode>? tags) { do { @@ -233,6 +265,52 @@ public string? RootId } } + /// + /// Events is the list of all objects which attached to this Activity object. + /// If there is no any object attached to the Activity object, Events will return empty list. + /// + public IEnumerable Events + { + get + { + LinkedListNode? events = _events; + return events != null ? Iterate(events) : s_emptyEvents; + + static IEnumerable Iterate(LinkedListNode? events) + { + do + { + yield return events!.keyValue; + events = events.Next; + } + while (events != null); + } + } + } + + /// + /// Links is the list of all objects which contain the attached to this Activity object. + /// If there is no any object attached to the Activity object, Links will return empty list. + /// + public IEnumerable Links + { + get + { + LinkedListNode? links = _links; + return links != null ? Iterate(links) : s_emptyLinks; + + static IEnumerable Iterate(LinkedListNode? links) + { + do + { + yield return links!.keyValue; + links = links.Next; + } + while (links != null); + } + } + } + /// /// Baggage is string-string key-value pairs that represent information that will /// be passed along to children of this activity. Baggage is serialized @@ -260,7 +338,7 @@ public string? RootId Debug.Assert(activity != null); do { - for (KeyValueListNode? baggage = activity._baggage; baggage != null; baggage = baggage.Next) + for (LinkedListNode>? baggage = activity._baggage; baggage != null; baggage = baggage.Next) { yield return baggage.keyValue; } @@ -293,6 +371,10 @@ public string? RootId /// Operation's name public Activity(string operationName) { + Source = s_defaultSource; + // Allow data by default in the constructor to keep the compatability. + IsAllDataRequested = true; + if (string.IsNullOrEmpty(operationName)) { NotifyError(new ArgumentException(SR.OperationNameInvalid)); @@ -310,8 +392,13 @@ public Activity(string operationName) /// 'this' for convenient chaining public Activity AddTag(string key, string? value) { - KeyValueListNode? currentTags = _tags; - KeyValueListNode newTags = new KeyValueListNode() { keyValue = new KeyValuePair(key, value) }; + if (!IsAllDataRequested) + { + return this; + } + + LinkedListNode>? currentTags = _tags; + LinkedListNode> newTags = new LinkedListNode>(new KeyValuePair(key, value)); do { newTags.Next = currentTags; @@ -321,6 +408,52 @@ public Activity AddTag(string key, string? value) return this; } + /// + /// Add object to the list. + /// + /// object of to add to the attached events list. + /// 'this' for convenient chaining + public Activity AddEvent(ActivityEvent e) + { + if (!IsAllDataRequested || e == null) + { + return this; + } + + LinkedListNode? currentEvents = _events; + LinkedListNode newEvents = new LinkedListNode(e); + do + { + newEvents.Next = currentEvents; + currentEvents = Interlocked.CompareExchange(ref _events, newEvents, currentEvents); + } while (!ReferenceEquals(newEvents.Next, currentEvents)); + + return this; + } + + /// + /// Add object to the list. + /// + /// object of to add to the attached links list. + /// 'this' for convenient chaining + public Activity AddLink(ActivityLink link) + { + if (!IsAllDataRequested) + { + return this; + } + + LinkedListNode? currentLinks = _links; + LinkedListNode newLinks = new LinkedListNode(link); + do + { + newLinks.Next = currentLinks; + currentLinks = Interlocked.CompareExchange(ref _links, newLinks, currentLinks); + } while (!ReferenceEquals(newLinks.Next, currentLinks)); + + return this; + } + /// /// Update the Activity to have baggage with an additional 'key' and value 'value'. /// This shows up in the enumeration as well as the @@ -332,8 +465,13 @@ public Activity AddTag(string key, string? value) /// 'this' for convenient chaining public Activity AddBaggage(string key, string? value) { - KeyValueListNode? currentBaggage = _baggage; - KeyValueListNode newBaggage = new KeyValueListNode() { keyValue = new KeyValuePair(key, value) }; + if (!IsAllDataRequested) + { + return this; + } + + LinkedListNode>? currentBaggage = _baggage; + LinkedListNode> newBaggage = new LinkedListNode>(new KeyValuePair(key, value)); do { @@ -437,6 +575,23 @@ public Activity SetEndTime(DateTime endTimeUtc) return this; } + /// + /// Get the context of the activity. Context becomes valid only if the activity has been started. + /// otherwise will default context. + /// + public ActivityContext Context + { + get + { + if (_id == null && _spanId == null) + { + return default; + } + + return new ActivityContext(new ActivityTraceId(_traceId), new ActivitySpanId(_spanId ?? _id), ActivityTraceFlags, _traceState); + } + } + /// /// Starts activity /// @@ -464,7 +619,7 @@ public Activity Start() if (parent != null) { // The parent change should not form a loop. We are actually guaranteed this because - // 1. Unstarted activities can't be 'Current' (thus can't be 'parent'), we throw if you try. + // 1. Un-started activities can't be 'Current' (thus can't be 'parent'), we throw if you try. // 2. All started activities have a finite parent change (by inductive reasoning). Parent = parent; } @@ -492,6 +647,8 @@ public Activity Start() _id = GenerateHierarchicalId(); SetCurrent(this); + + Source.NotifyActivityStart(this); } return this; } @@ -521,6 +678,8 @@ public void Stop() } SetCurrent(Parent); + + Source.NotifyActivityStop(this); } } @@ -604,6 +763,12 @@ public ActivityTraceId TraceId /// public bool Recorded { get => (ActivityTraceFlags & ActivityTraceFlags.Recorded) != 0; } + /// + /// Indicate if the this Activity object should be populated with all the propagation info and also all other + /// properties such as Links, Tags, and Events. + /// + public bool IsAllDataRequested { get; set;} + /// /// Return the flags (defined by the W3C ID specification) associated with the activity. /// @@ -718,6 +883,132 @@ private static bool IsW3CId(string id) ('0' <= id[1] && id[1] <= '9' || 'a' <= id[1] && id[1] <= 'e'); } + /// + /// Dispose will stop the Activity if it is already started and notify any event listeners. Nothing will happen otherwise. + /// + public void Dispose() + { + if (!IsFinished) + { + Stop(); + } + } + + /// + /// SetCustomProperty allow attaching any custom object to this Activity object. + /// If the property name was previously associated with other object, SetCustomProperty will update to use the new propert value instead. + /// + /// The name to associate the value with. + /// The object to attach and map to the property name. + public void SetCustomProperty(string propertyName, object? propertyValue) + { + if (!IsAllDataRequested) + { + return; + } + + // We don't check null name here as the dictionary is performing this check anyway. + + if (_customProperties == null) + { + Interlocked.CompareExchange(ref _customProperties, new ConcurrentDictionary(), null); + } + + _customProperties[propertyName] = propertyValue!; + } + + /// + /// GetCustomProperty retrieve previously attached object mapped to the property name. + /// + /// The name to get the associated object with. + /// The object mapped to the property name. Or null if there is no mapping previously done with this property name. + public object? GetCustomProperty(string propertyName) + { + // We don't check null name here as the dictionary is performing this check anyway. + + if (_customProperties == null) + { + return null; + } + + return _customProperties.TryGetValue(propertyName, out object? o) ? o! : null; + } + + internal static Activity CreateAndStart(ActivitySource source, string name, ActivityKind kind, string? parentId, ActivityContext parentContext, + IEnumerable>? tags, IEnumerable? links, + DateTimeOffset startTime, ActivityDataRequest request) + { + Activity activity = new Activity(name); + + activity.Source = source; + activity.Kind = kind; + + if (parentId != null) + { + activity._parentId = parentId; + } + else if (parentContext != default) + { + activity._traceId = parentContext.TraceId.ToString(); + activity._parentSpanId = parentContext.SpanId.ToString(); + activity.ActivityTraceFlags = parentContext.TraceFlags; + activity._traceState = parentContext.TraceState; + } + else + { + Activity? parent = Current; + if (parent != null) + { + // The parent change should not form a loop. We are actually guaranteed this because + // 1. Un-started activities can't be 'Current' (thus can't be 'parent'), we throw if you try. + // 2. All started activities have a finite parent change (by inductive reasoning). + activity.Parent = parent; + } + } + + activity.IdFormat = + ForceDefaultIdFormat ? DefaultIdFormat : + activity.Parent != null ? activity.Parent.IdFormat : + activity._parentSpanId != null ? ActivityIdFormat.W3C : + activity._parentId == null ? DefaultIdFormat : + IsW3CId(activity._parentId) ? ActivityIdFormat.W3C : + ActivityIdFormat.Hierarchical; + + if (activity.IdFormat == ActivityIdFormat.W3C) + activity.GenerateW3CId(); + else + activity._id = activity.GenerateHierarchicalId(); + + if (links != null) + { + foreach (ActivityLink link in links) + { + activity._links = new LinkedListNode(link, activity._links); + } + } + + if (tags != null) + { + foreach (KeyValuePair tag in tags) + { + activity._tags = new LinkedListNode>(tag, activity._tags); + } + } + + activity.StartTimeUtc = startTime == default ? DateTime.UtcNow : startTime.DateTime; + + activity.IsAllDataRequested = request == ActivityDataRequest.AllData || request == ActivityDataRequest.AllDataAndRecorded; + + if (request == ActivityDataRequest.AllDataAndRecorded) + { + activity.ActivityTraceFlags |= ActivityTraceFlags.Recorded; + } + + SetCurrent(activity); + + return activity; + } + /// /// Set the ID (lazily, avoiding strings if possible) to a W3C ID (using the /// traceId from the parent if possible @@ -946,10 +1237,16 @@ public ActivityIdFormat IdFormat /// /// Having our own key-value linked list allows us to be more efficient /// - private partial class KeyValueListNode + private partial class LinkedListNode { - public KeyValuePair keyValue; - public KeyValueListNode? Next; + public LinkedListNode(T value) => keyValue = value; + public LinkedListNode(T value, LinkedListNode? next) + { + keyValue = value; + Next = next; + } + public T keyValue; + public LinkedListNode? Next; } [Flags] diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityContext.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityContext.cs new file mode 100644 index 00000000000000..ca04080cdbc8ed --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityContext.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace System.Diagnostics +{ + /// + /// ActivityContext representation conforms to the w3c TraceContext specification. It contains two identifiers + /// a TraceId and a SpanId - along with a set of common TraceFlags and system-specific TraceState values. + /// + public readonly struct ActivityContext : IEquatable + { + /// + /// Construct a new object of ActivityContext. + /// + /// A trace identifier. + /// A span identifier + /// Contain details about the trace. + /// Carries system-specific configuration data. + public ActivityContext(ActivityTraceId traceId, ActivitySpanId spanId, ActivityTraceFlags traceFlags, string? traceState = null) + { + // We don't allow creating context with invalid span or trace Ids. + if (spanId.ToHexString() == "0000000000000000" || traceId.ToHexString() == "00000000000000000000000000000000") + { + throw new ArgumentException(SR.SpanIdOrTraceIdInvalid, traceId == default ? nameof(traceId) : nameof(spanId)); + } + + TraceId = traceId; + SpanId = spanId; + TraceFlags = traceFlags; + TraceState = traceState; + } + + /// + /// The trace identifier + /// + public ActivityTraceId TraceId { get; } + + /// + /// The span identifier + /// + public ActivitySpanId SpanId { get; } + + /// + /// The flags for the details about the trace. + /// + public ActivityTraceFlags TraceFlags { get; } + + /// + /// system-specific configuration data. + /// + public string? TraceState { get; } + + public static bool operator ==(ActivityContext context1, ActivityContext context2) + { + return context1.SpanId == context2.SpanId && + context1.TraceId == context2.TraceId && + context1.TraceFlags == context2.TraceFlags && + context1.TraceState == context2.TraceState; + } + + public static bool operator !=(ActivityContext context1, ActivityContext context2) => !(context1 == context2); + + public bool Equals(ActivityContext context) + { + return this == context; + } + + public override bool Equals(object? obj) + { + if (obj is ActivityContext context) + return this == context; + return false; + } + + public override int GetHashCode() + { + if (this == default) + return 0; + + // HashCode.Combine would be the best but we need to compile for the full framework which require adding dependency + // on the extensions package. Considering this simple type and hashing is not expected to be used, we are implementing + // the hashing manually. + int hash = 5381; + hash = ((hash << 5) + hash) + TraceId.GetHashCode(); + hash = ((hash << 5) + hash) + SpanId.GetHashCode(); + hash = ((hash << 5) + hash) + (int) TraceFlags; + hash = ((hash << 5) + hash) + (TraceState == null ? 0 : TraceState.GetHashCode()); + + return hash; + } + } +} diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityDataRequest.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityDataRequest.cs new file mode 100644 index 00000000000000..96eabcbe86cdd2 --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityDataRequest.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Diagnostics +{ + /// + /// Used by ActivityListener to indicate what amount of data should be collected for this Activity + /// Requesting more data causes greater performance overhead to collect it. + /// + public enum ActivityDataRequest + { + /// + /// The Activity object doesn't need to be created + /// + None, + + /// + /// The Activity object needs to be created. It will have Name, Source, Id and Baggage. + /// Other properties are unnecessary and will be ignored by this listener. + /// + PropagationData, + + /// + /// The activity object should be populated with all the propagation info and also all other + /// properties such as Links, Tags, and Events. Activity.IsAllDataRequested will return true. + /// + AllData, + + /// + /// The activity object should be populated the same as the AllData case and additionally + /// Activity.IsRecorded is set true. For activities using W3C trace ids this sets a flag bit in the + /// ID that will be propagated downstream requesting that trace is recorded everywhere. + /// + AllDataAndRecorded + } +} \ No newline at end of file diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityEvent.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityEvent.cs new file mode 100644 index 00000000000000..433f03567cc686 --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityEvent.cs @@ -0,0 +1,74 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace System.Diagnostics +{ + /// + /// A text annotation associated with a collection of attributes. + /// + public sealed class ActivityEvent + { + private static readonly IReadOnlyDictionary s_emptyAttributes = new Dictionary(new Dictionary()); + + /// + /// Initializes a new instance of the class. + /// + /// Event name. + public ActivityEvent(string name) : this(name, DateTimeOffset.UtcNow, s_emptyAttributes) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Event name. + /// Event timestamp. Timestamp MUST only be used for the events that happened in the past, not at the moment of this call. + public ActivityEvent(string name, DateTimeOffset timestamp) : this(name, timestamp, s_emptyAttributes) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Event name. + /// Event attributes. + public ActivityEvent(string name, IReadOnlyDictionary attributes) + { + this.Name = name ?? string.Empty; + this.Attributes = attributes ?? s_emptyAttributes; + this.Timestamp = DateTimeOffset.UtcNow; + } + + /// + /// Initializes a new instance of the class. + /// + /// Event name. + /// Event timestamp. Timestamp MUST only be used for the events that happened in the past, not at the moment of this call. + /// Event attributes. + public ActivityEvent(string name, DateTimeOffset timestamp, IReadOnlyDictionary attributes) + { + this.Name = name ?? string.Empty; + this.Attributes = attributes ?? s_emptyAttributes; + this.Timestamp = timestamp != default ? timestamp : DateTimeOffset.UtcNow; + } + + /// + /// Gets the name. + /// + public string Name { get; } + + /// + /// Gets the timestamp. + /// + public DateTimeOffset Timestamp { get; } + + /// + /// Gets the collection of attributes associated with the event. + /// + public IReadOnlyDictionary Attributes { get; } + } +} \ No newline at end of file diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityKind.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityKind.cs new file mode 100644 index 00000000000000..1325930078638d --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityKind.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Diagnostics +{ + /// + /// Kind describes the relationship between the Activity, its parents, and its children in a Trace. + /// -------------------------------------------------------------------------------- + /// ActivityKind Synchronous Asynchronous Remote Incoming Remote Outgoing + /// -------------------------------------------------------------------------------- + /// Internal + /// Client yes yes + /// Server yes yes + /// Producer yes maybe + /// Consumer yes maybe + /// -------------------------------------------------------------------------------- + /// + public enum ActivityKind + { + /// + /// Default value. + /// Indicates that the Activity represents an internal operation within an application, as opposed to an operations with remote parents or children. + /// + Internal = 1, + + /// + /// Server activity represents request incoming from external component. + /// + Server = 2, + + /// + /// Client activity represents outgoing request to the external component. + /// + Client = 3, + + /// + /// Producer activity represents output provided to external components. + /// + Producer = 4, + + /// + /// Consumer activity represents output received from an external component. + /// + Consumer = 5, + } +} diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.cs new file mode 100644 index 00000000000000..c441fdfe2b3a1f --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace System.Diagnostics +{ + /// + /// Activity may be linked to zero or more other that are causally related. + /// Links can point to ActivityContexts inside a single Trace or across different Traces. + /// Links can be used to represent batched operations where a Activity was initiated by multiple initiating Activities, + /// each representing a single incoming item being processed in the batch. + /// + public readonly struct ActivityLink + { + /// + /// Construct a new object which can be linked to an Activity object through method. + /// + /// The trace Activity context + public ActivityLink(ActivityContext context) : this(context, null) {} + + /// + /// Construct a new object which can be linked to an Activity object through method. + /// + /// The trace Activity context + /// The key-value pair list of attributes which associated to the + public ActivityLink(ActivityContext context, IReadOnlyDictionary? attributes) + { + Context = context; + Attributes = attributes; + } + + /// + /// Retrieve the object inside this object. + /// + public ActivityContext Context { get; } + + /// + /// Retrieve the key-value pair list of attributes attached with the . + /// + public IReadOnlyDictionary? Attributes { get; } + } +} diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityListener.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityListener.cs new file mode 100644 index 00000000000000..dafe2d1a9432a5 --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityListener.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace System.Diagnostics +{ + /// + /// ActivityListener allows listening to the start and stop Activity events and give the oppertunity to decide creating the Activity for sampling scenarios. + /// + internal class ActivityListener : IDisposable + { + internal System.Func ListenToSource { get; } + internal Func>?, IEnumerable?, ActivityDataRequest> GetActivityDataRequestUsingContext { get; } + internal Func>?, IEnumerable?, ActivityDataRequest> GetActivityDataRequestUsingParentId { get; } + internal Action OnActivityStarted { get; } + internal Action OnActivityStopped { get; } + + /// + /// Create a listener using the callback delegates. This factory method is useful in the scenario of the Dependency Injection (DI) which need a way to easily create a listener + /// using the reflection without the need to subclass the ActivityListener. + /// + internal ActivityListener( + System.Func listenToSource, + Func>?, IEnumerable?, ActivityDataRequest> getActivityDataRequestUsingContext, + Func>?, IEnumerable?, ActivityDataRequest> getActivityDataRequestUsingParentId, + Action onActivityStarted, + Action onActivityStopped) + { + if (listenToSource == null) + throw new ArgumentNullException(nameof(listenToSource)); + + if (getActivityDataRequestUsingContext == null) + throw new ArgumentNullException(nameof(getActivityDataRequestUsingContext)); + + if (getActivityDataRequestUsingParentId == null) + throw new ArgumentNullException(nameof(getActivityDataRequestUsingParentId)); + + if (onActivityStarted == null) + throw new ArgumentNullException(nameof(onActivityStarted)); + + if (onActivityStopped == null) + throw new ArgumentNullException(nameof(onActivityStopped)); + + ListenToSource = listenToSource; + GetActivityDataRequestUsingContext = getActivityDataRequestUsingContext; + GetActivityDataRequestUsingParentId = getActivityDataRequestUsingParentId; + OnActivityStarted = onActivityStarted; + OnActivityStopped = onActivityStopped; + } + + public void Dispose() => ActivitySource.DetachListener(this); + } +} diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivitySource.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivitySource.cs new file mode 100644 index 00000000000000..a2fd09c4920d7a --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivitySource.cs @@ -0,0 +1,335 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using System.Collections.Generic; + +namespace System.Diagnostics +{ + public sealed class ActivitySource : IDisposable + { + private static SynchronizedList s_activeSources = new SynchronizedList(); + private static SynchronizedList s_allListeners = new SynchronizedList(); + private SynchronizedList? _listeners; + + private ActivitySource() { throw new InvalidOperationException(); } + + /// + /// Construct an ActivitySource object with the input name + /// + /// The name of the ActivitySource object + /// The version of the component publishing the tracing info. + public ActivitySource(string name, Version version) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + Name = name; + Version = version; + + s_activeSources.Add(this); + + if (s_allListeners.Count > 0) + { + s_allListeners.EnumWithAction(listener => { + if (listener.ListenToSource(this)) + { + AddListener(listener); + } + }); + } + } + + /// + /// Returns the ActivitySource name. + /// + public string Name { get; } + + /// + /// Returns the ActivitySource version. + /// + public Version Version { get; } + + /// + /// Check if there is any listeners for this ActivitySource. + /// This property can be helpful to tell if there is no listener, then no need to create Activity object + /// and avoid creating the objects needed to create Activity (e.g. ActivityContext) + /// Example of that is http scenario which can avoid reading the context data from the wire. + /// + public bool HasListeners => _listeners != null && _listeners.Count > 0; + + /// + /// Creates a new object if there is any listener to the Activity, returns null otherwise. + /// + /// The operation name of the Activity + /// The + /// The created object or null if there is no any event listener. + public Activity? StartActivity(string name, ActivityKind kind = ActivityKind.Internal) + => StartActivity(name, kind, default, null, null, null, default); + + /// + /// Creates a new object if there is any listener to the Activity events, returns null otherwise. + /// + /// The operation name of the Activity. + /// The + /// The parent object to initialize the created Activity object with. + /// The optional tags list to initialize the created Activity object with. + /// The optional list to initialize the created Activity object with. + /// The optional start timestamp to set on the created Activity object. + /// The created object or null if there is no any listener. + public Activity? StartActivity(string name, ActivityKind kind, ActivityContext parentContext, IEnumerable>? tags = null, IEnumerable? links = null, DateTimeOffset startTime = default) + => StartActivity(name, kind, parentContext, null, tags, links, startTime); + + /// + /// Creates a new object if there is any listener to the Activity events, returns null otherwise. + /// + /// The operation name of the Activity. + /// The + /// The parent Id to initialize the created Activity object with. + /// The optional tags list to initialize the created Activity object with. + /// The optional list to initialize the created Activity object with. + /// The optional start timestamp to set on the created Activity object. + /// The created object or null if there is no any listener. + public Activity? StartActivity(string name, ActivityKind kind, string parentId, IEnumerable>? tags = null, IEnumerable? links = null, DateTimeOffset startTime = default) + => StartActivity(name, kind, default, parentId, tags, links, startTime); + + private Activity? StartActivity(string name, ActivityKind kind, ActivityContext context, string? parentId, IEnumerable>? tags, IEnumerable? links, DateTimeOffset startTime) + { + // _listeners can get assigned to null in Dispose. + SynchronizedList? listeners = _listeners; + if (listeners == null || listeners.Count == 0) + { + return null; + } + + Activity? activity = null; + + ActivityDataRequest dateRequest = ActivityDataRequest.None; + + if (parentId != null) + { + listeners.EnumWithFunc(listener => { + ActivityDataRequest dr = listener.GetActivityDataRequestUsingParentId(this, name, kind, parentId, tags, links); + if (dr > dateRequest) + { + dateRequest = dr; + } + + // Stop the enumeration if we get the max value RecordingAndSampling. + return dateRequest != ActivityDataRequest.AllDataAndRecorded; + }); + } + else + { + listeners.EnumWithFunc(listener => { + ActivityDataRequest dr = listener.GetActivityDataRequestUsingContext(this, name, kind, context, tags, links); + if (dr > dateRequest) + { + dateRequest = dr; + } + + // Stop the enumeration if we get the max value RecordingAndSampling. + return dateRequest != ActivityDataRequest.AllDataAndRecorded; + }); + } + + if (dateRequest != ActivityDataRequest.None) + { + activity = Activity.CreateAndStart(this, name, kind, parentId, default, tags, links, startTime, dateRequest); + listeners.EnumWithAction(listener => listener.OnActivityStarted(activity)); + } + + return activity; + } + + /// + /// Dispose the ActivitySource object and remove the current instance from the global list. empty the listeners list too. + /// + public void Dispose() + { + _listeners = null; + s_activeSources.Remove(this); + } + + /// + /// Add a listener to the starting and stopping events. + /// + /// The callback which will get called every time the ActivitySource get created to decide if need to listen to this source. + /// The callback which will get called every time the ActivitySource try to creates a new Activity object using ActivityContext. + /// The callback which will get called every time the ActivitySource try to creates a new Activity object using ParentId. + /// The callback which get called everytime the activity get started. + /// The callback which get called everytime the activity get stopped. + /// The disposable object represent the listener. When disposing the listener, it will stop the Activity event notification. + public static IDisposable AddActivityListener( + System.Func listenToSource, + System.Func>?, System.Collections.Generic.IEnumerable?, System.Diagnostics.ActivityDataRequest> getActivityDataRequestUsingContext, + System.Func>?, System.Collections.Generic.IEnumerable?, System.Diagnostics.ActivityDataRequest> getActivityDataRequestUsingParentId, + Action onActivityStarted, + Action onActivityStopped) + { + ActivityListener listener = new ActivityListener(listenToSource, getActivityDataRequestUsingContext, getActivityDataRequestUsingParentId, onActivityStarted, onActivityStopped); + s_allListeners.Add(listener); + + s_activeSources.EnumWithAction(source => { + if (listener.ListenToSource(source)) + { + source.AddListener(listener); + } + }); + + return listener; + } + + internal void AddListener(ActivityListener listener) + { + if (_listeners == null) + { + Interlocked.CompareExchange(ref _listeners, new SynchronizedList(), null); + } + + _listeners.AddIfNotExist(listener); + } + + internal static void DetachListener(ActivityListener listener) + { + s_activeSources.EnumWithAction(source => { + var listeners = source._listeners; + listeners?.Remove(listener); + }); + s_allListeners.Remove(listener); + } + + internal void NotifyActivityStart(Activity activity) + { + Debug.Assert(activity != null); + + // _listeners can get assigned to null in Dispose. + SynchronizedList? listeners = _listeners; + if (listeners != null && listeners.Count > 0) + { + listeners.EnumWithAction(listener => listener.OnActivityStarted(activity)); + } + } + + internal void NotifyActivityStop(Activity activity) + { + Debug.Assert(activity != null); + + // _listeners can get assigned to null in Dispose. + SynchronizedList? listeners = _listeners; + if (listeners != null && listeners.Count > 0) + { + listeners.EnumWithAction(listener => listener.OnActivityStopped(activity)); + } + } + } + + // SynchronizedList is a helper collection which ensure thread safety on the collection + // and allow enumerating the collection items and execute some action on the enumerated item and can detect any change in the collection + // during the enumeration which force restarting the enumeration again. + // Causion: We can have teh action executed on the same item more than once which is ok in our scenarios. + internal class SynchronizedList + { + private List _list; + private uint _version; + + public SynchronizedList() => _list = new List(); + + public void Add(T item) + { + lock (_list) + { + _list.Add(item); + _version++; + } + } + + public void AddIfNotExist(T item) + { + lock (_list) + { + if (!_list.Contains(item)) + { + _list.Add(item); + _version++; + } + } + } + + public bool Remove(T item) + { + lock (_list) + { + if (_list.Remove(item)) + { + _version++; + return true; + } + return false; + } + } + + public int Count => _list.Count; + + public void EnumWithFunc(Func func) + { + uint version = _version; + int index = 0; + + while (index < _list.Count) + { + T item; + lock (_list) + { + if (version != _version) + { + version = _version; + index = 0; + continue; + } + + item = _list[index]; + index++; + } + + // Important to call the func outside the lock. + // This is the whole point we are having this wrapper class. + if (!func(item)) + { + break; + } + } + } + + public void EnumWithAction(Action action) + { + uint version = _version; + int index = 0; + + while (index < _list.Count) + { + T item; + lock (_list) + { + if (version != _version) + { + version = _version; + index = 0; + continue; + } + + item = _list[index]; + index++; + } + + // Important to call the action outside the lock. + // This is the whole point we are having this wrapper class. + action(item); + } + } + + } +} diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/DiagnosticListener.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/DiagnosticListener.cs index bfaeb3f406fbc2..9710efe818ea4b 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/DiagnosticListener.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/DiagnosticListener.cs @@ -279,7 +279,7 @@ private class DiagnosticSubscription : IDisposable // - IsEnabled1Arg invoked for DiagnosticSource.IsEnabled(string) // - IsEnabled3Arg invoked for DiagnosticSource.IsEnabled(string, obj, obj) // Subscriber MUST set both IsEnabled1Arg and IsEnabled3Arg or none of them: - // when Predicate is provided in DiagosticListener.Subscribe, + // when Predicate is provided in DiagnosticListener.Subscribe, // - IsEnabled1Arg is set to predicate // - IsEnabled3Arg falls back to predicate ignoring extra arguments. // similarly, when Func is provided, diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivitySourceTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivitySourceTests.cs new file mode 100644 index 00000000000000..61989996fa3ea0 --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivitySourceTests.cs @@ -0,0 +1,214 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Reflection; +using System.Threading.Tasks; +using Microsoft.DotNet.RemoteExecutor; +using Xunit; + +namespace System.Diagnostics.Tests +{ + public class ActivitySourceTests : IDisposable + { + [Fact] + public void TestConstruction() + { + RemoteExecutor.Invoke(() => { + using (ActivitySource as1 = new ActivitySource("Source1", new Version())) + { + Assert.Equal("Source1", as1.Name); + Assert.Equal(new Version(), as1.Version); + Assert.False(as1.HasListeners); + using (ActivitySource as2 = new ActivitySource("Source2", new Version(1, 1, 1, 2))) + { + Assert.Equal("Source2", as2.Name); + Assert.Equal(new Version(1, 1, 1, 2), as2.Version); + Assert.False(as2.HasListeners); + } + } + }).Dispose(); + } + + [Fact] + public void TestStartActivityWithNoListener() + { + RemoteExecutor.Invoke(() => { + using (ActivitySource aSource = new ActivitySource("SourceActivity", new Version())) + { + Assert.Equal("SourceActivity", aSource.Name); + Assert.Equal(new Version(), aSource.Version); + Assert.False(aSource.HasListeners); + + Activity current = Activity.Current; + using (Activity a1 = aSource.StartActivity("a1")) + { + // no listeners, we should get null activity. + Assert.Null(a1); + Assert.Equal(Activity.Current, current); + } + } + }).Dispose(); + } + + [Fact] + public void TestActivityWithListenerNoActivityCreate() + { + RemoteExecutor.Invoke(() => { + using (ActivitySource aSource = new ActivitySource("SourceActivityListener", new Version())) + { + Assert.False(aSource.HasListeners); + + using (IDisposable listener1 = ActivitySource.AddActivityListener( + (activitySource) => object.ReferenceEquals(aSource, activitySource), + (source, name, kind, context, tags, links) => ActivityDataRequest.None, + (source, name, kind, parentId, tags, links) => ActivityDataRequest.None, + activity => Assert.NotNull(activity), activity => Assert.NotNull(activity))) + { + Assert.True(aSource.HasListeners); + + // The listener is not allowing to create a new Activity. + Assert.Null(aSource.StartActivity("nullActivity")); + } + } + }).Dispose(); + } + + [Fact] + public void TestActivityWithListenerActivityCreateNoDataAllowed() + { + RemoteExecutor.Invoke(() => { + using (ActivitySource aSource = new ActivitySource("SourceActivityListener", new Version())) + { + int counter = 0; + Assert.False(aSource.HasListeners); + + using (IDisposable listener1 = ActivitySource.AddActivityListener( + (activitySource) => object.ReferenceEquals(aSource, activitySource), + (source, name, kind, parentId, tags, links) => ActivityDataRequest.PropagationData, + (source, name, kind, context, tags, links) => ActivityDataRequest.PropagationData, + activity => counter++, activity => counter--)) + { + Assert.True(aSource.HasListeners); + Activity current = Activity.Current; + // The listener allow creating activity but not allowing adding any data to it. + Activity activity = aSource.StartActivity("NotAllDataRequestedActivity"); + Assert.NotNull(activity); + Assert.False(activity.IsAllDataRequested); + Assert.Equal(1, counter); + Assert.Equal(activity, Activity.Current); + Assert.Equal(current, activity.Parent); + + Assert.Equal(0, activity.Tags.Count()); + Assert.Equal(0, activity.Baggage.Count()); + Assert.Equal(0, activity.Links.Count()); + Assert.Equal(0, activity.Events.Count()); + + Assert.True(object.ReferenceEquals(activity, activity.AddTag("key", "value"))); + Assert.True(object.ReferenceEquals(activity, activity.AddBaggage("key", "value"))); + Assert.True(object.ReferenceEquals(activity, activity.AddLink(new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, null))))); + Assert.True(object.ReferenceEquals(activity, activity.AddEvent(new ActivityEvent("e1")))); + + Assert.Equal(0, activity.Tags.Count()); + Assert.Equal(0, activity.Baggage.Count()); + Assert.Equal(0, activity.Links.Count()); + Assert.Equal(0, activity.Events.Count()); + + activity.Dispose(); + Assert.Equal(0, counter); + Assert.Equal(current, Activity.Current); + } + } + }).Dispose(); + } + + [Fact] + public void TestActivityWithListenerActivityCreateAndAllDataRequested() + { + RemoteExecutor.Invoke(() => { + using (ActivitySource aSource = new ActivitySource("SourceActivityListener", new Version())) + { + int counter = 0; + Assert.False(aSource.HasListeners); + + using (IDisposable listener1 = ActivitySource.AddActivityListener( + (activitySource) => object.ReferenceEquals(aSource, activitySource), + (source, name, kind, context, tags, links) => ActivityDataRequest.AllDataAndRecorded, + (source, name, kind, parentId, tags, links) => ActivityDataRequest.AllDataAndRecorded, + activity => counter++, activity => counter--)) + { + Assert.True(aSource.HasListeners); + + // The listener allow creating activity but not allowing adding any data to it. + using (Activity activity = aSource.StartActivity("AllDataRequestedActivity")) + { + Assert.NotNull(activity); + // Assert.True(activity.IsAllDataRequested); + Assert.Equal(1, counter); + + Assert.Equal(0, activity.Tags.Count()); + Assert.Equal(0, activity.Baggage.Count()); + + Assert.True(object.ReferenceEquals(activity, activity.AddTag("key", "value"))); + Assert.True(object.ReferenceEquals(activity, activity.AddBaggage("key", "value"))); + + Assert.Equal(1, activity.Tags.Count()); + Assert.Equal(1, activity.Baggage.Count()); + + using (Activity activity1 = aSource.StartActivity("AllDataRequestedActivity1")) + { + Assert.NotNull(activity1); + Assert.True(activity1.IsAllDataRequested); + Assert.Equal(2, counter); + + Assert.Equal(0, activity1.Links.Count()); + Assert.Equal(0, activity1.Events.Count()); + Assert.True(object.ReferenceEquals(activity1, activity1.AddLink(new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, null))))); + Assert.True(object.ReferenceEquals(activity1, activity1.AddEvent(new ActivityEvent("e1")))); + Assert.Equal(1, activity1.Links.Count()); + Assert.Equal(1, activity1.Events.Count()); + } + Assert.Equal(1, counter); + } + + Assert.Equal(0, counter); + } + } + }).Dispose(); + } + + [Fact] + public void TestActivitySourceAttachedObject() + { + RemoteExecutor.Invoke(() => { + // All Activities created through the constructor should have same source. + Assert.True(object.ReferenceEquals(new Activity("a1").Source, new Activity("a2").Source)); + Assert.True(string.IsNullOrEmpty(new Activity("a3").Source.Name)); + Assert.Equal(new Version(), new Activity("a4").Source.Version); + + using (ActivitySource aSource = new ActivitySource("SourceToTest", new Version(1, 2, 3, 4))) + { + //Ensure at least we have a listener to allow Activity creation + using (IDisposable listener1 = ActivitySource.AddActivityListener( + (activitySource) => object.ReferenceEquals(aSource, activitySource), + (source, name, kind, context, tags, links) => ActivityDataRequest.AllData, + (source, name, kind, parentId, tags, links) => ActivityDataRequest.AllData, + activity => Assert.NotNull(activity), activity => Assert.NotNull(activity))) + { + using (Activity activity = aSource.StartActivity("ActivityToTest")) + { + Assert.True(object.ReferenceEquals(aSource, activity.Source)); + } + } + } + }).Dispose(); + } + + public void Dispose() => Activity.Current = null; + } +} diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs index 153990a6ee49b0..6c94a9d7062f7a 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs @@ -7,6 +7,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; +using System.Reflection; using System.Threading.Tasks; using Microsoft.DotNet.RemoteExecutor; using Xunit; @@ -1347,6 +1348,117 @@ public void ActivityCurrentNotSetToStopped() Assert.Same(started, Activity.Current); } + [Fact] + public void TestDispose() + { + Activity current = Activity.Current; + using (Activity activity = new Activity("Mine").Start()) + { + Assert.Same(activity, Activity.Current); + Assert.Same(current, activity.Parent); + } + + Assert.Same(current, Activity.Current); + } + + [Fact] + public void TestCustomProperties() + { + Activity activity = new Activity("Custom"); + activity.SetCustomProperty("P1", "Prop1"); + activity.SetCustomProperty("P2", "Prop2"); + activity.SetCustomProperty("P3", null); + + Assert.Equal("Prop1", activity.GetCustomProperty("P1")); + Assert.Equal("Prop2", activity.GetCustomProperty("P2")); + Assert.Null(activity.GetCustomProperty("P3")); + Assert.Null(activity.GetCustomProperty("P4")); + + activity.SetCustomProperty("P1", "Prop5"); + Assert.Equal("Prop5", activity.GetCustomProperty("P1")); + + } + + [Fact] + public void TestLinks() + { + Activity activity = new Activity("Links"); + IEnumerable links = activity.Links; + Assert.Equal(0, links.Count()); + + activity.AddLink(new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None))); + activity.AddLink(new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded))); + + links = activity.Links; + Assert.Equal(2, links.Count()); + + activity.AddLink(new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, "I=Cool"))); + links = activity.Links; + Assert.Equal(3, links.Count()); + + Assert.Equal(ActivityTraceFlags.None, links.ElementAt(2).Context.TraceFlags); + Assert.Null(links.ElementAt(2).Context.TraceState); + Assert.Equal(ActivityTraceFlags.Recorded, links.ElementAt(1).Context.TraceFlags); + Assert.Null(links.ElementAt(1).Context.TraceState); + Assert.Equal(ActivityTraceFlags.Recorded, links.ElementAt(0).Context.TraceFlags); + Assert.NotNull(links.ElementAt(0).Context.TraceState); + } + + [Fact] + public void TestKind() + { + Activity activity = new Activity("Kind"); + Assert.Equal(ActivityKind.Internal, activity.Kind); + } + + [Fact] + public void TestDisplayName() + { + Activity activity = new Activity("Op1"); + Assert.Equal("Op1", activity.OperationName); + Assert.Equal("Op1", activity.DisplayName); + + activity.DisplayName = "Op2"; + Assert.Equal("Op1", activity.OperationName); + Assert.Equal("Op2", activity.DisplayName); + } + + [Fact] + public void TestEvent() + { + Activity activity = new Activity("EventTest"); + Assert.Equal(0, activity.Events.Count()); + + DateTimeOffset ts1 = DateTimeOffset.UtcNow; + DateTimeOffset ts2 = ts1.AddMinutes(1); + + Assert.True(object.ReferenceEquals(activity, activity.AddEvent(new ActivityEvent("Event1", ts1)))); + Assert.True(object.ReferenceEquals(activity, activity.AddEvent(new ActivityEvent("Event2", ts2)))); + + Assert.Equal(2, activity.Events.Count()); + Assert.Equal("Event2", activity.Events.ElementAt(0).Name); + Assert.Equal(ts2, activity.Events.ElementAt(0).Timestamp); + Assert.Equal(0, activity.Events.ElementAt(0).Attributes.Keys.Count()); + + Assert.Equal("Event1", activity.Events.ElementAt(1).Name); + Assert.Equal(ts1, activity.Events.ElementAt(1).Timestamp); + Assert.Equal(0, activity.Events.ElementAt(1).Attributes.Keys.Count()); + } + + [Fact] + public void TestIsAllDataRequested() + { + // Activity constructor allways set IsAllDataRequested to true for compatability. + Activity a1 = new Activity("a1"); + Assert.True(a1.IsAllDataRequested); + Assert.True(object.ReferenceEquals(a1, a1.AddTag("k1", "v1"))); + Assert.Equal(1, a1.Tags.Count()); + + a1.IsAllDataRequested = false; + Assert.True(object.ReferenceEquals(a1, a1.AddTag("k2", "v2"))); + Assert.Equal(1, a1.Tags.Count()); + } + public void Dispose() { Activity.Current = null; 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 09d3a0d5d7f1b9..3d34832a280f1e 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 @@ -9,6 +9,7 @@ + From c6b71a1de6a0f8db88109cd0d00d4ecc5273ef79 Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Fri, 3 Apr 2020 20:49:32 -0700 Subject: [PATCH 2/2] Update Per Feedback --- ...em.Diagnostics.DiagnosticSourceActivity.cs | 15 +- .../src/System/Diagnostics/Activity.cs | 98 ++----- .../src/System/Diagnostics/ActivityContext.cs | 24 +- .../src/System/Diagnostics/ActivityLink.cs | 37 ++- .../src/System/Diagnostics/ActivitySource.cs | 11 +- .../tests/ActivitySourceTests.cs | 246 +++++++++++++----- .../tests/ActivityTests.cs | 29 --- 7 files changed, 256 insertions(+), 204 deletions(-) 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 54f18d83d77250..2714cdfdd2696c 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs @@ -54,7 +54,6 @@ public string? Id public string? TraceStateString { get { throw null; } set { } } public System.Diagnostics.Activity AddBaggage(string key, string? value) { throw null; } public System.Diagnostics.Activity AddEvent(System.Diagnostics.ActivityEvent e) { throw null; } - public System.Diagnostics.Activity AddLink(System.Diagnostics.ActivityLink link) { throw null; } public System.Diagnostics.Activity AddTag(string key, string? value) { throw null; } public string? GetBaggageItem(string key) { throw null; } public System.Diagnostics.Activity SetEndTime(System.DateTime endTimeUtc) { throw null; } @@ -97,10 +96,10 @@ public void CopyTo(System.Span destination) { } } public sealed class ActivitySource : IDisposable { - public ActivitySource(string name, System.Version version) { throw null; } + public ActivitySource(string name, string version = "") { throw null; } public string Name { get { throw null; } } - public System.Version Version { get { throw null; } } - public bool HasListeners { get { throw null; } } + public string Version { get { throw null; } } + public bool HasListeners() { throw null; } public System.Diagnostics.Activity? StartActivity(string name, System.Diagnostics.ActivityKind kind = ActivityKind.Internal) { throw null; } public System.Diagnostics.Activity? StartActivity(string name, System.Diagnostics.ActivityKind kind, System.Diagnostics.ActivityContext parentContext, System.Collections.Generic.IEnumerable>? tags = null, System.Collections.Generic.IEnumerable? links = null, System.DateTimeOffset startTime = default) { throw null; } public System.Diagnostics.Activity? StartActivity(string name, System.Diagnostics.ActivityKind kind, string parentId, System.Collections.Generic.IEnumerable>? tags = null, System.Collections.Generic.IEnumerable? links = null, System.DateTimeOffset startTime = default) { throw null; } @@ -189,12 +188,18 @@ public sealed class ActivityEvent public override bool Equals(object? obj) { throw null; } public override int GetHashCode() { throw null; } } - public readonly struct ActivityLink + public readonly struct ActivityLink : IEquatable { public ActivityLink(System.Diagnostics.ActivityContext context) { throw null; } public ActivityLink(System.Diagnostics.ActivityContext context, System.Collections.Generic.IReadOnlyDictionary? attributes) { throw null; } public System.Diagnostics.ActivityContext Context { get; } public System.Collections.Generic.IReadOnlyDictionary? Attributes { get; } + + public override bool Equals(object? obj) { throw null; } + public bool Equals(System.Diagnostics.ActivityLink link) { throw null; } + public static bool operator ==(System.Diagnostics.ActivityLink link1, System.Diagnostics.ActivityLink link2) { throw null; } + public static bool operator !=(System.Diagnostics.ActivityLink link1, System.Diagnostics.ActivityLink link2) { throw null; } + public override int GetHashCode() { throw null; } } } 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 6a376e0e6579e2..f9238b7bab173b 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs @@ -37,7 +37,7 @@ public partial class Activity : IDisposable private static readonly IEnumerable s_emptyLinks = new ActivityLink[0]; private static readonly IEnumerable s_emptyEvents = new ActivityEvent[0]; #pragma warning restore CA1825 - private static ActivitySource s_defaultSource = new ActivitySource(string.Empty, new Version()); + private static ActivitySource s_defaultSource = new ActivitySource(string.Empty); private const byte ActivityTraceFlagsIsSet = 0b_1_0000000; // Internal flag to indicate if flags have been set private const int RequestIdMaxLength = 1024; @@ -95,10 +95,8 @@ public partial class Activity : IDisposable public string OperationName { get; } = null!; /// - /// An operation name is a COARSEST name that is useful grouping/filtering. - /// The name is typically a compile-time constant. Names of Rest APIs are - /// reasonable, but arguments (e.g. specific accounts etc), should not be in - /// the name but rather in the tags. + /// DisplayName is name mainly intended to be used in the UI and not necessary has to be + /// same as OperationName. /// public string DisplayName { @@ -108,8 +106,8 @@ public string DisplayName /// /// Get the ActivitySource object associated with this Activity. - /// All Activities created from constructors will have a singelton source. - /// Otherwise, the source will be holding the object created the Activity through ActivitySource.StartActivity. + /// All Activities created from public constructors will have a singleton source where the source name is empty string. + /// Otherwise, the source will be holding the object that created the Activity through ActivitySource.StartActivity. /// public ActivitySource Source { get; private set; } @@ -257,7 +255,7 @@ public string? RootId { do { - yield return tags!.keyValue; + yield return tags!.Value; tags = tags.Next; } while (tags != null); @@ -266,8 +264,8 @@ public string? RootId } /// - /// Events is the list of all objects which attached to this Activity object. - /// If there is no any object attached to the Activity object, Events will return empty list. + /// Events is the list of all objects attached to this Activity object. + /// If there is not any object attached to the Activity object, Events will return empty list. /// public IEnumerable Events { @@ -280,7 +278,7 @@ static IEnumerable Iterate(LinkedListNode? events) { do { - yield return events!.keyValue; + yield return events!.Value; events = events.Next; } while (events != null); @@ -289,7 +287,7 @@ static IEnumerable Iterate(LinkedListNode? events) } /// - /// Links is the list of all objects which contain the attached to this Activity object. + /// Links is the list of all objects attached to this Activity object. /// If there is no any object attached to the Activity object, Links will return empty list. /// public IEnumerable Links @@ -303,7 +301,7 @@ static IEnumerable Iterate(LinkedListNode? links) { do { - yield return links!.keyValue; + yield return links!.Value; links = links.Next; } while (links != null); @@ -340,7 +338,7 @@ static IEnumerable Iterate(LinkedListNode? links) { for (LinkedListNode>? baggage = activity._baggage; baggage != null; baggage = baggage.Next) { - yield return baggage.keyValue; + yield return baggage.Value; } activity = activity.Parent; @@ -392,11 +390,6 @@ public Activity(string operationName) /// 'this' for convenient chaining public Activity AddTag(string key, string? value) { - if (!IsAllDataRequested) - { - return this; - } - LinkedListNode>? currentTags = _tags; LinkedListNode> newTags = new LinkedListNode>(new KeyValuePair(key, value)); do @@ -415,7 +408,7 @@ public Activity AddTag(string key, string? value) /// 'this' for convenient chaining public Activity AddEvent(ActivityEvent e) { - if (!IsAllDataRequested || e == null) + if (e == null) { return this; } @@ -431,29 +424,6 @@ public Activity AddEvent(ActivityEvent e) return this; } - /// - /// Add object to the list. - /// - /// object of to add to the attached links list. - /// 'this' for convenient chaining - public Activity AddLink(ActivityLink link) - { - if (!IsAllDataRequested) - { - return this; - } - - LinkedListNode? currentLinks = _links; - LinkedListNode newLinks = new LinkedListNode(link); - do - { - newLinks.Next = currentLinks; - currentLinks = Interlocked.CompareExchange(ref _links, newLinks, currentLinks); - } while (!ReferenceEquals(newLinks.Next, currentLinks)); - - return this; - } - /// /// Update the Activity to have baggage with an additional 'key' and value 'value'. /// This shows up in the enumeration as well as the @@ -465,11 +435,6 @@ public Activity AddLink(ActivityLink link) /// 'this' for convenient chaining public Activity AddBaggage(string key, string? value) { - if (!IsAllDataRequested) - { - return this; - } - LinkedListNode>? currentBaggage = _baggage; LinkedListNode> newBaggage = new LinkedListNode>(new KeyValuePair(key, value)); @@ -513,7 +478,7 @@ public Activity SetParentId(string parentId) } /// - /// Set the parent ID using the W3C convention using a TraceId and a SpanId. This + /// Set the parent ID using the W3C convention using a TraceId and a SpanId. This /// constructor has the advantage that no string manipulation is needed to set the ID. /// public Activity SetParentId(ActivityTraceId traceId, ActivitySpanId spanId, ActivityTraceFlags activityTraceFlags = ActivityTraceFlags.None) @@ -579,18 +544,7 @@ public Activity SetEndTime(DateTime endTimeUtc) /// Get the context of the activity. Context becomes valid only if the activity has been started. /// otherwise will default context. /// - public ActivityContext Context - { - get - { - if (_id == null && _spanId == null) - { - return default; - } - - return new ActivityContext(new ActivityTraceId(_traceId), new ActivitySpanId(_spanId ?? _id), ActivityTraceFlags, _traceState); - } - } + public ActivityContext Context => new ActivityContext(TraceId, SpanId, ActivityTraceFlags, TraceStateString); /// /// Starts activity @@ -902,19 +856,19 @@ public void Dispose() /// The object to attach and map to the property name. public void SetCustomProperty(string propertyName, object? propertyValue) { - if (!IsAllDataRequested) - { - return; - } - - // We don't check null name here as the dictionary is performing this check anyway. - if (_customProperties == null) { Interlocked.CompareExchange(ref _customProperties, new ConcurrentDictionary(), null); } - _customProperties[propertyName] = propertyValue!; + if (propertyValue == null) + { + _customProperties.TryRemove(propertyName, out object _); + } + else + { + _customProperties[propertyName] = propertyValue!; + } } /// @@ -1239,13 +1193,13 @@ public ActivityIdFormat IdFormat /// private partial class LinkedListNode { - public LinkedListNode(T value) => keyValue = value; + public LinkedListNode(T value) => Value = value; public LinkedListNode(T value, LinkedListNode? next) { - keyValue = value; + Value = value; Next = next; } - public T keyValue; + public T Value; public LinkedListNode? Next; } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityContext.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityContext.cs index ca04080cdbc8ed..779b8789bed40e 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityContext.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityContext.cs @@ -22,7 +22,7 @@ namespace System.Diagnostics public ActivityContext(ActivityTraceId traceId, ActivitySpanId spanId, ActivityTraceFlags traceFlags, string? traceState = null) { // We don't allow creating context with invalid span or trace Ids. - if (spanId.ToHexString() == "0000000000000000" || traceId.ToHexString() == "00000000000000000000000000000000") + if (traceId == default || spanId == default) { throw new ArgumentException(SR.SpanIdOrTraceIdInvalid, traceId == default ? nameof(traceId) : nameof(spanId)); } @@ -53,28 +53,12 @@ public ActivityContext(ActivityTraceId traceId, ActivitySpanId spanId, ActivityT /// public string? TraceState { get; } - public static bool operator ==(ActivityContext context1, ActivityContext context2) - { - return context1.SpanId == context2.SpanId && - context1.TraceId == context2.TraceId && - context1.TraceFlags == context2.TraceFlags && - context1.TraceState == context2.TraceState; - } + public bool Equals(ActivityContext context) => SpanId.Equals(context.SpanId) && TraceId.Equals(context.TraceId) && TraceFlags == context.TraceFlags && TraceState == context.TraceState; + public override bool Equals(object? obj) => (obj is ActivityContext context) ? Equals(context) : false; + public static bool operator ==(ActivityContext context1, ActivityContext context2) => context1.Equals(context2); public static bool operator !=(ActivityContext context1, ActivityContext context2) => !(context1 == context2); - public bool Equals(ActivityContext context) - { - return this == context; - } - - public override bool Equals(object? obj) - { - if (obj is ActivityContext context) - return this == context; - return false; - } - public override int GetHashCode() { if (this == default) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.cs index c441fdfe2b3a1f..b07c3952ea155a 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.cs @@ -12,16 +12,16 @@ namespace System.Diagnostics /// Links can be used to represent batched operations where a Activity was initiated by multiple initiating Activities, /// each representing a single incoming item being processed in the batch. /// - public readonly struct ActivityLink + public readonly struct ActivityLink : IEquatable { /// - /// Construct a new object which can be linked to an Activity object through method. + /// Construct a new object which can be linked to an Activity object. /// /// The trace Activity context public ActivityLink(ActivityContext context) : this(context, null) {} /// - /// Construct a new object which can be linked to an Activity object through method. + /// Construct a new object which can be linked to an Activity object. /// /// The trace Activity context /// The key-value pair list of attributes which associated to the @@ -40,5 +40,36 @@ public ActivityLink(ActivityContext context, IReadOnlyDictionary /// Retrieve the key-value pair list of attributes attached with the . /// public IReadOnlyDictionary? Attributes { get; } + + public override bool Equals(object? obj) => (obj is ActivityLink link) && this.Equals(link); + + public bool Equals(ActivityLink link) => Context == link.Context && link.Attributes == Attributes; + public static bool operator ==(ActivityLink link1, ActivityLink link2) => link1.Equals(link2); + public static bool operator !=(ActivityLink link1, ActivityLink link2) => !link1.Equals(link2); + + public override int GetHashCode() + { + if (this == default) + return 0; + + // HashCode.Combine would be the best but we need to compile for the full framework which require adding dependency + // on the extensions package. Considering this simple type and hashing is not expected to be used, we are implementing + // the hashing manually. + int hash = 5381; + hash = ((hash << 5) + hash) + this.Context.GetHashCode(); + if (Attributes != null) + { + foreach (KeyValuePair kvp in Attributes) + { + hash = ((hash << 5) + hash) + kvp.Key.GetHashCode(); + if (kvp.Value != null) + { + hash = ((hash << 5) + hash) + kvp.Value.GetHashCode(); + } + } + } + + return hash; + } } } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivitySource.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivitySource.cs index a2fd09c4920d7a..fc7b2f5eb303ea 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivitySource.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivitySource.cs @@ -20,7 +20,7 @@ public sealed class ActivitySource : IDisposable /// /// The name of the ActivitySource object /// The version of the component publishing the tracing info. - public ActivitySource(string name, Version version) + public ActivitySource(string name, string version = "") { if (name == null) { @@ -51,7 +51,7 @@ public ActivitySource(string name, Version version) /// /// Returns the ActivitySource version. /// - public Version Version { get; } + public string Version { get; } /// /// Check if there is any listeners for this ActivitySource. @@ -59,7 +59,7 @@ public ActivitySource(string name, Version version) /// and avoid creating the objects needed to create Activity (e.g. ActivityContext) /// Example of that is http scenario which can avoid reading the context data from the wire. /// - public bool HasListeners => _listeners != null && _listeners.Count > 0; + public bool HasListeners() => _listeners != null && _listeners.Count > 0; /// /// Creates a new object if there is any listener to the Activity, returns null otherwise. @@ -138,7 +138,7 @@ public ActivitySource(string name, Version version) if (dateRequest != ActivityDataRequest.None) { - activity = Activity.CreateAndStart(this, name, kind, parentId, default, tags, links, startTime, dateRequest); + activity = Activity.CreateAndStart(this, name, kind, parentId, context, tags, links, startTime, dateRequest); listeners.EnumWithAction(listener => listener.OnActivityStarted(activity)); } @@ -195,11 +195,12 @@ internal void AddListener(ActivityListener listener) internal static void DetachListener(ActivityListener listener) { + s_allListeners.Remove(listener); + s_activeSources.EnumWithAction(source => { var listeners = source._listeners; listeners?.Remove(listener); }); - s_allListeners.Remove(listener); } internal void NotifyActivityStart(Activity activity) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivitySourceTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivitySourceTests.cs index 61989996fa3ea0..df12d88d373bdc 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivitySourceTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivitySourceTests.cs @@ -20,16 +20,16 @@ public class ActivitySourceTests : IDisposable public void TestConstruction() { RemoteExecutor.Invoke(() => { - using (ActivitySource as1 = new ActivitySource("Source1", new Version())) + using (ActivitySource as1 = new ActivitySource("Source1")) { Assert.Equal("Source1", as1.Name); - Assert.Equal(new Version(), as1.Version); - Assert.False(as1.HasListeners); - using (ActivitySource as2 = new ActivitySource("Source2", new Version(1, 1, 1, 2))) + Assert.Equal(String.Empty, as1.Version); + Assert.False(as1.HasListeners()); + using (ActivitySource as2 = new ActivitySource("Source2", "1.1.1.2")) { Assert.Equal("Source2", as2.Name); - Assert.Equal(new Version(1, 1, 1, 2), as2.Version); - Assert.False(as2.HasListeners); + Assert.Equal("1.1.1.2", as2.Version); + Assert.False(as2.HasListeners()); } } }).Dispose(); @@ -39,11 +39,11 @@ public void TestConstruction() public void TestStartActivityWithNoListener() { RemoteExecutor.Invoke(() => { - using (ActivitySource aSource = new ActivitySource("SourceActivity", new Version())) + using (ActivitySource aSource = new ActivitySource("SourceActivity")) { Assert.Equal("SourceActivity", aSource.Name); - Assert.Equal(new Version(), aSource.Version); - Assert.False(aSource.HasListeners); + Assert.Equal(string.Empty, aSource.Version); + Assert.False(aSource.HasListeners()); Activity current = Activity.Current; using (Activity a1 = aSource.StartActivity("a1")) @@ -60,9 +60,9 @@ public void TestStartActivityWithNoListener() public void TestActivityWithListenerNoActivityCreate() { RemoteExecutor.Invoke(() => { - using (ActivitySource aSource = new ActivitySource("SourceActivityListener", new Version())) + using (ActivitySource aSource = new ActivitySource("SourceActivityListener")) { - Assert.False(aSource.HasListeners); + Assert.False(aSource.HasListeners()); using (IDisposable listener1 = ActivitySource.AddActivityListener( (activitySource) => object.ReferenceEquals(aSource, activitySource), @@ -70,7 +70,7 @@ public void TestActivityWithListenerNoActivityCreate() (source, name, kind, parentId, tags, links) => ActivityDataRequest.None, activity => Assert.NotNull(activity), activity => Assert.NotNull(activity))) { - Assert.True(aSource.HasListeners); + Assert.True(aSource.HasListeners()); // The listener is not allowing to create a new Activity. Assert.Null(aSource.StartActivity("nullActivity")); @@ -79,62 +79,14 @@ public void TestActivityWithListenerNoActivityCreate() }).Dispose(); } - [Fact] - public void TestActivityWithListenerActivityCreateNoDataAllowed() - { - RemoteExecutor.Invoke(() => { - using (ActivitySource aSource = new ActivitySource("SourceActivityListener", new Version())) - { - int counter = 0; - Assert.False(aSource.HasListeners); - - using (IDisposable listener1 = ActivitySource.AddActivityListener( - (activitySource) => object.ReferenceEquals(aSource, activitySource), - (source, name, kind, parentId, tags, links) => ActivityDataRequest.PropagationData, - (source, name, kind, context, tags, links) => ActivityDataRequest.PropagationData, - activity => counter++, activity => counter--)) - { - Assert.True(aSource.HasListeners); - Activity current = Activity.Current; - // The listener allow creating activity but not allowing adding any data to it. - Activity activity = aSource.StartActivity("NotAllDataRequestedActivity"); - Assert.NotNull(activity); - Assert.False(activity.IsAllDataRequested); - Assert.Equal(1, counter); - Assert.Equal(activity, Activity.Current); - Assert.Equal(current, activity.Parent); - - Assert.Equal(0, activity.Tags.Count()); - Assert.Equal(0, activity.Baggage.Count()); - Assert.Equal(0, activity.Links.Count()); - Assert.Equal(0, activity.Events.Count()); - - Assert.True(object.ReferenceEquals(activity, activity.AddTag("key", "value"))); - Assert.True(object.ReferenceEquals(activity, activity.AddBaggage("key", "value"))); - Assert.True(object.ReferenceEquals(activity, activity.AddLink(new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, null))))); - Assert.True(object.ReferenceEquals(activity, activity.AddEvent(new ActivityEvent("e1")))); - - Assert.Equal(0, activity.Tags.Count()); - Assert.Equal(0, activity.Baggage.Count()); - Assert.Equal(0, activity.Links.Count()); - Assert.Equal(0, activity.Events.Count()); - - activity.Dispose(); - Assert.Equal(0, counter); - Assert.Equal(current, Activity.Current); - } - } - }).Dispose(); - } - [Fact] public void TestActivityWithListenerActivityCreateAndAllDataRequested() { RemoteExecutor.Invoke(() => { - using (ActivitySource aSource = new ActivitySource("SourceActivityListener", new Version())) + using (ActivitySource aSource = new ActivitySource("SourceActivityListener")) { int counter = 0; - Assert.False(aSource.HasListeners); + Assert.False(aSource.HasListeners()); using (IDisposable listener1 = ActivitySource.AddActivityListener( (activitySource) => object.ReferenceEquals(aSource, activitySource), @@ -142,13 +94,12 @@ public void TestActivityWithListenerActivityCreateAndAllDataRequested() (source, name, kind, parentId, tags, links) => ActivityDataRequest.AllDataAndRecorded, activity => counter++, activity => counter--)) { - Assert.True(aSource.HasListeners); + Assert.True(aSource.HasListeners()); - // The listener allow creating activity but not allowing adding any data to it. using (Activity activity = aSource.StartActivity("AllDataRequestedActivity")) { Assert.NotNull(activity); - // Assert.True(activity.IsAllDataRequested); + Assert.True(activity.IsAllDataRequested); Assert.Equal(1, counter); Assert.Equal(0, activity.Tags.Count()); @@ -168,9 +119,7 @@ public void TestActivityWithListenerActivityCreateAndAllDataRequested() Assert.Equal(0, activity1.Links.Count()); Assert.Equal(0, activity1.Events.Count()); - Assert.True(object.ReferenceEquals(activity1, activity1.AddLink(new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, null))))); Assert.True(object.ReferenceEquals(activity1, activity1.AddEvent(new ActivityEvent("e1")))); - Assert.Equal(1, activity1.Links.Count()); Assert.Equal(1, activity1.Events.Count()); } Assert.Equal(1, counter); @@ -188,10 +137,10 @@ public void TestActivitySourceAttachedObject() RemoteExecutor.Invoke(() => { // All Activities created through the constructor should have same source. Assert.True(object.ReferenceEquals(new Activity("a1").Source, new Activity("a2").Source)); - Assert.True(string.IsNullOrEmpty(new Activity("a3").Source.Name)); - Assert.Equal(new Version(), new Activity("a4").Source.Version); + Assert.Equal("", new Activity("a3").Source.Name); + Assert.Equal(string.Empty, new Activity("a4").Source.Version); - using (ActivitySource aSource = new ActivitySource("SourceToTest", new Version(1, 2, 3, 4))) + using (ActivitySource aSource = new ActivitySource("SourceToTest", "1.2.3.4")) { //Ensure at least we have a listener to allow Activity creation using (IDisposable listener1 = ActivitySource.AddActivityListener( @@ -209,6 +158,163 @@ public void TestActivitySourceAttachedObject() }).Dispose(); } + [Fact] + public void TestListeningToConstructedActivityEvents() + { + RemoteExecutor.Invoke(() => { + int activityStartCount = 0; + int activityStopCount = 0; + + using (IDisposable listener = ActivitySource.AddActivityListener( + (activitySource) => activitySource.Name == "" && activitySource.Version == "", + (source, name, kind, context, tags, links) => ActivityDataRequest.AllData, + (source, name, kind, parentId, tags, links) => ActivityDataRequest.AllData, + activity => activityStartCount++, activity => activityStopCount++)) + { + Assert.Equal(0, activityStartCount); + Assert.Equal(0, activityStopCount); + + using (Activity a1 = new Activity("a1")) + { + Assert.Equal(0, activityStartCount); + Assert.Equal(0, activityStopCount); + + a1.Start(); + + Assert.Equal(1, activityStartCount); + Assert.Equal(0, activityStopCount); + } + + Assert.Equal(1, activityStartCount); + Assert.Equal(1, activityStopCount); + } + + // Ensure the listener is disposed + using (Activity a2 = new Activity("a2")) + { + a2.Start(); + + Assert.Equal(1, activityStartCount); + Assert.Equal(1, activityStopCount); + } + }).Dispose(); + } + + [Fact] + public void TestExpectedListenersReturnValues() + { + RemoteExecutor.Invoke(() => { + + ActivitySource source = new ActivitySource("MultipleListenerSource"); + IDisposable [] listeners = new IDisposable[4]; + + listeners[0] = ActivitySource.AddActivityListener( + (activitySource) => true, + (source, name, kind, context, tags, links) => ActivityDataRequest.None, + (source, name, kind, parentId, tags, links) => ActivityDataRequest.None, + activity => Assert.NotNull(activity), activity => Assert.NotNull(activity)); + + Assert.Null(source.StartActivity("a1")); + + listeners[1] = ActivitySource.AddActivityListener( + (activitySource) => true, + (source, name, kind, context, tags, links) => ActivityDataRequest.PropagationData, + (source, name, kind, parentId, tags, links) => ActivityDataRequest.PropagationData, + activity => Assert.NotNull(activity), activity => Assert.NotNull(activity)); + + using (Activity a2 = source.StartActivity("a2")) + { + Assert.False(a2.IsAllDataRequested); + Assert.True((a2.ActivityTraceFlags & ActivityTraceFlags.Recorded) == 0); + } + + listeners[2] = ActivitySource.AddActivityListener( + (activitySource) => true, + (source, name, kind, context, tags, links) => ActivityDataRequest.AllData, + (source, name, kind, parentId, tags, links) => ActivityDataRequest.AllData, + activity => Assert.NotNull(activity), activity => Assert.NotNull(activity)); + + using (Activity a3 = source.StartActivity("a3")) + { + Assert.True(a3.IsAllDataRequested); + Assert.True((a3.ActivityTraceFlags & ActivityTraceFlags.Recorded) == 0); + } + + listeners[3] = ActivitySource.AddActivityListener( + (activitySource) => true, + (source, name, kind, context, tags, links) => ActivityDataRequest.AllDataAndRecorded, + (source, name, kind, parentId, tags, links) => ActivityDataRequest.AllDataAndRecorded, + activity => Assert.NotNull(activity), activity => Assert.NotNull(activity)); + + using (Activity a4 = source.StartActivity("a4")) + { + Assert.True(a4.IsAllDataRequested); + Assert.True((a4.ActivityTraceFlags & ActivityTraceFlags.Recorded) != 0, $"a4.ActivityTraceFlags failed: {a4.ActivityTraceFlags}"); + } + + foreach (IDisposable listener in listeners) + { + listener.Dispose(); + } + + Assert.Null(source.StartActivity("a5")); + }).Dispose(); + } + + [Fact] + public void TestActivityCreationProperties() + { + RemoteExecutor.Invoke(() => { + ActivitySource source = new ActivitySource("MultipleListenerSource"); + + using (IDisposable listener = ActivitySource.AddActivityListener( + (activitySource) => true, + (source, name, kind, context, tags, links) => ActivityDataRequest.AllData, + (source, name, kind, parentId, tags, links) => ActivityDataRequest.AllData, + activity => Assert.NotNull(activity), activity => Assert.NotNull(activity))) + { + ActivityContext ctx = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, "key0-value0"); + + List links = new List(); + links.Add(new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, "key1-value1"))); + links.Add(new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None, "key2-value2"))); + + List> attributes = new List>(); + attributes.Add(new KeyValuePair("tag1", "tagValue1")); + attributes.Add(new KeyValuePair("tag2", "tagValue2")); + attributes.Add(new KeyValuePair("tag3", "tagValue3")); + + using (Activity activity = source.StartActivity("a1", ActivityKind.Client, ctx, attributes, links)) + { + Assert.NotNull(activity); + Assert.Equal("a1", activity.OperationName); + Assert.Equal("a1", activity.DisplayName); + Assert.Equal(ActivityKind.Client, activity.Kind); + + Assert.Equal(ctx.TraceId, activity.TraceId); + Assert.Equal(ctx.SpanId, activity.ParentSpanId); + Assert.Equal(ctx.TraceFlags, activity.ActivityTraceFlags); + Assert.Equal(ctx.TraceState, activity.TraceStateString); + Assert.Equal(ActivityIdFormat.W3C, activity.IdFormat); + + foreach (KeyValuePair pair in attributes) + { + Assert.NotEqual(default, activity.Tags.FirstOrDefault((p) => pair.Key == p.Key && pair.Value == pair.Value)); + } + + foreach (ActivityLink link in links) + { + Assert.NotEqual(default, activity.Links.FirstOrDefault((l) => link == l)); + } + } + + using (Activity activity = source.StartActivity("a2", ActivityKind.Client, "NoW3CParentId", attributes, links)) + { + Assert.Equal(ActivityIdFormat.Hierarchical, activity.IdFormat); + } + } + }).Dispose(); + } public void Dispose() => Activity.Current = null; } } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs index 6c94a9d7062f7a..e664fbb00aed3f 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs @@ -1379,31 +1379,6 @@ public void TestCustomProperties() } - [Fact] - public void TestLinks() - { - Activity activity = new Activity("Links"); - IEnumerable links = activity.Links; - Assert.Equal(0, links.Count()); - - activity.AddLink(new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.None))); - activity.AddLink(new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded))); - - links = activity.Links; - Assert.Equal(2, links.Count()); - - activity.AddLink(new ActivityLink(new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), ActivityTraceFlags.Recorded, "I=Cool"))); - links = activity.Links; - Assert.Equal(3, links.Count()); - - Assert.Equal(ActivityTraceFlags.None, links.ElementAt(2).Context.TraceFlags); - Assert.Null(links.ElementAt(2).Context.TraceState); - Assert.Equal(ActivityTraceFlags.Recorded, links.ElementAt(1).Context.TraceFlags); - Assert.Null(links.ElementAt(1).Context.TraceState); - Assert.Equal(ActivityTraceFlags.Recorded, links.ElementAt(0).Context.TraceFlags); - Assert.NotNull(links.ElementAt(0).Context.TraceState); - } - [Fact] public void TestKind() { @@ -1453,10 +1428,6 @@ public void TestIsAllDataRequested() Assert.True(a1.IsAllDataRequested); Assert.True(object.ReferenceEquals(a1, a1.AddTag("k1", "v1"))); Assert.Equal(1, a1.Tags.Count()); - - a1.IsAllDataRequested = false; - Assert.True(object.ReferenceEquals(a1, a1.AddTag("k2", "v2"))); - Assert.Equal(1, a1.Tags.Count()); } public void Dispose()