File: System\Diagnostics\ActivitySource.cs
Web Access
Project: src\src\libraries\System.Diagnostics.DiagnosticSource\src\System.Diagnostics.DiagnosticSource.csproj (System.Diagnostics.DiagnosticSource)
// 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.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
 
namespace System.Diagnostics
{
    public sealed class ActivitySource : IDisposable
    {
        private static readonly SynchronizedList<ActivitySource> s_activeSources = new SynchronizedList<ActivitySource>();
        private static readonly SynchronizedList<ActivityListener> s_allListeners = new SynchronizedList<ActivityListener>();
        private SynchronizedList<ActivityListener>? _listeners;
 
        /// <summary>
        /// Construct an ActivitySource object with the input name
        /// </summary>
        /// <param name="name">The name of the ActivitySource object</param>
        public ActivitySource(string name) : this(name, version: "", tags: null) {}
 
        /// <summary>
        /// Construct an ActivitySource object with the input name
        /// </summary>
        /// <param name="name">The name of the ActivitySource object</param>
        /// <param name="version">The version of the component publishing the tracing info.</param>
        [EditorBrowsable(EditorBrowsableState.Never)]
        public ActivitySource(string name, string? version = "") : this(name, version, tags: null) {}
 
        /// <summary>
        /// Construct an ActivitySource object with the input name
        /// </summary>
        /// <param name="name">The name of the ActivitySource object</param>
        /// <param name="version">The version of the component publishing the tracing info.</param>
        /// <param name="tags">The optional ActivitySource tags.</param>
        public ActivitySource(string name, string? version = "", IEnumerable<KeyValuePair<string, object?>>? tags = default)
        {
            Name = name ?? throw new ArgumentNullException(nameof(name));
            Version = version;
 
            // Sorting the tags to make sure the tags are always in the same order.
            // Sorting can help in comparing the tags used for any scenario.
            if (tags is not null)
            {
                var tagList = new List<KeyValuePair<string, object?>>(tags);
                tagList.Sort((left, right) => string.Compare(left.Key, right.Key, StringComparison.Ordinal));
                Tags = tagList.AsReadOnly();
            }
 
            s_activeSources.Add(this);
 
            if (s_allListeners.Count > 0)
            {
                s_allListeners.EnumWithAction((listener, source) =>
                {
                    Func<ActivitySource, bool>? shouldListenTo = listener.ShouldListenTo;
                    if (shouldListenTo != null)
                    {
                        var activitySource = (ActivitySource)source;
                        if (shouldListenTo(activitySource))
                        {
                            activitySource.AddListener(listener);
                        }
                    }
                }, this);
            }
 
            GC.KeepAlive(DiagnosticSourceEventSource.Log);
        }
 
        /// <summary>
        /// Returns the ActivitySource name.
        /// </summary>
        public string Name { get; }
 
        /// <summary>
        /// Returns the ActivitySource version.
        /// </summary>
        public string? Version { get; }
 
        /// <summary>
        /// Returns the tags associated with the ActivitySource.
        /// </summary>
        public IEnumerable<KeyValuePair<string, object?>>? Tags { get; }
 
        /// <summary>
        /// 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.
        /// </summary>
        public bool HasListeners()
        {
            SynchronizedList<ActivityListener>? listeners = _listeners;
            return listeners != null && listeners.Count > 0;
        }
 
        /// <summary>
        /// Creates a new <see cref="Activity"/> object if there is any listener to the Activity, returns null otherwise.
        /// </summary>
        /// <param name="name">The operation name of the Activity</param>
        /// <param name="kind">The <see cref="ActivityKind"/></param>
        /// <returns>The created <see cref="Activity"/> object or null if there is no any event listener.</returns>
        /// <remarks>
        /// If the Activity object is created, it will not start automatically. Callers need to call <see cref="Activity.Start()"/> to start it.
        /// </remarks>
        public Activity? CreateActivity(string name, ActivityKind kind)
            => CreateActivity(name, kind, default, null, null, null, default, startIt: false);
 
        /// <summary>
        /// Creates a new <see cref="Activity"/> object if there is any listener to the Activity, returns null otherwise.
        /// If the Activity object is created, it will not automatically start. Callers will need to call <see cref="Activity.Start()"/> to start it.
        /// </summary>
        /// <param name="name">The operation name of the Activity.</param>
        /// <param name="kind">The <see cref="ActivityKind"/></param>
        /// <param name="parentContext">The parent <see cref="ActivityContext"/> object to initialize the created Activity object with.</param>
        /// <param name="tags">The optional tags list to initialize the created Activity object with.</param>
        /// <param name="links">The optional <see cref="ActivityLink"/> list to initialize the created Activity object with.</param>
        /// <param name="idFormat">The default Id format to use.</param>
        /// <returns>The created <see cref="Activity"/> object or null if there is no any listener.</returns>
        /// <remarks>
        /// If the Activity object is created, it will not start automatically. Callers need to call <see cref="Activity.Start()"/> to start it.
        /// </remarks>
        public Activity? CreateActivity(string name, ActivityKind kind, ActivityContext parentContext, IEnumerable<KeyValuePair<string, object?>>? tags = null, IEnumerable<ActivityLink>? links = null, ActivityIdFormat idFormat = ActivityIdFormat.Unknown)
            => CreateActivity(name, kind, parentContext, null, tags, links, default, startIt: false, idFormat);
 
        /// <summary>
        /// Creates a new <see cref="Activity"/> object if there is any listener to the Activity, returns null otherwise.
        /// </summary>
        /// <param name="name">The operation name of the Activity.</param>
        /// <param name="kind">The <see cref="ActivityKind"/></param>
        /// <param name="parentId">The parent Id to initialize the created Activity object with.</param>
        /// <param name="tags">The optional tags list to initialize the created Activity object with.</param>
        /// <param name="links">The optional <see cref="ActivityLink"/> list to initialize the created Activity object with.</param>
        /// <param name="idFormat">The default Id format to use.</param>
        /// <returns>The created <see cref="Activity"/> object or null if there is no any listener.</returns>
        /// <remarks>
        /// If the Activity object is created, it will not start automatically. Callers need to call <see cref="Activity.Start()"/> to start it.
        /// </remarks>
        public Activity? CreateActivity(string name, ActivityKind kind, string? parentId, IEnumerable<KeyValuePair<string, object?>>? tags = null, IEnumerable<ActivityLink>? links = null, ActivityIdFormat idFormat = ActivityIdFormat.Unknown)
            => CreateActivity(name, kind, default, parentId, tags, links, default, startIt: false, idFormat);
 
        /// <summary>
        /// Creates and starts a new <see cref="Activity"/> object if there is any listener to the Activity, returns null otherwise.
        /// </summary>
        /// <param name="name">The operation name of the Activity</param>
        /// <param name="kind">The <see cref="ActivityKind"/></param>
        /// <returns>The created <see cref="Activity"/> object or null if there is no any event listener.</returns>
        public Activity? StartActivity([CallerMemberName] string name = "", ActivityKind kind = ActivityKind.Internal)
            => CreateActivity(name, kind, default, null, null, null, default);
 
        /// <summary>
        /// Creates and starts a new <see cref="Activity"/> object if there is any listener to the Activity events, returns null otherwise.
        /// </summary>
        /// <param name="name">The operation name of the Activity.</param>
        /// <param name="kind">The <see cref="ActivityKind"/></param>
        /// <param name="parentContext">The parent <see cref="ActivityContext"/> object to initialize the created Activity object with.</param>
        /// <param name="tags">The optional tags list to initialize the created Activity object with.</param>
        /// <param name="links">The optional <see cref="ActivityLink"/> list to initialize the created Activity object with.</param>
        /// <param name="startTime">The optional start timestamp to set on the created Activity object.</param>
        /// <returns>The created <see cref="Activity"/> object or null if there is no any listener.</returns>
        public Activity? StartActivity(string name, ActivityKind kind, ActivityContext parentContext, IEnumerable<KeyValuePair<string, object?>>? tags = null, IEnumerable<ActivityLink>? links = null, DateTimeOffset startTime = default)
            => CreateActivity(name, kind, parentContext, null, tags, links, startTime);
 
        /// <summary>
        /// Creates and starts a new <see cref="Activity"/> object if there is any listener to the Activity events, returns null otherwise.
        /// </summary>
        /// <param name="name">The operation name of the Activity.</param>
        /// <param name="kind">The <see cref="ActivityKind"/></param>
        /// <param name="parentId">The parent Id to initialize the created Activity object with.</param>
        /// <param name="tags">The optional tags list to initialize the created Activity object with.</param>
        /// <param name="links">The optional <see cref="ActivityLink"/> list to initialize the created Activity object with.</param>
        /// <param name="startTime">The optional start timestamp to set on the created Activity object.</param>
        /// <returns>The created <see cref="Activity"/> object or null if there is no any listener.</returns>
        public Activity? StartActivity(string name, ActivityKind kind, string? parentId, IEnumerable<KeyValuePair<string, object?>>? tags = null, IEnumerable<ActivityLink>? links = null, DateTimeOffset startTime = default)
            => CreateActivity(name, kind, default, parentId, tags, links, startTime);
 
        /// <summary>
        /// Creates and starts a new <see cref="Activity"/> object if there is any listener to the Activity events, returns null otherwise.
        /// </summary>
        /// <param name="kind">The <see cref="ActivityKind"/></param>
        /// <param name="parentContext">The parent <see cref="ActivityContext"/> object to initialize the created Activity object with.</param>
        /// <param name="tags">The optional tags list to initialize the created Activity object with.</param>
        /// <param name="links">The optional <see cref="ActivityLink"/> list to initialize the created Activity object with.</param>
        /// <param name="startTime">The optional start timestamp to set on the created Activity object.</param>
        /// <param name="name">The operation name of the Activity.</param>
        /// <returns>The created <see cref="Activity"/> object or null if there is no any listener.</returns>
        public Activity? StartActivity(ActivityKind kind, ActivityContext parentContext = default, IEnumerable<KeyValuePair<string, object?>>? tags = null, IEnumerable<ActivityLink>? links = null, DateTimeOffset startTime = default, [CallerMemberName] string name = "")
            => CreateActivity(name, kind, parentContext, null, tags, links, startTime);
 
        private Activity? CreateActivity(string name, ActivityKind kind, ActivityContext context, string? parentId, IEnumerable<KeyValuePair<string, object?>>? tags,
                                            IEnumerable<ActivityLink>? links, DateTimeOffset startTime, bool startIt = true, ActivityIdFormat idFormat = ActivityIdFormat.Unknown)
        {
            // _listeners can get assigned to null in Dispose.
            SynchronizedList<ActivityListener>? listeners = _listeners;
            if (listeners == null || listeners.Count == 0)
            {
                return null;
            }
 
            Activity? activity = null;
            ActivityTagsCollection? samplerTags;
            string? traceState;
 
            ActivitySamplingResult samplingResult = ActivitySamplingResult.None;
 
            if (parentId != null)
            {
                ActivityCreationOptions<string> aco = default;
                ActivityCreationOptions<ActivityContext> acoContext = default;
 
                aco = new ActivityCreationOptions<string>(this, name, parentId, kind, tags, links, idFormat);
                if (aco.IdFormat == ActivityIdFormat.W3C)
                {
                    // acoContext is used only in the Sample calls which called only when we have W3C Id format.
                    acoContext = new ActivityCreationOptions<ActivityContext>(this, name, aco.GetContext(), kind, tags, links, ActivityIdFormat.W3C);
                }
 
                listeners.EnumWithFunc((ActivityListener listener, ref ActivityCreationOptions<string> data, ref ActivitySamplingResult result, ref ActivityCreationOptions<ActivityContext> dataWithContext) => {
                    SampleActivity<string>? sampleUsingParentId = listener.SampleUsingParentId;
                    if (sampleUsingParentId != null)
                    {
                        ActivitySamplingResult sr = sampleUsingParentId(ref data);
                        dataWithContext.SetTraceState(data.TraceState); // Keep the trace state in sync between data and dataWithContext
 
                        if (sr > result)
                        {
                            result = sr;
                        }
                    }
                    else if (data.IdFormat == ActivityIdFormat.W3C)
                    {
                        // In case we have a parent Id and the listener not providing the SampleUsingParentId, we'll try to find out if the following conditions are true:
                        //   - The listener is providing the Sample callback
                        //   - Can convert the parent Id to a Context. ActivityCreationOptions.TraceId != default means parent id converted to a valid context.
                        // Then we can call the listener Sample callback with the constructed context.
                        SampleActivity<ActivityContext>? sample = listener.Sample;
                        if (sample != null)
                        {
                            ActivitySamplingResult sr = sample(ref dataWithContext);
                            data.SetTraceState(dataWithContext.TraceState); // Keep the trace state in sync between data and dataWithContext
 
                            if (sr > result)
                            {
                                result = sr;
                            }
                        }
                    }
                }, ref aco, ref samplingResult, ref acoContext);
 
                if (context == default)
                {
                    if (aco.GetContext() != default)
                    {
                        context = aco.GetContext();
                        parentId = null;
                    }
                    else if (acoContext.GetContext() != default)
                    {
                        context = acoContext.GetContext();
                        parentId = null;
                    }
                }
 
                samplerTags = aco.GetSamplingTags();
                ActivityTagsCollection? atc = acoContext.GetSamplingTags();
                if (atc != null)
                {
                    if (samplerTags == null)
                    {
                        samplerTags = atc;
                    }
                    else
                    {
                        foreach (KeyValuePair<string, object?> tag in atc)
                        {
                            samplerTags.Add(tag);
                        }
                    }
                }
 
                idFormat = aco.IdFormat;
                traceState = aco.TraceState;
            }
            else
            {
                bool useCurrentActivityContext = context == default && Activity.Current != null;
                var aco = new ActivityCreationOptions<ActivityContext>(this, name, useCurrentActivityContext ? Activity.Current!.Context : context, kind, tags, links, idFormat);
                listeners.EnumWithFunc((ActivityListener listener, ref ActivityCreationOptions<ActivityContext> data, ref ActivitySamplingResult result, ref ActivityCreationOptions<ActivityContext> unused) => {
                    SampleActivity<ActivityContext>? sample = listener.Sample;
                    if (sample != null)
                    {
                        ActivitySamplingResult dr = sample(ref data);
                        if (dr > result)
                        {
                            result = dr;
                        }
                    }
                }, ref aco, ref samplingResult, ref aco);
 
                if (!useCurrentActivityContext)
                {
                    // We use the context stored inside ActivityCreationOptions as it is possible the trace id get automatically generated during the sampling.
                    // We don't use the context stored inside ActivityCreationOptions only in case if we used Activity.Current context, the reason is we need to
                    // create the new child activity with Parent set to Activity.Current.
                    context = aco.GetContext();
                }
 
                samplerTags = aco.GetSamplingTags();
                idFormat = aco.IdFormat;
                traceState = aco.TraceState;
            }
 
            if (samplingResult != ActivitySamplingResult.None)
            {
                activity = Activity.Create(this, name, kind, parentId, context, tags, links, startTime, samplerTags, samplingResult, startIt, idFormat, traceState);
            }
 
            return activity;
        }
 
        /// <summary>
        /// Dispose the ActivitySource object and remove the current instance from the global list. empty the listeners list too.
        /// </summary>
        public void Dispose()
        {
            _listeners = null;
            s_activeSources.Remove(this);
        }
 
        /// <summary>
        /// Add a listener to the <see cref="Activity"/> starting and stopping events.
        /// </summary>
        /// <param name="listener"> The <see cref="ActivityListener"/> object to use for listening to the <see cref="Activity"/> events.</param>
        public static void AddActivityListener(ActivityListener listener)
        {
            if (listener is null)
            {
                throw new ArgumentNullException(nameof(listener));
            }
 
            if (s_allListeners.AddIfNotExist(listener))
            {
                s_activeSources.EnumWithAction((source, obj) => {
                    var shouldListenTo = ((ActivityListener)obj).ShouldListenTo;
                    if (shouldListenTo != null && shouldListenTo(source))
                    {
                        source.AddListener((ActivityListener)obj);
                    }
                }, listener);
            }
        }
 
        internal delegate void Function<T, TParent>(T item, ref ActivityCreationOptions<TParent> data, ref ActivitySamplingResult samplingResult, ref ActivityCreationOptions<ActivityContext> dataWithContext);
 
        internal void AddListener(ActivityListener listener)
        {
            if (_listeners == null)
            {
                Interlocked.CompareExchange(ref _listeners, new SynchronizedList<ActivityListener>(), null);
            }
 
            _listeners.AddIfNotExist(listener);
        }
 
        internal static void DetachListener(ActivityListener listener)
        {
            s_allListeners.Remove(listener);
            s_activeSources.EnumWithAction((source, obj) => source._listeners?.Remove((ActivityListener) obj), listener);
        }
 
        internal void NotifyActivityStart(Activity activity)
        {
            Debug.Assert(activity != null);
 
            // _listeners can get assigned to null in Dispose.
            SynchronizedList<ActivityListener>? listeners = _listeners;
            if (listeners != null && listeners.Count > 0)
            {
                listeners.EnumWithAction((listener, obj) => listener.ActivityStarted?.Invoke((Activity)obj), activity);
            }
        }
 
        internal void NotifyActivityStop(Activity activity)
        {
            Debug.Assert(activity != null);
 
            // _listeners can get assigned to null in Dispose.
            SynchronizedList<ActivityListener>? listeners = _listeners;
            if (listeners != null && listeners.Count > 0)
            {
                listeners.EnumWithAction((listener, obj) => listener.ActivityStopped?.Invoke((Activity)obj), activity);
            }
        }
 
        internal void NotifyActivityAddException(Activity activity, Exception exception, ref TagList tags)
        {
            Debug.Assert(activity != null);
 
            // _listeners can get assigned to null in Dispose.
            SynchronizedList<ActivityListener>? listeners = _listeners;
            if (listeners != null && listeners.Count > 0)
            {
                listeners.EnumWithExceptionNotification(activity, exception, ref tags);
            }
        }
    }
 
    // This class uses a copy-on-write design to ensure thread safety all operations are thread safe.
    // However, it is possible for read-only operations to see stale versions of the item while a change
    // is occurring.
    internal sealed class SynchronizedList<T>
    {
        private readonly object _writeLock;
        // This array must not be mutated directly. To mutate, obtain the lock, copy the array and then replace it with the new array.
        private T[] _volatileArray;
        public SynchronizedList()
        {
            _volatileArray = [];
            _writeLock = new();
        }
 
        public void Add(T item)
        {
            lock (_writeLock)
            {
                T[] newArray = new T[_volatileArray.Length + 1];
 
                Array.Copy(_volatileArray, newArray, _volatileArray.Length);// copy existing items
                newArray[_volatileArray.Length] = item;// copy new item
 
                _volatileArray = newArray;
            }
        }
 
        public bool AddIfNotExist(T item)
        {
            lock (_writeLock)
            {
                int index = Array.IndexOf(_volatileArray, item);
 
                if (index >= 0)
                {
                    return false;
                }
 
                T[] newArray = new T[_volatileArray.Length + 1];
 
                Array.Copy(_volatileArray, newArray, _volatileArray.Length);// copy existing items
                newArray[_volatileArray.Length] = item;// copy new item
 
                _volatileArray = newArray;
 
                return true;
            }
        }
 
        public bool Remove(T item)
        {
            lock (_writeLock)
            {
                int index = Array.IndexOf(_volatileArray, item);
 
                if (index < 0)
                {
                    return false;
                }
 
                T[] newArray = new T[_volatileArray.Length - 1];
 
                Array.Copy(_volatileArray, newArray, index);// copy existing items before index
 
                Array.Copy(
                    _volatileArray, index + 1, // position after the index, skipping it
                    newArray, index, _volatileArray.Length - index - 1// remaining items accounting for removed item
                );
 
                _volatileArray = newArray;
                return true;
            }
        }
 
        public int Count => _volatileArray.Length;
 
        public void EnumWithFunc<TParent>(ActivitySource.Function<T, TParent> func, ref ActivityCreationOptions<TParent> data, ref ActivitySamplingResult samplingResult, ref ActivityCreationOptions<ActivityContext> dataWithContext)
        {
            foreach (T item in _volatileArray)
            {
                func(item, ref data, ref samplingResult, ref dataWithContext);
            }
        }
 
        public void EnumWithAction(Action<T, object> action, object arg)
        {
            foreach (T item in _volatileArray)
            {
                action(item, arg);
            }
        }
 
        public void EnumWithExceptionNotification(Activity activity, Exception exception, ref TagList tags)
        {
            if (typeof(T) != typeof(ActivityListener))
            {
                return;
            }
 
            foreach (T item in _volatileArray)
            {
                (item as ActivityListener)!.ExceptionRecorder?.Invoke(activity, exception, ref tags);
            }
        }
    }
}