File: System\Diagnostics\DsesActivitySourceListener.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.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
 
namespace System.Diagnostics;
 
internal sealed class DsesActivitySourceListener : IDisposable
{
    private readonly DiagnosticSourceEventSource _eventSource;
    private DsesFilterAndTransform? _wildcardSpec;
    private Dictionary<SpecLookupKey, DsesFilterAndTransform>? _specsBySourceNameAndActivityName;
    private HashSet<string>? _listenToActivitySourceNames;
    private bool _hasActivityNameSpecDefined;
    private ActivityListener? _activityListener;
 
    public static DsesActivitySourceListener Create(
        DiagnosticSourceEventSource eventSource,
        DsesFilterAndTransform activitySourceSpecs)
    {
        var listener = new DsesActivitySourceListener(eventSource);
 
        listener.NormalizeActivitySourceSpecsList(activitySourceSpecs);
 
        listener.CreateActivityListener();
 
        return listener;
    }
 
    private DsesActivitySourceListener(DiagnosticSourceEventSource eventSource)
    {
        _eventSource = eventSource;
    }
 
    public void Dispose()
    {
        _activityListener?.Dispose();
        _activityListener = null;
        _wildcardSpec = null;
        _specsBySourceNameAndActivityName = null;
        _listenToActivitySourceNames = null;
    }
 
    private void NormalizeActivitySourceSpecsList(
        DsesFilterAndTransform? activitySourceSpecs)
    {
        Debug.Assert(activitySourceSpecs != null);
 
        while (activitySourceSpecs != null)
        {
            DsesFilterAndTransform? currentActivitySourceSpec = activitySourceSpecs;
 
            Debug.Assert(currentActivitySourceSpec.SourceName != null);
            Debug.Assert(currentActivitySourceSpec.SampleFunc != null);
 
            activitySourceSpecs = activitySourceSpecs.Next;
 
            if (currentActivitySourceSpec.SourceName == "*")
            {
                if (_wildcardSpec != null)
                {
                    if (_eventSource.IsEnabled(EventLevel.Warning, DiagnosticSourceEventSource.Keywords.Messages))
                        _eventSource.Message("DiagnosticSource: Ignoring wildcard activity source filterAndPayloadSpec rule because a previous rule was defined");
                    continue;
                }
 
                _wildcardSpec = currentActivitySourceSpec;
            }
            else
            {
                var specs = _specsBySourceNameAndActivityName ??= new(SpecLookupKeyComparer.Instance);
                var allSources = _listenToActivitySourceNames ??= new(StringComparer.OrdinalIgnoreCase);
 
                SpecLookupKey key = new(currentActivitySourceSpec.SourceName, currentActivitySourceSpec.ActivityName);
 
#if NETFRAMEWORK || NETSTANDARD2_0
                if (specs.ContainsKey(key))
                {
                    LogIgnoredSpecRule(currentActivitySourceSpec.SourceName, currentActivitySourceSpec.ActivityName);
                    continue;
                }
                specs[key] = currentActivitySourceSpec;
#else
                if (!specs.TryAdd(key, currentActivitySourceSpec))
                {
                    LogIgnoredSpecRule(currentActivitySourceSpec.SourceName, currentActivitySourceSpec.ActivityName);
                    continue;
                }
#endif
                allSources.Add(key.activitySourceName);
                if (key.activityName != null)
                {
                    _hasActivityNameSpecDefined = true;
                }
            }
        }
 
        Debug.Assert(_wildcardSpec != null || _specsBySourceNameAndActivityName != null);
 
        void LogIgnoredSpecRule(string activitySourceName, string? activityName)
        {
            if (_eventSource.IsEnabled(EventLevel.Warning, DiagnosticSourceEventSource.Keywords.Messages))
            {
                if (activityName == null)
                {
                    _eventSource.Message($"DiagnosticSource: Ignoring filterAndPayloadSpec rule for '[AS]{activitySourceName}' because a previous rule was defined");
                }
                else
                {
                    _eventSource.Message($"DiagnosticSource: Ignoring filterAndPayloadSpec rule for '[AS]{activitySourceName}+{activityName}' because a previous rule was defined");
                }
            }
        }
    }
 
    private void CreateActivityListener()
    {
        Debug.Assert(_activityListener == null);
        Debug.Assert(_wildcardSpec != null
            || _specsBySourceNameAndActivityName != null);
 
        _activityListener = new ActivityListener();
 
        _activityListener.SampleUsingParentId = OnSampleUsingParentId;
        _activityListener.Sample = OnSample;
 
        _activityListener.ShouldListenTo = (activitySource) =>
        {
            return _wildcardSpec != null
                || (_listenToActivitySourceNames != null
                && _listenToActivitySourceNames.Contains(activitySource.Name));
        };
 
        _activityListener.ActivityStarted = OnActivityStarted;
 
        _activityListener.ActivityStopped = OnActivityStopped;
 
        ActivitySource.AddActivityListener(_activityListener);
    }
 
    private bool TryFindSpecForActivity(
        string activitySourceName,
        string activityName,
        [NotNullWhen(true)] out DsesFilterAndTransform? spec)
    {
        if (_specsBySourceNameAndActivityName != null)
        {
            if (_hasActivityNameSpecDefined &&
                _specsBySourceNameAndActivityName.TryGetValue(new(activitySourceName, activityName), out spec))
            {
                return true;
            }
            if (_specsBySourceNameAndActivityName.TryGetValue(new(activitySourceName, null), out spec))
            {
                return true;
            }
        }
 
        return (spec = _wildcardSpec) != null;
    }
 
    [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Activity))]
    [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(ActivityContext))]
    [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(ActivityEvent))]
    [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(ActivityLink))]
    [DynamicDependency(nameof(DateTime.Ticks), typeof(DateTime))]
    [DynamicDependency(nameof(TimeSpan.Ticks), typeof(TimeSpan))]
    [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
        Justification = "Activity's properties are being preserved with the DynamicDependencies on OnActivityStarted.")]
    private void OnActivityStarted(Activity activity)
    {
        if (TryFindSpecForActivity(activity.Source.Name, activity.OperationName, out var spec)
            && (spec.Events & DsesActivityEvents.ActivityStart) != 0)
        {
            _eventSource.ActivityStart(activity.Source.Name, activity.OperationName, spec.Morph(activity));
        }
    }
 
    [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
        Justification = "Activity's properties are being preserved with the DynamicDependencies on OnActivityStarted.")]
    private void OnActivityStopped(Activity activity)
    {
        if (TryFindSpecForActivity(activity.Source.Name, activity.OperationName, out var spec)
            && (spec.Events & DsesActivityEvents.ActivityStop) != 0)
        {
            _eventSource.ActivityStop(activity.Source.Name, activity.OperationName, spec.Morph(activity));
        }
    }
 
    private ActivitySamplingResult OnSampleUsingParentId(ref ActivityCreationOptions<string> options)
    {
        ActivityCreationOptions<ActivityContext> activityContextOptions = default;
 
        return OnSample(options.Source.Name, options.Name, hasActivityContext: false, ref activityContextOptions);
    }
 
    private ActivitySamplingResult OnSample(ref ActivityCreationOptions<ActivityContext> options)
    {
        return OnSample(options.Source.Name, options.Name, hasActivityContext: true, ref options);
    }
 
    private ActivitySamplingResult OnSample(
        string activitySourceName,
        string activityName,
        bool hasActivityContext,
        ref ActivityCreationOptions<ActivityContext> options)
    {
        return TryFindSpecForActivity(activitySourceName, activityName, out var spec)
            ? spec.SampleFunc!(hasActivityContext, ref options)
            : ActivitySamplingResult.None;
    }
 
    private readonly struct SpecLookupKey
    {
        public SpecLookupKey(
            string activitySourceName,
            string? activityName)
        {
            Debug.Assert(activitySourceName != null);
 
            this.activitySourceName = activitySourceName;
            this.activityName = activityName;
        }
 
        public readonly string activitySourceName;
        public readonly string? activityName;
    }
 
    private sealed class SpecLookupKeyComparer : IEqualityComparer<SpecLookupKey>
    {
        public static readonly SpecLookupKeyComparer Instance = new();
 
        public bool Equals(SpecLookupKey x, SpecLookupKey y)
            => string.Equals(x.activitySourceName, y.activitySourceName, StringComparison.OrdinalIgnoreCase)
                && string.Equals(x.activityName, y.activityName, StringComparison.OrdinalIgnoreCase);
 
        public int GetHashCode(SpecLookupKey obj)
        {
            // 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 much, we are implementing
            // the hashing manually.
            int hash = 5381;
            hash = ((hash << 5) + hash) + StringComparer.OrdinalIgnoreCase.GetHashCode(obj.activitySourceName);
            hash = ((hash << 5) + hash) + (obj.activityName == null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(obj.activityName));
 
            return hash;
        }
    }
}