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..2714cdfdd2696c 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,12 @@ 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 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 +63,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 +94,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, string version = "") { throw null; } + public string Name { 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; } + 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 +150,56 @@ 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 : 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/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..f9238b7bab173b 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); 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,23 @@ public partial class Activity /// public string OperationName { get; } = null!; + /// + /// DisplayName is name mainly intended to be used in the UI and not necessary has to be + /// same as OperationName. + /// + public string DisplayName + { + get => _displayName ?? OperationName; + set => _displayName = value; + } + + /// + /// Get the ActivitySource object associated with this Activity. + /// 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; } + /// /// 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,16 +246,16 @@ 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 { - yield return tags!.keyValue; + yield return tags!.Value; tags = tags.Next; } while (tags != null); @@ -233,6 +263,52 @@ public string? RootId } } + /// + /// 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 + { + get + { + LinkedListNode? events = _events; + return events != null ? Iterate(events) : s_emptyEvents; + + static IEnumerable Iterate(LinkedListNode? events) + { + do + { + yield return events!.Value; + events = events.Next; + } + while (events != null); + } + } + } + + /// + /// 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 + { + get + { + LinkedListNode? links = _links; + return links != null ? Iterate(links) : s_emptyLinks; + + static IEnumerable Iterate(LinkedListNode? links) + { + do + { + yield return links!.Value; + 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,9 +336,9 @@ 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; + yield return baggage.Value; } activity = activity.Parent; @@ -293,6 +369,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 +390,8 @@ 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) }; + LinkedListNode>? currentTags = _tags; + LinkedListNode> newTags = new LinkedListNode>(new KeyValuePair(key, value)); do { newTags.Next = currentTags; @@ -321,6 +401,29 @@ 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 (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; + } + /// /// 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 +435,8 @@ 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) }; + LinkedListNode>? currentBaggage = _baggage; + LinkedListNode> newBaggage = new LinkedListNode>(new KeyValuePair(key, value)); do { @@ -375,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) @@ -437,6 +540,12 @@ 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 => new ActivityContext(TraceId, SpanId, ActivityTraceFlags, TraceStateString); + /// /// Starts activity /// @@ -464,7 +573,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 +601,8 @@ public Activity Start() _id = GenerateHierarchicalId(); SetCurrent(this); + + Source.NotifyActivityStart(this); } return this; } @@ -521,6 +632,8 @@ public void Stop() } SetCurrent(Parent); + + Source.NotifyActivityStop(this); } } @@ -604,6 +717,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 +837,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 (_customProperties == null) + { + Interlocked.CompareExchange(ref _customProperties, new ConcurrentDictionary(), null); + } + + if (propertyValue == null) + { + _customProperties.TryRemove(propertyName, out object _); + } + else + { + _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 +1191,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) => Value = value; + public LinkedListNode(T value, LinkedListNode? next) + { + Value = value; + Next = next; + } + public T Value; + 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..779b8789bed40e --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityContext.cs @@ -0,0 +1,79 @@ +// 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 (traceId == default || spanId == default) + { + 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 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 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..b07c3952ea155a --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.cs @@ -0,0 +1,75 @@ +// 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 : IEquatable + { + /// + /// 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. + /// + /// 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; } + + 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/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..fc7b2f5eb303ea --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivitySource.cs @@ -0,0 +1,336 @@ +// 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, string 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 string 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, context, 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_allListeners.Remove(listener); + + s_activeSources.EnumWithAction(source => { + var listeners = source._listeners; + listeners?.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..df12d88d373bdc --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivitySourceTests.cs @@ -0,0 +1,320 @@ +// 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")) + { + Assert.Equal("Source1", as1.Name); + 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("1.1.1.2", as2.Version); + Assert.False(as2.HasListeners()); + } + } + }).Dispose(); + } + + [Fact] + public void TestStartActivityWithNoListener() + { + RemoteExecutor.Invoke(() => { + using (ActivitySource aSource = new ActivitySource("SourceActivity")) + { + Assert.Equal("SourceActivity", aSource.Name); + Assert.Equal(string.Empty, 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")) + { + 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 TestActivityWithListenerActivityCreateAndAllDataRequested() + { + RemoteExecutor.Invoke(() => { + using (ActivitySource aSource = new ActivitySource("SourceActivityListener")) + { + 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()); + + 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.AddEvent(new ActivityEvent("e1")))); + 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.Equal("", new Activity("a3").Source.Name); + Assert.Equal(string.Empty, new Activity("a4").Source.Version); + + 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( + (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(); + } + + [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 153990a6ee49b0..e664fbb00aed3f 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,88 @@ 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 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()); + } + 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 @@ +