From b89566689612a36e0c44e55597677dd230c4f98a Mon Sep 17 00:00:00 2001 From: Tarek Mahmoud Sayed Date: Tue, 14 Jul 2020 22:06:57 -0700 Subject: [PATCH] Improve Activity API usability and OpenTelemetry integration (Part 2) (#39087) --- ...em.Diagnostics.DiagnosticSourceActivity.cs | 68 +++-- .../src/Resources/Strings.resx | 3 + ...System.Diagnostics.DiagnosticSource.csproj | 1 + .../src/System/Diagnostics/Activity.cs | 195 +++++++++++- .../src/System/Diagnostics/ActivityContext.cs | 21 +- .../Diagnostics/ActivityCreationOptions.cs | 4 +- .../src/System/Diagnostics/ActivityEvent.cs | 34 +-- .../src/System/Diagnostics/ActivityLink.cs | 18 +- .../Diagnostics/ActivityLink.netcoreapp.cs | 4 +- .../System/Diagnostics/ActivityLink.netfx.cs | 4 +- .../System/Diagnostics/ActivityListener.cs | 5 + .../src/System/Diagnostics/ActivitySource.cs | 18 +- .../Diagnostics/ActivityTagsCollection.cs | 278 ++++++++++++++++++ .../tests/ActivitySourceTests.cs | 108 ++++++- .../tests/ActivityTagsCollectionTests.cs | 200 +++++++++++++ .../tests/ActivityTests.cs | 84 +++++- ....Diagnostics.DiagnosticSource.Tests.csproj | 7 +- 17 files changed, 960 insertions(+), 92 deletions(-) create mode 100644 src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityTagsCollection.cs create mode 100644 src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTagsCollectionTests.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 2d308fd3ba8db..f0b93c82b4075 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs @@ -33,7 +33,7 @@ public string? Id get { throw null; } } - public bool IsAllDataRequested { get { throw null; } set { 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 { throw null; } } public string OperationName { get { throw null; } } @@ -47,6 +47,7 @@ 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> TagObjects { 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; } } @@ -54,6 +55,8 @@ public string? Id 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 System.Diagnostics.Activity AddTag(string key, object value) { throw null; } + public System.Diagnostics.Activity SetTag(string key, object value) { throw null; } public string? GetBaggageItem(string key) { throw null; } public System.Diagnostics.Activity SetEndTime(System.DateTime endTimeUtc) { throw null; } public System.Diagnostics.Activity SetIdFormat(System.Diagnostics.ActivityIdFormat format) { throw null; } @@ -68,6 +71,37 @@ public string? Id public object? GetCustomProperty(string propertyName) { throw null; } public ActivityContext Context { get { throw null; } } } + public class ActivityTagsCollection : System.Collections.Generic.IDictionary + { + public ActivityTagsCollection() { throw null; } + public ActivityTagsCollection(System.Collections.Generic.IEnumerable> list) { throw null; } + public object? this[string key] { get { throw null; } set { } } + public System.Collections.Generic.ICollection Keys { get { throw null; } } + public System.Collections.Generic.ICollection Values { get { throw null; } } + public int Count { get { throw null; } } + public bool IsReadOnly { get { throw null; } } + public void Add(string key, object value) { throw null; } + public void Add(System.Collections.Generic.KeyValuePair item) { throw null; } + public void Clear() { throw null; } + public bool Contains(System.Collections.Generic.KeyValuePair item) { throw null; } + public bool ContainsKey(string key) { throw null; } + public void CopyTo(System.Collections.Generic.KeyValuePair[] array, int arrayIndex) { throw null; } + System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + public bool Remove(string key) { throw null; } + public bool Remove(System.Collections.Generic.KeyValuePair item) { throw null; } + public bool TryGetValue(string key, out object value) { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public Enumerator GetEnumerator() { throw null; } + + public struct Enumerator : System.Collections.Generic.IEnumerator>, System.Collections.IEnumerator + { + public System.Collections.Generic.KeyValuePair Current { get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + public void Dispose() { throw null; } + public bool MoveNext() { throw null; } + void System.Collections.IEnumerator.Reset() { throw null; } + } + } public enum ActivityIdFormat { Unknown = 0, @@ -101,8 +135,8 @@ public sealed class ActivitySource : IDisposable 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 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 static void AddActivityListener(System.Diagnostics.ActivityListener listener) { throw null; } public void Dispose() { throw null; } } @@ -163,20 +197,19 @@ public enum ActivityKind public readonly struct 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.IEnumerable>? attributes) { throw null; } - public ActivityEvent(string name, System.Collections.Generic.IEnumerable>? attributes) { throw null; } + public ActivityEvent(string name, System.DateTimeOffset timestamp = default, System.Diagnostics.ActivityTagsCollection? tags = null) { throw null; } public string Name { get { throw null; } } public System.DateTimeOffset Timestamp { get { throw null; } } - public System.Collections.Generic.IEnumerable> Attributes { get { throw null; } } + public System.Collections.Generic.IEnumerable> Tags { 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 ActivityContext(System.Diagnostics.ActivityTraceId traceId, System.Diagnostics.ActivitySpanId spanId, System.Diagnostics.ActivityTraceFlags traceOptions, string? traceState = null, bool isRemote = false) { throw null; } public System.Diagnostics.ActivityTraceId TraceId { get { throw null; } } public System.Diagnostics.ActivitySpanId SpanId { get { throw null; } } public System.Diagnostics.ActivityTraceFlags TraceFlags { get { throw null; } } public string? TraceState { get { throw null; } } + public bool IsRemote { get { throw null; } } public static bool operator ==(System.Diagnostics.ActivityContext left, System.Diagnostics.ActivityContext right) { throw null; } public static bool operator !=(System.Diagnostics.ActivityContext left, System.Diagnostics.ActivityContext right) { throw null; } public bool Equals(System.Diagnostics.ActivityContext value) { throw null; } @@ -185,11 +218,9 @@ public readonly struct ActivityEvent } public readonly struct ActivityLink : IEquatable { - public ActivityLink(System.Diagnostics.ActivityContext context) { throw null; } - public ActivityLink(System.Diagnostics.ActivityContext context, System.Collections.Generic.IEnumerable>? attributes) { throw null; } + public ActivityLink(System.Diagnostics.ActivityContext context, System.Diagnostics.ActivityTagsCollection? tags = null) { throw null; } public System.Diagnostics.ActivityContext Context { get { throw null; } } - public System.Collections.Generic.IEnumerable>? Attributes { get { throw null; } } - + public System.Collections.Generic.IEnumerable>? Tags { get { throw null; } } public override bool Equals(object? obj) { throw null; } public bool Equals(System.Diagnostics.ActivityLink value) { throw null; } public static bool operator ==(System.Diagnostics.ActivityLink left, System.Diagnostics.ActivityLink right) { throw null; } @@ -202,18 +233,19 @@ public readonly struct ActivityCreationOptions public string Name { get { throw null; } } public System.Diagnostics.ActivityKind Kind { get { throw null; } } public T Parent { get { throw null; } } - public System.Collections.Generic.IEnumerable> Tags { get { throw null; } } + public System.Collections.Generic.IEnumerable> Tags { get { throw null; } } public System.Collections.Generic.IEnumerable Links { get { throw null; } } } public delegate System.Diagnostics.ActivityDataRequest GetRequestedData(ref System.Diagnostics.ActivityCreationOptions options); public sealed class ActivityListener : IDisposable { public ActivityListener() { throw null; } - public System.Action? ActivityStarted { get { throw null; } set { } } - public System.Action? ActivityStopped { get { throw null; } set { } } - public System.Func? ShouldListenTo { get { throw null; } set { } } - public System.Diagnostics.GetRequestedData? GetRequestedDataUsingParentId { get { throw null; } set { } } - public System.Diagnostics.GetRequestedData? GetRequestedDataUsingContext { get { throw null; } set { } } + public System.Action? ActivityStarted { get { throw null; } set { throw null; } } + public System.Action? ActivityStopped { get { throw null; } set { throw null; } } + public System.Func? ShouldListenTo { get { throw null; } set { throw null; } } + public System.Diagnostics.GetRequestedData? GetRequestedDataUsingParentId { get { throw null; } set { throw null; } } + public System.Diagnostics.GetRequestedData? GetRequestedDataUsingContext { get { throw null; } set { throw null; } } + public bool AutoGenerateRootContextTraceId { get { throw null; } set { throw null; } } public void Dispose() { 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 1d9bff34062b3..52df21c79f1ee 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx @@ -153,4 +153,7 @@ "StartTime is not UTC" + + "The collection already contains item with same key '{0}''" + \ No newline at end of file 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 f18ad5915f4a5..0745b28f5a954 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj @@ -35,6 +35,7 @@ + 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 5382c925c2bb6..1701db6d5562b 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs @@ -73,7 +73,7 @@ public partial class Activity : IDisposable private byte _w3CIdFlags; - private LinkedList>? _tags; + private TagsLinkedList? _tags; private LinkedList>? _baggage; private LinkedList? _links; private LinkedList? _events; @@ -243,7 +243,19 @@ public string? RootId /// public IEnumerable> Tags { - get => _tags != null ? _tags.Enumerate() : s_emptyBaggageTags; + get => _tags?.EnumerateStringValues() ?? s_emptyBaggageTags; + } + + /// + /// List of the tags which represent information that will be logged along with the Activity to the logging system. + /// This information however is NOT passed on to the children of this activity. + /// + public IEnumerable> TagObjects + { +#if ALLOW_PARTIALLY_TRUSTED_CALLERS + [System.Security.SecuritySafeCriticalAttribute] +#endif + get => _tags?.Enumerate() ?? Unsafe.As>>(s_emptyBaggageTags); } /// @@ -340,15 +352,27 @@ public Activity(string operationName) /// /// Update the Activity to have a tag with an additional 'key' and value 'value'. - /// This shows up in the enumeration. It is meant for information that + /// This shows up in the enumeration. It is meant for information that /// is useful to log but not needed for runtime control (for the latter, ) /// /// 'this' for convenient chaining - public Activity AddTag(string key, string? value) + /// The tag key name + /// The tag value mapped to the input key + public Activity AddTag(string key, string? value) => AddTag(key, (object?) value); + + /// + /// Update the Activity to have a tag with an additional 'key' and value 'value'. + /// This shows up in the enumeration. It is meant for information that + /// is useful to log but not needed for runtime control (for the latter, ) + /// + /// 'this' for convenient chaining + /// The tag key name + /// The tag value mapped to the input key + public Activity AddTag(string key, object? value) { - KeyValuePair kvp = new KeyValuePair(key, value); + KeyValuePair kvp = new KeyValuePair(key, value); - if (_tags != null || Interlocked.CompareExchange(ref _tags, new LinkedList>(kvp), null) != null) + if (_tags != null || Interlocked.CompareExchange(ref _tags, new TagsLinkedList(kvp), null) != null) { _tags.Add(kvp); } @@ -356,6 +380,30 @@ public Activity AddTag(string key, string? value) return this; } + /// + /// Add or update the Activity tag with the input key and value. + /// If the input value is null + /// - if the collection has any tag with the same key, then this tag will get removed from the collection. + /// - otherwise, nothing will happen and the collection will not change. + /// If the input value is not null + /// - if the collection has any tag with the same key, then the value mapped to this key will get updated with the new input value. + /// - otherwise, the key and value will get added as a new tag to the collection. + /// + /// The tag key name + /// The tag value mapped to the input key + /// 'this' for convenient chaining + public Activity SetTag(string key, object value) + { + KeyValuePair kvp = new KeyValuePair(key, value); + + if (_tags != null || Interlocked.CompareExchange(ref _tags, new TagsLinkedList(kvp, set: true), null) != null) + { + _tags.Set(kvp); + } + + return this; + } + /// /// Add object to the list. /// @@ -872,7 +920,7 @@ public void SetCustomProperty(string propertyName, object? propertyValue) } internal static Activity CreateAndStart(ActivitySource source, string name, ActivityKind kind, string? parentId, ActivityContext parentContext, - IEnumerable>? tags, IEnumerable? links, + IEnumerable>? tags, IEnumerable? links, DateTimeOffset startTime, ActivityDataRequest request) { Activity activity = new Activity(name); @@ -929,11 +977,11 @@ internal static Activity CreateAndStart(ActivitySource source, string name, Acti if (tags != null) { - using (IEnumerator> enumerator = tags.GetEnumerator()) + using (IEnumerator> enumerator = tags.GetEnumerator()) { if (enumerator.MoveNext()) { - activity._tags = new LinkedList>(enumerator); + activity._tags = new TagsLinkedList(enumerator); } } } @@ -1227,6 +1275,135 @@ public IEnumerable Enumerate() } } + private class TagsLinkedList + { + private LinkedListNode>? _first; + private LinkedListNode>? _last; + + public TagsLinkedList(KeyValuePair firstValue, bool set = false) => _last = _first = ((set && firstValue.Value == null) ? null : new LinkedListNode>(firstValue)); + + public TagsLinkedList(IEnumerator> e) + { + _last = _first = new LinkedListNode>(e.Current); + + while (e.MoveNext()) + { + _last.Next = new LinkedListNode>(e.Current); + _last = _last.Next; + } + } + + public void Add(KeyValuePair value) + { + LinkedListNode> newNode = new LinkedListNode>(value); + + lock (this) + { + if (_first == null) + { + _first = _last = newNode; + return; + } + + Debug.Assert(_last != null); + + _last!.Next = newNode; + _last = newNode; + } + } + + public void Remove(string key) + { + if (_first == null) + { + return; + } + + lock (this) + { + if (_first.Value.Key == key) + { + _first = _first.Next; + return; + } + + LinkedListNode> previous = _first; + + while (previous.Next != null) + { + if (previous.Next.Value.Key == key) + { + previous.Next = previous.Next.Next; + return; + } + previous = previous.Next; + } + } + } + + public void Set(KeyValuePair value) + { + if (value.Value == null) + { + Remove(value.Key); + return; + } + + lock (this) + { + LinkedListNode>? current = _first; + while (current != null) + { + if (current.Value.Key == value.Key) + { + current.Value = value; + return; + } + + current = current.Next; + } + + LinkedListNode> newNode = new LinkedListNode>(value); + if (_first == null) + { + _first = _last = newNode; + return; + } + + Debug.Assert(_last != null); + + _last!.Next = newNode; + _last = newNode; + } + } + + public IEnumerable> EnumerateStringValues() + { + LinkedListNode>? current = _first; + + while (current != null) + { + if (current.Value.Value is string || current.Value.Value == null) + { + yield return new KeyValuePair(current.Value.Key, (string?) current.Value.Value); + } + + current = current.Next; + }; + } + + public IEnumerable> Enumerate() + { + LinkedListNode>? current = _first; + + while (current != null) + { + yield return current.Value; + current = current.Next; + }; + } + } + [Flags] private enum State : byte { 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 86d43ae25e2ca..454eba3ab0c60 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityContext.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityContext.cs @@ -14,16 +14,21 @@ namespace System.Diagnostics /// /// Construct a new object of ActivityContext. /// - /// A trace identifier. - /// A span identifier + /// 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) + /// Carries system-specific configuration data. + /// Indicate the context is propagated from remote parent. + /// + /// isRemote is not a part of W3C specification. It is needed for the OpenTelemetry scenarios. + /// + public ActivityContext(ActivityTraceId traceId, ActivitySpanId spanId, ActivityTraceFlags traceFlags, string? traceState = null, bool isRemote = false) { TraceId = traceId; SpanId = spanId; TraceFlags = traceFlags; TraceState = traceState; + IsRemote = isRemote; } /// @@ -46,6 +51,14 @@ public ActivityContext(ActivityTraceId traceId, ActivitySpanId spanId, ActivityT /// public string? TraceState { get; } + /// + /// IsRemote indicates if the ActivityContext was propagated from a remote parent. + /// + /// + /// IsRemote is not a part of W3C specification. It is needed for the OpenTelemetry scenarios. + /// + public bool IsRemote { get; } + public bool Equals(ActivityContext value) => SpanId.Equals(value.SpanId) && TraceId.Equals(value.TraceId) && TraceFlags == value.TraceFlags && TraceState == value.TraceState; public override bool Equals(object? obj) => (obj is ActivityContext context) ? Equals(context) : false; diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityCreationOptions.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityCreationOptions.cs index b77de1dda79a6..5d484594e455a 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityCreationOptions.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityCreationOptions.cs @@ -19,7 +19,7 @@ public readonly struct ActivityCreationOptions /// to create the Activity object with. /// Key-value pairs list for the tags to create the Activity object with. /// list to create the Activity object with. - internal ActivityCreationOptions(ActivitySource source, string name, T parent, ActivityKind kind, IEnumerable>? tags, IEnumerable? links) + internal ActivityCreationOptions(ActivitySource source, string name, T parent, ActivityKind kind, IEnumerable>? tags, IEnumerable? links) { Source = source; Name = name; @@ -52,7 +52,7 @@ internal ActivityCreationOptions(ActivitySource source, string name, T parent, A /// /// Retrieve the tags which requested to create the Activity object with. /// - public IEnumerable>? Tags { get; } + public IEnumerable>? Tags { get; } /// /// Retrieve the list of which requested to create the Activity object with. diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityEvent.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityEvent.cs index c8abd399be709..1b1e201089f9f 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityEvent.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityEvent.cs @@ -7,17 +7,17 @@ namespace System.Diagnostics { /// - /// A text annotation associated with a collection of attributes. + /// A text annotation associated with a collection of tags. /// public readonly struct ActivityEvent { - private static readonly IEnumerable> s_emptyAttributes = new Dictionary(); + private static readonly ActivityTagsCollection s_emptyTags = new ActivityTagsCollection(); /// /// Initializes a new instance of the class. /// /// Event name. - public ActivityEvent(string name) : this(name, DateTimeOffset.UtcNow, s_emptyAttributes) + public ActivityEvent(string name) : this(name, DateTimeOffset.UtcNow, s_emptyTags) { } @@ -26,29 +26,11 @@ public ActivityEvent(string name) : this(name, DateTimeOffset.UtcNow, s_emptyAtt /// /// 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, IEnumerable>? attributes) : this(name, default, attributes) - { - } - - /// - /// 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, IEnumerable>? attributes) + /// Event Tags. + public ActivityEvent(string name, DateTimeOffset timestamp = default, ActivityTagsCollection? tags = null) { Name = name ?? string.Empty; - Attributes = attributes ?? s_emptyAttributes; + Tags = tags ?? s_emptyTags; Timestamp = timestamp != default ? timestamp : DateTimeOffset.UtcNow; } @@ -63,8 +45,8 @@ public ActivityEvent(string name, DateTimeOffset timestamp, IEnumerable - /// Gets the collection of attributes associated with the event. + /// Gets the collection of tags associated with the event. /// - public IEnumerable> Attributes { get; } + public IEnumerable> Tags { get; } } } 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 d129b521bef10..d34c688ff1923 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.cs @@ -17,17 +17,11 @@ namespace System.Diagnostics /// 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, IEnumerable>? attributes) + /// The key-value pair list of tags which associated to the + public ActivityLink(ActivityContext context, ActivityTagsCollection? tags = null) { Context = context; - Attributes = attributes; + Tags = tags; } /// @@ -36,13 +30,13 @@ public ActivityLink(ActivityContext context, IEnumerable - /// Retrieve the key-value pair list of attributes attached with the . + /// Retrieve the key-value pair list of tags attached with the . /// - public IEnumerable>? Attributes { get; } + public IEnumerable>? Tags { get; } public override bool Equals(object? obj) => (obj is ActivityLink link) && this.Equals(link); - public bool Equals(ActivityLink value) => Context == value.Context && value.Attributes == Attributes; + public bool Equals(ActivityLink value) => Context == value.Context && value.Tags == Tags; public static bool operator ==(ActivityLink left, ActivityLink right) => left.Equals(right); public static bool operator !=(ActivityLink left, ActivityLink right) => !left.Equals(right); } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.netcoreapp.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.netcoreapp.cs index c2c9d57a1c103..e370bdd2f1513 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.netcoreapp.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.netcoreapp.cs @@ -17,9 +17,9 @@ public override int GetHashCode() { HashCode hashCode = default; hashCode.Add(Context); - if (Attributes != null) + if (Tags != null) { - foreach (KeyValuePair kvp in Attributes) + foreach (KeyValuePair kvp in Tags) { hashCode.Add(kvp.Key); hashCode.Add(kvp.Value); diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.netfx.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.netfx.cs index 0f23122b49e9a..93501800ed9d8 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.netfx.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityLink.netfx.cs @@ -23,9 +23,9 @@ public override int GetHashCode() // the hashing manually. int hash = 5381; hash = ((hash << 5) + hash) + this.Context.GetHashCode(); - if (Attributes != null) + if (Tags != null) { - foreach (KeyValuePair kvp in Attributes) + foreach (KeyValuePair kvp in Tags) { hash = ((hash << 5) + hash) + kvp.Key.GetHashCode(); if (kvp.Value != null) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityListener.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityListener.cs index 804e416d60048..c7d290d5ba049 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityListener.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityListener.cs @@ -47,6 +47,11 @@ public ActivityListener() /// public GetRequestedData? GetRequestedDataUsingContext { get; set; } + /// + /// Determine if the listener automatically generates a new trace Id before sampling when there is no parent context. + /// + public bool AutoGenerateRootContextTraceId { get; set;} + /// /// Dispose will unregister this object from listeneing to events. /// 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 9e282ec28516e..a89068f6c11bb 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivitySource.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivitySource.cs @@ -87,7 +87,7 @@ public bool HasListeners() /// 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) + 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); /// @@ -100,10 +100,10 @@ public bool HasListeners() /// 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) + 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) + 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; @@ -169,12 +169,18 @@ public bool HasListeners() } else { - ActivityContext initializedContext = context == default && Activity.Current != null ? Activity.Current.Context : context; - var aco = new ActivityCreationOptions(this, name, initializedContext, kind, tags, links); + ActivityContext initializedContext = context == default && Activity.Current != null ? Activity.Current.Context : context; + optionsWithContext = new ActivityCreationOptions(this, name, initializedContext, kind, tags, links); listeners.EnumWithFunc((ActivityListener listener, ref ActivityCreationOptions data, ref ActivityDataRequest request, ref bool? canUseContext, ref ActivityCreationOptions dataWithContext) => { GetRequestedData? getRequestedDataUsingContext = listener.GetRequestedDataUsingContext; if (getRequestedDataUsingContext != null) { + if (listener.AutoGenerateRootContextTraceId && !canUseContext.HasValue && data.Parent == default) + { + ActivityContext ctx = new ActivityContext(ActivityTraceId.CreateRandom(), default, default); + dataWithContext = new ActivityCreationOptions(data.Source, data.Name, ctx, data.Kind, data.Tags, data.Links); + canUseContext = true; + } ActivityDataRequest dr = getRequestedDataUsingContext(ref data); if (dr > request) { @@ -185,7 +191,7 @@ public bool HasListeners() return request != ActivityDataRequest.AllDataAndRecorded; } return true; - }, ref aco, ref dataRequest, ref useContext, ref optionsWithContext); + }, ref optionsWithContext, ref dataRequest, ref useContext, ref optionsWithContext); } if (dataRequest != ActivityDataRequest.None) diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityTagsCollection.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityTagsCollection.cs new file mode 100644 index 0000000000000..74bdc49e6cd93 --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/ActivityTagsCollection.cs @@ -0,0 +1,278 @@ +// 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.Collections; +using System.Diagnostics.CodeAnalysis; + +namespace System.Diagnostics +{ + /// + /// ActivityTagsCollection is a collection class used to store tracing tags. + /// This collection will be used with classes like and . + /// This collection behaves as follows: + /// - The collection items will be ordered according to how they are added. + /// - Don't allow duplication of items with the same key. + /// - When using the indexer to store an item in the collection: + /// - If the item has a key that previously existed in the collection and the value is null, the collection item matching the key will be removed from the collection. + /// - If the item has a key that previously existed in the collection and the value is not null, the new item value will replace the old value stored in the collection. + /// - Otherwise, the item will be added to the collection. + /// - Add method will add a new item to the collection if an item doesn't already exist with the same key. Otherwise, it will throw an exception. + /// + public class ActivityTagsCollection : IDictionary + { + private List> _list = new List>(); + + /// + /// Create a new instance of the collection. + /// + public ActivityTagsCollection() + { + } + + /// + /// Create a new instance of the collection and store the input list items in the collection. + /// + /// Initial list to store in the collection. + public ActivityTagsCollection(IEnumerable> list) + { + if (list == null) + { + throw new ArgumentNullException(nameof(list)); + } + + foreach (KeyValuePair kvp in list) + { + if (kvp.Key != null) + { + this[kvp.Key] = kvp.Value; + } + } + } + + /// + /// Get or set collection item + /// When setting a value to this indexer property, the following behavior will be observed: + /// - If the key previously existed in the collection and the value is null, the collection item matching the key will get removed from the collection. + /// - If the key previously existed in the collection and the value is not null, the value will replace the old value stored in the collection. + /// - Otherwise, a new item will get added to the collection. + /// + /// Object mapped to the key + public object this[string key] + { + get + { + int index = _list.FindIndex(kvp => kvp.Key == key); + return index < 0 ? null! : _list[index].Value; + } + + set + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + int index = _list.FindIndex(kvp => kvp.Key == key); + if (value == null) + { + if (index >= 0) + { + _list.RemoveAt(index); + } + return; + } + + if (index >= 0) + { + _list[index] = new KeyValuePair(key, value); + } + else + { + _list.Add(new KeyValuePair(key, value)); + } + } + } + + /// + /// Get the list of the keys of all stored tags. + /// + public ICollection Keys + { + get + { + List list = new List(_list.Count); + foreach (KeyValuePair kvp in _list) + { + list.Add(kvp.Key); + } + return list; + } + } + + /// + /// Get the list of the values of all stored tags. + /// + public ICollection Values + { + get + { + List list = new List(_list.Count); + foreach (KeyValuePair kvp in _list) + { + list.Add(kvp.Value); + } + return list; + } + } + + /// + /// Gets a value indicating whether the collection is read-only. + /// + public bool IsReadOnly => false; + + /// + /// Gets the number of elements contained in the collection. + /// + public int Count => _list.Count; + + /// + /// Adds a tag with the provided key and value to the collection. + /// This collection doesn't allow adding two tags with the same key. + /// + /// The tag key. + /// The tag value. + public void Add(string key, object value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + int index = _list.FindIndex(kvp => kvp.Key == key); + if (index >= 0) + { + throw new InvalidOperationException(SR.Format(SR.KeyAlreadyExist, key)); + } + + _list.Add(new KeyValuePair(key, value)); + } + + /// + /// Adds an item to the collection + /// + /// Key and value pair of the tag to add to the collection. + public void Add(KeyValuePair item) + { + if (item.Key == null) + { + throw new ArgumentNullException(nameof(item)); + } + + int index = _list.FindIndex(kvp => kvp.Key == item.Key); + if (index >= 0) + { + throw new InvalidOperationException(SR.Format(SR.KeyAlreadyExist, item.Key)); + } + + _list.Add(item); + } + + /// + /// Removes all items from the collection. + /// + public void Clear() => _list.Clear(); + + public bool Contains(KeyValuePair item) => _list.Contains(item); + + /// + /// Determines whether the collection contains an element with the specified key. + /// + /// + /// True if the collection contains tag with that key. False otherwise. + public bool ContainsKey(string key) => _list.FindIndex(kvp => kvp.Key == key) >= 0; + + /// + /// Copies the elements of the collection to an array, starting at a particular array index. + /// + /// The array that is the destination of the elements copied from collection. + /// The zero-based index in array at which copying begins. + public void CopyTo(KeyValuePair[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + + /// + /// Returns an enumerator that iterates through the collection. + /// + IEnumerator> IEnumerable>.GetEnumerator() => new Enumerator(_list); + + /// + /// Returns an enumerator that iterates through the collection. + /// + public Enumerator GetEnumerator() => new Enumerator(_list); + + /// + /// Returns an enumerator that iterates through the collection. + /// + IEnumerator IEnumerable.GetEnumerator() => new Enumerator(_list); + + /// + /// Removes the tag with the specified key from the collection. + /// + /// The tag key + /// True if the item existed and removed. False otherwise. + public bool Remove(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + int index = _list.FindIndex(kvp => kvp.Key == key); + if (index >= 0) + { + _list.RemoveAt(index); + return true; + } + + return false; + } + + /// + /// Removes the first occurrence of a specific item from the collection. + /// + /// The tag key value pair to remove. + /// True if item was successfully removed from the collection; otherwise, false. This method also returns false if item is not found in the original collection. + public bool Remove(KeyValuePair item) => _list.Remove(item); + + /// + /// Gets the value associated with the specified key. + /// + /// The tag key. + /// The tag value. + /// When this method returns, the value associated with the specified key, if the key is found; otherwise, the default value for the type of the value parameter. This parameter is passed uninitialized. + public bool TryGetValue(string key, [MaybeNullWhen(false)] out object value) + { + int index = _list.FindIndex(kvp => kvp.Key == key); + if (index >= 0) + { + value = _list[index].Value; + return true; + } + + value = null; + return false; + } + + public struct Enumerator : IEnumerator>, IEnumerator + { + private List>.Enumerator _enumerator; + internal Enumerator(List> list) => _enumerator = list.GetEnumerator(); + + public KeyValuePair Current => _enumerator.Current; + object IEnumerator.Current => ((IEnumerator)_enumerator).Current; + public void Dispose() => _enumerator.Dispose(); + public bool MoveNext() => _enumerator.MoveNext(); + void IEnumerator.Reset() => ((IEnumerator)_enumerator).Reset(); + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivitySourceTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivitySourceTests.cs index fdd3365d78d78..dd86a36b3341a 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivitySourceTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivitySourceTests.cs @@ -318,10 +318,10 @@ public void TestActivityCreationProperties() 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")); + 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)) { @@ -336,7 +336,7 @@ public void TestActivityCreationProperties() Assert.Equal(ctx.TraceState, activity.TraceStateString); Assert.Equal(ActivityIdFormat.W3C, activity.IdFormat); - foreach (KeyValuePair pair in attributes) + foreach (KeyValuePair pair in attributes) { Assert.NotEqual(default, activity.Tags.FirstOrDefault((p) => pair.Key == p.Key && pair.Value == pair.Value)); } @@ -434,6 +434,104 @@ public void TestCreatingActivityUsingDifferentParentIds() }).Dispose(); } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TestActivityContextIsRemote() + { + RemoteExecutor.Invoke(() => { + ActivityContext ctx = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), default); + Assert.False(ctx.IsRemote); + + bool isRemote = false; + + using ActivitySource aSource = new ActivitySource("RemoteContext"); + using ActivityListener listener = new ActivityListener(); + listener.ShouldListenTo = (activitySource) => activitySource.Name == "RemoteContext"; + + listener.GetRequestedDataUsingContext = (ref ActivityCreationOptions activityOptions) => + { + isRemote = activityOptions.Parent.IsRemote; + return ActivityDataRequest.AllData; + }; + + ActivitySource.AddActivityListener(listener); + + foreach (bool b in new bool[] { true, false }) + { + ctx = new ActivityContext(ActivityTraceId.CreateRandom(), ActivitySpanId.CreateRandom(), default, default, b); + Assert.Equal(b, ctx.IsRemote); + + aSource.StartActivity("a1", default, ctx); + Assert.Equal(b , isRemote); + } + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TestTraceIdAutoGeneration() + { + RemoteExecutor.Invoke(() => { + + using ActivitySource aSource = new ActivitySource("TraceIdAutoGeneration"); + using ActivityListener listener = new ActivityListener(); + listener.ShouldListenTo = (activitySource) => activitySource.Name == "TraceIdAutoGeneration"; + + ActivityContext ctx = default; + + listener.GetRequestedDataUsingContext = (ref ActivityCreationOptions activityOptions) => + { + ctx = activityOptions.Parent; + return ActivityDataRequest.AllData; + }; + + ActivitySource.AddActivityListener(listener); + + using (aSource.StartActivity("a1", default, ctx)) + { + Assert.Equal(default, ctx); + } + + listener.AutoGenerateRootContextTraceId = true; + + Activity activity = aSource.StartActivity("a2", default, ctx); + + Assert.NotNull(activity); + Assert.NotEqual(default, ctx); + Assert.Equal(ctx.TraceId, activity.TraceId); + Assert.Equal(ctx.SpanId.ToHexString(), activity.ParentSpanId.ToHexString()); + Assert.Equal(default(ActivitySpanId).ToHexString(), ctx.SpanId.ToHexString()); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TestTraceIdAutoGenerationWithNullParentId() + { + RemoteExecutor.Invoke(() => { + + using ActivitySource aSource = new ActivitySource("TraceIdAutoGenerationWithNullParent"); + using ActivityListener listener = new ActivityListener(); + listener.ShouldListenTo = (activitySource) => activitySource.Name == "TraceIdAutoGenerationWithNullParent"; + + ActivityContext ctx = default; + + listener.GetRequestedDataUsingContext = (ref ActivityCreationOptions activityOptions) => + { + ctx = activityOptions.Parent; + return ActivityDataRequest.AllData; + }; + + listener.AutoGenerateRootContextTraceId = true; + ActivitySource.AddActivityListener(listener); + + Activity activity = aSource.StartActivity("a2", default, null); + + Assert.NotNull(activity); + Assert.NotEqual(default, ctx); + Assert.Equal(ctx.TraceId, activity.TraceId); + Assert.Equal(ctx.SpanId.ToHexString(), activity.ParentSpanId.ToHexString()); + Assert.Equal(default(ActivitySpanId).ToHexString(), ctx.SpanId.ToHexString()); + }).Dispose(); + } + public void Dispose() => Activity.Current = null; } } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTagsCollectionTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTagsCollectionTests.cs new file mode 100644 index 0000000000000..eb27d13c1f4c4 --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTagsCollectionTests.cs @@ -0,0 +1,200 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +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 ActivityTagsCollectionTests : IDisposable + { + private readonly static KeyValuePair [] s_list = new KeyValuePair[] + { + new KeyValuePair("Key1", "Value1"), + new KeyValuePair("Key2", "Value2"), + new KeyValuePair("Key3", "Value3"), + new KeyValuePair("Key4", "Value4"), + + // Duplicate Keys to replace the old one + new KeyValuePair("Key3", 3), + new KeyValuePair("Key4", true), + + // null values is not allowed. + new KeyValuePair("Key5", null), + }; + + + [Fact] + public void TestDefaultConstructor() + { + ActivityTagsCollection tags = new ActivityTagsCollection(); + Assert.Equal(0, tags.Count); + Assert.Equal(0, tags.Keys.Count); + Assert.Equal(0, tags.Values.Count); + Assert.False(tags.Remove("")); + Assert.False(tags.Remove(new KeyValuePair("", null))); + Assert.False(tags.TryGetValue("", out object _)); + Assert.False(tags.Contains(new KeyValuePair("", null))); + Assert.False(tags.ContainsKey("")); + Assert.False(tags.IsReadOnly); + Assert.Null(tags[""]); + } + + [Fact] + public void TestNonDefaultConstructor() + { + ActivityTagsCollection tags = new ActivityTagsCollection(s_list); + + Assert.Equal(4, tags.Count); + Assert.Equal(4, tags.Keys.Count); + Assert.Equal(4, tags.Values.Count); + + Assert.Equal(3, tags["Key3"]); + Assert.True((bool) tags["Key4"]); + } + + [Fact] + public void TestAdd() + { + ActivityTagsCollection tags = new ActivityTagsCollection(); + tags.Add("k1", "v1"); + tags.Add("k2", 2); + tags.Add("k3", new List()); + + Assert.Equal(3, tags.Count); + Assert.Equal("v1", tags["k1"]); + Assert.Equal(2, tags["k2"]); + Assert.True(tags["k3"] is List); + Assert.Null(tags["k4"]); + + AssertExtensions.Throws(() => tags.Add(null, "v")); + AssertExtensions.Throws(() => tags.Add("k1", "v")); + } + + [Fact] + public void TestTryGetValue() + { + ActivityTagsCollection tags = new ActivityTagsCollection(); + tags.Add("k1", "v1"); + tags.Add("k2", 2); + + var list = new List(); + tags.Add("k3", list); + + Assert.True(tags.TryGetValue("k1", out object o)); + Assert.Equal("v1", o); + + Assert.True(tags.TryGetValue("k2", out o)); + Assert.Equal(2, o); + + Assert.True(tags.TryGetValue("k3", out o)); + Assert.Equal(list, tags["k3"]); + + Assert.False(tags.TryGetValue("k4", out o)); + } + + [Fact] + public void TestIndexer() + { + ActivityTagsCollection tags = new ActivityTagsCollection(); + Assert.Null(tags["k1"]); + Assert.Equal(0, tags.Count); + + tags["k1"] = "v1"; + Assert.Equal("v1", tags["k1"]); + Assert.Equal(1, tags.Count); + + tags["k1"] = "v2"; + Assert.Equal("v2", tags["k1"]); + Assert.Equal(1, tags.Count); + + tags["k1"] = null; + Assert.Null(tags["k1"]); + Assert.Equal(0, tags.Count); + + AssertExtensions.Throws(() => tags[null] = ""); + } + + [Fact] + public void TestContains() + { + ActivityTagsCollection tags = new ActivityTagsCollection(s_list); + Assert.True(tags.ContainsKey("Key1")); + Assert.True(tags.Contains(s_list[0])); + + Assert.True(tags.ContainsKey("Key2")); + Assert.True(tags.Contains(s_list[1])); + + Assert.True(tags.ContainsKey("Key3")); + Assert.False(tags.Contains(s_list[2])); + Assert.True(tags.Contains(s_list[4])); + + Assert.True(tags.ContainsKey("Key4")); + Assert.False(tags.Contains(s_list[3])); + Assert.True(tags.Contains(s_list[5])); + } + + [Fact] + public void TestRemove() + { + ActivityTagsCollection tags = new ActivityTagsCollection(s_list); + Assert.True(tags.ContainsKey("Key1")); + Assert.True(tags.Remove("Key1")); + Assert.False(tags.ContainsKey("Key1")); + + Assert.True(tags.ContainsKey("Key2")); + Assert.True(tags.Remove(new KeyValuePair("Key2", "Value2"))); + Assert.False(tags.ContainsKey("Key2")); + + Assert.True(tags.Contains(new KeyValuePair("Key3", 3))); + Assert.False(tags.Remove(new KeyValuePair("Key3", 4))); + Assert.True(tags.Remove(new KeyValuePair("Key3", 3))); + } + + [Fact] + public void TestEnumeration() + { + var list = new KeyValuePair[20]; + for (int i = 0; i < list.Length; i++) + { + list[i] = new KeyValuePair(i.ToString(), i); + } + + ActivityTagsCollection tags = new ActivityTagsCollection(list); + + int index = 0; + foreach (KeyValuePair kvp in tags) + { + Assert.Equal(new KeyValuePair(index.ToString(), index), kvp); + index++; + } + Assert.Equal(list.Length, index); + + index = 0; + foreach (string key in tags.Keys) + { + Assert.Equal(index.ToString(), key); + index++; + } + Assert.Equal(list.Length, index); + + index = 0; + foreach (object value in tags.Values) + { + Assert.Equal(index, value); + index++; + } + Assert.Equal(list.Length, index); + } + + + 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 8af17417a0b05..a085d3103bdbd 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/ActivityTests.cs @@ -1411,11 +1411,11 @@ public void TestEvent() Assert.Equal(2, activity.Events.Count()); Assert.Equal("Event1", activity.Events.ElementAt(0).Name); Assert.Equal(ts1, activity.Events.ElementAt(0).Timestamp); - Assert.Equal(0, activity.Events.ElementAt(0).Attributes.Count()); + Assert.Equal(0, activity.Events.ElementAt(0).Tags.Count()); Assert.Equal("Event2", activity.Events.ElementAt(1).Name); Assert.Equal(ts2, activity.Events.ElementAt(1).Timestamp); - Assert.Equal(0, activity.Events.ElementAt(1).Attributes.Count()); + Assert.Equal(0, activity.Events.ElementAt(1).Tags.Count()); } [Fact] @@ -1428,6 +1428,86 @@ public void TestIsAllDataRequested() Assert.Equal(1, a1.Tags.Count()); } + [Fact] + public void TestTagObjects() + { + Activity activity = new Activity("TagObjects"); + Assert.Equal(0, activity.Tags.Count()); + Assert.Equal(0, activity.TagObjects.Count()); + + activity.AddTag("s1", "s1").AddTag("s2", "s2").AddTag("s3", null); + Assert.Equal(3, activity.Tags.Count()); + Assert.Equal(3, activity.TagObjects.Count()); + + KeyValuePair[] tags = activity.Tags.ToArray(); + KeyValuePair[] tagObjects = activity.TagObjects.ToArray(); + Assert.Equal(tags.Length, tagObjects.Length); + + for (int i = 0; i < tagObjects.Length; i++) + { + Assert.Equal(tags[i].Key, tagObjects[i].Key); + Assert.Equal(tags[i].Value, tagObjects[i].Value); + } + + activity.AddTag("s4", (object) null); + Assert.Equal(4, activity.Tags.Count()); + Assert.Equal(4, activity.TagObjects.Count()); + tags = activity.Tags.ToArray(); + tagObjects = activity.TagObjects.ToArray(); + Assert.Equal(tags[3].Key, tagObjects[3].Key); + Assert.Equal(tags[3].Value, tagObjects[3].Value); + + activity.AddTag("s5", 5); + Assert.Equal(4, activity.Tags.Count()); + Assert.Equal(5, activity.TagObjects.Count()); + tagObjects = activity.TagObjects.ToArray(); + Assert.Equal(5, tagObjects[4].Value); + + activity.AddTag(null, null); // we allow that and we keeping the behavior for the compatability reason + Assert.Equal(5, activity.Tags.Count()); + Assert.Equal(6, activity.TagObjects.Count()); + + activity.SetTag("s6", "s6"); + Assert.Equal(6, activity.Tags.Count()); + Assert.Equal(7, activity.TagObjects.Count()); + + activity.SetTag("s5", "s6"); + Assert.Equal(7, activity.Tags.Count()); + Assert.Equal(7, activity.TagObjects.Count()); + + activity.SetTag("s3", null); // remove the tag + Assert.Equal(6, activity.Tags.Count()); + Assert.Equal(6, activity.TagObjects.Count()); + + tags = activity.Tags.ToArray(); + tagObjects = activity.TagObjects.ToArray(); + for (int i = 0; i < tagObjects.Length; i++) + { + Assert.Equal(tags[i].Key, tagObjects[i].Key); + Assert.Equal(tags[i].Value, tagObjects[i].Value); + } + } + + [Theory] + [InlineData("key1", null, true, 1)] + [InlineData("key2", null, false, 0)] + [InlineData("key3", "v1", true, 1)] + [InlineData("key4", "v2", false, 1)] + public void TestInsertingFirstTag(string key, object value, bool add, int resultCount) + { + Activity a = new Activity("SetFirstTag"); + if (add) + { + a.AddTag(key, value); + } + else + { + a.SetTag(key, value); + } + + Assert.Equal(resultCount, a.TagObjects.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 ed8d7de3079d1..d06639f76f9c6 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 @@ -10,13 +10,12 @@ + - - + + \ No newline at end of file