File: System\Diagnostics\DsesFilterAndTransform.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;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Tracing;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
 
namespace System.Diagnostics;
 
/// <summary>
/// FilterAndTransform represents on transformation specification from a DiagnosticsSource
/// to EventSource's 'Event' method. (e.g. MySource/MyEvent:out=prop1.prop2.prop3).
/// Its main method is 'Morph' which takes a DiagnosticSource object and morphs it into
/// a list of string,string key value pairs.
///
/// This method also contains that static 'Create/Destroy FilterAndTransformList, which
/// simply parse a series of transformation specifications.
/// </summary>
internal sealed class DsesFilterAndTransform : IDisposable
{
    private const string c_ActivitySourcePrefix = "[AS]";
    private const string c_ParentRatioSamplerPrefix = "ParentRatioSampler(";
 
    /// <summary>
    /// Parses filterAndPayloadSpecs which is a list of lines each of which has the from
    ///
    ///    DiagnosticSourceName/EventName:PAYLOAD_SPEC
    ///
    /// where PAYLOADSPEC is a semicolon separated list of specifications of the form
    ///
    ///    OutputName=Prop1.Prop2.PropN
    ///
    /// Into linked list of FilterAndTransform that together forward events from the given
    /// DiagnosticSource's to 'eventSource'. Sets the 'specList' variable to this value
    /// (destroying anything that was there previously).
    ///
    /// By default any serializable properties of the payload object are also included
    /// in the output payload, however this feature and be tuned off by prefixing the
    /// PAYLOADSPEC with a '-'.
    /// </summary>
    public static IDisposable ParseFilterAndPayloadSpecs(
        DiagnosticSourceEventSource eventSource,
        string? filterAndPayloadSpecs)
    {
        filterAndPayloadSpecs ??= "";
 
        DsesFilterAndTransform? specList = null;
        DsesFilterAndTransform? activitySourceSpecList = null;
 
        // Points just beyond the last point in the string that has yet to be parsed. Thus we start with the whole string.
        int endIdx = filterAndPayloadSpecs.Length;
        while (true)
        {
            // Skip trailing whitespace.
            while (0 < endIdx && char.IsWhiteSpace(filterAndPayloadSpecs[endIdx - 1]))
                --endIdx;
 
            int newlineIdx = filterAndPayloadSpecs.LastIndexOf('\n', endIdx - 1, endIdx);
            int startIdx = 0;
            if (0 <= newlineIdx)
                startIdx = newlineIdx + 1;  // starts after the newline, or zero if we don't find one.
 
            // Skip leading whitespace
            while (startIdx < endIdx && char.IsWhiteSpace(filterAndPayloadSpecs[startIdx]))
                startIdx++;
 
            if (IsActivitySourceEntry(filterAndPayloadSpecs, startIdx, endIdx))
            {
                activitySourceSpecList = CreateActivitySourceTransform(eventSource, filterAndPayloadSpecs, startIdx, endIdx, activitySourceSpecList);
            }
            else
            {
                specList = CreateTransform(eventSource, filterAndPayloadSpecs, startIdx, endIdx, specList);
            }
 
            endIdx = newlineIdx;
            if (endIdx < 0)
                break;
        }
 
        DsesActivitySourceListener? activitySourceListener = activitySourceSpecList != null
            ? DsesActivitySourceListener.Create(eventSource, activitySourceSpecList)
            : null;
 
        return new ParsedFilterAndPayloadSpecs(specList, activitySourceListener);
    }
 
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool IsActivitySourceEntry(string filterAndPayloadSpec, int startIdx, int endIdx) =>
        filterAndPayloadSpec.AsSpan(startIdx, endIdx - startIdx).StartsWith(c_ActivitySourcePrefix.AsSpan(), StringComparison.Ordinal);
 
    /// <summary>
    /// Creates one FilterAndTransform specification from filterAndPayloadSpec starting at 'startIdx' and ending just before 'endIdx'.
    /// This FilterAndTransform will subscribe to DiagnosticSources specified by the specification and forward them to 'eventSource.
    /// For convenience, the 'Next' field is set to the 'next' parameter, so you can easily form linked lists.
    /// </summary>
    private static DsesFilterAndTransform? CreateTransform(
        DiagnosticSourceEventSource eventSource,
        string filterAndPayloadSpec,
        int startIdx,
        int endIdx,
        DsesFilterAndTransform? next)
    {
        Debug.Assert(filterAndPayloadSpec != null && startIdx >= 0 && startIdx <= endIdx && endIdx <= filterAndPayloadSpec.Length);
 
        string? listenerNameFilter = null;       // Means WildCard.
        string? eventNameFilter = null;          // Means WildCard.
        string? activityName = null;
        bool noImplicitTransforms = false;
        TransformSpec? explicitTransforms = null;
 
        var startTransformIdx = startIdx;
        var endEventNameIdx = endIdx;
        var colonIdx = filterAndPayloadSpec.IndexOf(':', startIdx, endIdx - startIdx);
        if (0 <= colonIdx)
        {
            endEventNameIdx = colonIdx;
            startTransformIdx = colonIdx + 1;
        }
 
        // Parse the Source/Event name into listenerNameFilter and eventNameFilter
        var slashIdx = filterAndPayloadSpec.IndexOf('/', startIdx, endEventNameIdx - startIdx);
        if (0 <= slashIdx)
        {
            listenerNameFilter = filterAndPayloadSpec.Substring(startIdx, slashIdx - startIdx);
 
            var atIdx = filterAndPayloadSpec.IndexOf('@', slashIdx + 1, endEventNameIdx - slashIdx - 1);
            if (0 <= atIdx)
            {
                activityName = filterAndPayloadSpec.Substring(atIdx + 1, endEventNameIdx - atIdx - 1);
                eventNameFilter = filterAndPayloadSpec.Substring(slashIdx + 1, atIdx - slashIdx - 1);
            }
            else
            {
                eventNameFilter = filterAndPayloadSpec.Substring(slashIdx + 1, endEventNameIdx - slashIdx - 1);
            }
        }
        else if (startIdx < endEventNameIdx)
        {
            listenerNameFilter = filterAndPayloadSpec.Substring(startIdx, endEventNameIdx - startIdx);
        }
 
        eventSource.Message("DiagnosticSource: Enabling '" + (listenerNameFilter ?? "*") + "/" + (eventNameFilter ?? "*") + "'");
 
        // If the transform spec begins with a - it means you don't want implicit transforms.
        if (startTransformIdx < endIdx && filterAndPayloadSpec[startTransformIdx] == '-')
        {
            eventSource.Message("DiagnosticSource: suppressing implicit transforms.");
            noImplicitTransforms = true;
            startTransformIdx++;
        }
 
        // Parse all the explicit transforms, if present
        if (startTransformIdx < endIdx)
        {
            while (true)
            {
                int specStartIdx = startTransformIdx;
                int semiColonIdx = filterAndPayloadSpec.LastIndexOf(';', endIdx - 1, endIdx - startTransformIdx);
                if (0 <= semiColonIdx)
                    specStartIdx = semiColonIdx + 1;
 
                // Ignore empty specifications.
                if (specStartIdx < endIdx)
                {
                    if (eventSource.IsEnabled(EventLevel.Informational, DiagnosticSourceEventSource.Keywords.Messages))
                        eventSource.Message("DiagnosticSource: Parsing Explicit Transform '" + filterAndPayloadSpec.Substring(specStartIdx, endIdx - specStartIdx) + "'");
 
                    explicitTransforms = new TransformSpec(eventSource, filterAndPayloadSpec, specStartIdx, endIdx, explicitTransforms);
                }
                if (startTransformIdx == specStartIdx)
                    break;
                endIdx = semiColonIdx;
            }
        }
 
        var transform = new DsesFilterAndTransform(
            eventSource,
            next,
            noImplicitTransforms,
            explicitTransforms,
            sourceName: null,
            activityName: null,
            activityEvents: default,
            sampleFunc: null);
 
        transform.SetupDiagnosticListenerSubscription(listenerNameFilter, eventNameFilter, activityName);
 
        return transform;
    }
 
    private static DsesFilterAndTransform? CreateActivitySourceTransform(
        DiagnosticSourceEventSource eventSource,
        string filterAndPayloadSpec,
        int startIdx,
        int endIdx,
        DsesFilterAndTransform? next)
    {
        Debug.Assert(endIdx - startIdx >= 4);
        Debug.Assert(IsActivitySourceEntry(filterAndPayloadSpec, startIdx, endIdx));
 
        bool noImplicitTransforms = false;
        TransformSpec? explicitTransforms = null;
        ReadOnlySpan<char> eventName;
        ReadOnlySpan<char> activitySourceName;
 
        DsesActivityEvents supportedEvent = DsesActivityEvents.All; // Default events
        DsesSampleActivityFunc sampleFunc = static (bool hasActivityContext, ref ActivityCreationOptions<ActivityContext> options)
            => ActivitySamplingResult.AllDataAndRecorded; // Default sampler
 
        int colonIdx = filterAndPayloadSpec.IndexOf(':', startIdx + c_ActivitySourcePrefix.Length, endIdx - startIdx - c_ActivitySourcePrefix.Length);
 
        ReadOnlySpan<char> entry = filterAndPayloadSpec.AsSpan(
            startIdx + c_ActivitySourcePrefix.Length,
            (colonIdx >= 0 ? colonIdx : endIdx) - startIdx - c_ActivitySourcePrefix.Length)
            .Trim();
 
        int eventNameIndex = entry.IndexOf('/');
        if (eventNameIndex >= 0)
        {
            activitySourceName = entry.Slice(0, eventNameIndex).Trim();
 
            ReadOnlySpan<char> suffixPart = entry.Slice(eventNameIndex + 1).Trim();
            int samplingResultIndex = suffixPart.IndexOf('-');
            if (samplingResultIndex >= 0)
            {
                // We have the format "[AS]SourceName/[EventName]-[SamplingResult]
                eventName = suffixPart.Slice(0, samplingResultIndex).Trim();
                suffixPart = suffixPart.Slice(samplingResultIndex + 1).Trim();
 
                if (suffixPart.Length > 0)
                {
                    if (suffixPart.Equals("Propagate".AsSpan(), StringComparison.OrdinalIgnoreCase))
                    {
                        sampleFunc = static (bool hasActivityContext, ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.PropagationData;
                    }
                    else if (suffixPart.Equals("Record".AsSpan(), StringComparison.OrdinalIgnoreCase))
                    {
                        sampleFunc = static (bool hasActivityContext, ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData;
                    }
                    else if (suffixPart.StartsWith(c_ParentRatioSamplerPrefix.AsSpan(), StringComparison.OrdinalIgnoreCase))
                    {
                        int endingLocation = suffixPart.IndexOf(')');
                        if (endingLocation < 0
#if NETFRAMEWORK || NETSTANDARD
                            || !double.TryParse(suffixPart.Slice(c_ParentRatioSamplerPrefix.Length, endingLocation - c_ParentRatioSamplerPrefix.Length).ToString(), NumberStyles.Float, CultureInfo.InvariantCulture, out double ratio))
#else
                            || !double.TryParse(suffixPart.Slice(c_ParentRatioSamplerPrefix.Length, endingLocation - c_ParentRatioSamplerPrefix.Length), NumberStyles.Float, CultureInfo.InvariantCulture, out double ratio))
#endif
                        {
                            if (eventSource.IsEnabled(EventLevel.Warning, DiagnosticSourceEventSource.Keywords.Messages))
                                eventSource.Message("DiagnosticSource: Ignoring filterAndPayloadSpec '[AS]" + entry.ToString() + "' because sampling ratio was invalid");
                            return next;
                        }
 
                        sampleFunc = DsesSamplerBuilder.CreateParentRatioSampler(ratio);
                    }
                    else
                    {
                        if (eventSource.IsEnabled(EventLevel.Warning, DiagnosticSourceEventSource.Keywords.Messages))
                            eventSource.Message("DiagnosticSource: Ignoring filterAndPayloadSpec '[AS]" + entry.ToString() + "' because sampling method was invalid");
                        return next;
                    }
                }
            }
            else
            {
                // We have the format "[AS]SourceName/[EventName]
                eventName = suffixPart;
            }
 
            if (eventName.Length > 0)
            {
                if (eventName.Equals("Start".AsSpan(), StringComparison.OrdinalIgnoreCase))
                {
                    supportedEvent = DsesActivityEvents.ActivityStart;
                }
                else if (eventName.Equals("Stop".AsSpan(), StringComparison.OrdinalIgnoreCase))
                {
                    supportedEvent = DsesActivityEvents.ActivityStop;
                }
                else
                {
                    if (eventSource.IsEnabled(EventLevel.Warning, DiagnosticSourceEventSource.Keywords.Messages))
                        eventSource.Message("DiagnosticSource: Ignoring filterAndPayloadSpec '[AS]" + entry.ToString() + "' because event name was invalid");
                    return next;
                }
            }
        }
        else
        {
            // We have the format "[AS]SourceName"
            activitySourceName = entry;
        }
 
        string? activityName = null;
        int plusSignIndex = activitySourceName.IndexOf('+');
        if (plusSignIndex >= 0)
        {
            activityName = activitySourceName.Slice(plusSignIndex + 1).Trim().ToString();
            activitySourceName = activitySourceName.Slice(0, plusSignIndex).Trim();
 
            if (activityName.Length > 0 && activitySourceName.Length == 1 && activitySourceName[0] == '*')
            {
                if (eventSource.IsEnabled(EventLevel.Warning, DiagnosticSourceEventSource.Keywords.Messages))
                    eventSource.Message("DiagnosticSource: Ignoring filterAndPayloadSpec '[AS]" + entry.ToString() + "' because activity name cannot be specified for wildcard activity sources");
                return next;
            }
        }
 
        if (colonIdx >= 0)
        {
            int startTransformIdx = colonIdx + 1;
 
            // If the transform spec begins with a - it means you don't want implicit transforms.
            if (startTransformIdx < endIdx && filterAndPayloadSpec[startTransformIdx] == '-')
            {
                eventSource.Message("DiagnosticSource: suppressing implicit transforms.");
                noImplicitTransforms = true;
                startTransformIdx++;
            }
 
            // Parse all the explicit transforms, if present
            if (startTransformIdx < endIdx)
            {
                while (true)
                {
                    int specStartIdx = startTransformIdx;
                    int semiColonIdx = filterAndPayloadSpec.LastIndexOf(';', endIdx - 1, endIdx - startTransformIdx);
                    if (0 <= semiColonIdx)
                        specStartIdx = semiColonIdx + 1;
 
                    // Ignore empty specifications.
                    if (specStartIdx < endIdx)
                    {
                        if (eventSource.IsEnabled(EventLevel.Informational, DiagnosticSourceEventSource.Keywords.Messages))
                            eventSource.Message("DiagnosticSource: Parsing Explicit Transform '" + filterAndPayloadSpec.Substring(specStartIdx, endIdx - specStartIdx) + "'");
 
                        explicitTransforms = new TransformSpec(eventSource, filterAndPayloadSpec, specStartIdx, endIdx, explicitTransforms);
                    }
                    if (startTransformIdx == specStartIdx)
                        break;
                    endIdx = semiColonIdx;
                }
            }
        }
 
        return new DsesFilterAndTransform(
            eventSource,
            next,
            noImplicitTransforms,
            explicitTransforms,
            activitySourceName.ToString(),
            activityName,
            supportedEvent,
            sampleFunc);
    }
 
    private DsesFilterAndTransform(
        DiagnosticSourceEventSource eventSource,
        DsesFilterAndTransform? next,
        bool noImplicitTransforms,
        TransformSpec? explicitTransforms,
        string? sourceName,
        string? activityName,
        DsesActivityEvents activityEvents,
        DsesSampleActivityFunc? sampleFunc)
    {
        _eventSource = eventSource;
        _noImplicitTransforms = noImplicitTransforms;
        _explicitTransforms = explicitTransforms;
 
        Next = next;
        SourceName = sourceName;
        ActivityName = activityName;
        Events = activityEvents;
        SampleFunc = sampleFunc;
    }
 
    public void Dispose()
    {
        if (_diagnosticsListenersSubscription != null)
        {
            _diagnosticsListenersSubscription.Dispose();
            _diagnosticsListenersSubscription = null;
        }
 
        if (_liveSubscriptions != null)
        {
            Subscriptions? subscr = _liveSubscriptions;
            _liveSubscriptions = null;
            while (subscr != null)
            {
                subscr.Subscription.Dispose();
                subscr = subscr.Next;
            }
        }
    }
 
    [RequiresUnreferencedCode(DiagnosticSource.WriteRequiresUnreferencedCode)]
    public List<KeyValuePair<string, string?>> Morph(object? args)
    {
        // Transform the args into a bag of key-value strings.
        var outputArgs = new List<KeyValuePair<string, string?>>();
        if (args != null)
        {
            if (!_noImplicitTransforms)
            {
                // given the type, fetch the implicit transforms for that type and put it in the implicitTransforms variable.
                Type argType = args.GetType();
                TransformSpec? implicitTransforms;
 
                // First check the one-element cache _firstImplicitTransformsEntry
                ImplicitTransformEntry? cacheEntry = _firstImplicitTransformsEntry;
                if (cacheEntry != null && cacheEntry.Type == argType)
                {
                    implicitTransforms = cacheEntry.Transforms;     // Yeah we hit the cache.
                }
                else if (cacheEntry == null)
                {
                    // _firstImplicitTransformsEntry is empty, we should fill it.
                    // Note that it is OK that two threads may race and both call MakeImplicitTransforms on their own
                    // (that is we don't expect exactly once initialization of _firstImplicitTransformsEntry)
                    implicitTransforms = MakeImplicitTransforms(_eventSource, argType);
                    Interlocked.CompareExchange(ref _firstImplicitTransformsEntry,
                        new ImplicitTransformEntry() { Type = argType, Transforms = implicitTransforms }, null);
                }
                else
                {
                    // This should only happen when you are wildcarding your events (reasonably rare).
                    // In that case you will probably need many types
                    // Note currently we don't limit the cache size, but it is limited by the number of
                    // distinct types of objects passed to DiagnosticSource.Write.
                    if (_implicitTransformsTable == null)
                    {
                        Interlocked.CompareExchange(ref _implicitTransformsTable,
                            new ConcurrentDictionary<Type, TransformSpec?>(1, 8), null);
                    }
                    implicitTransforms = _implicitTransformsTable.GetOrAdd(argType, t => MakeImplicitTransforms(_eventSource, t));
                }
 
                // implicitTransformas now fetched from cache or constructed, use it to Fetch all the implicit fields.
                if (implicitTransforms != null)
                {
                    for (TransformSpec? serializableArg = implicitTransforms; serializableArg != null; serializableArg = serializableArg.Next)
                        outputArgs.Add(serializableArg.Morph(args));
                }
            }
 
            if (_explicitTransforms != null)
            {
                for (TransformSpec? explicitTransform = _explicitTransforms; explicitTransform != null; explicitTransform = explicitTransform.Next)
                {
                    var keyValue = explicitTransform.Morph(args);
                    if (keyValue.Value != null)
                        outputArgs.Add(keyValue);
                }
            }
        }
        return outputArgs;
    }
 
    private void SetupDiagnosticListenerSubscription(
        string? listenerNameFilter, // Means WildCard.
        string? eventNameFilter, // Means WildCard.
        string? activityName)
    {
        Action<string, string, IEnumerable<KeyValuePair<string, string?>>>? writeEvent = null;
        if (activityName != null && activityName.Contains("Activity"))
        {
            writeEvent = activityName switch
            {
                nameof(DiagnosticSourceEventSource.Activity1Start) => _eventSource.Activity1Start,
                nameof(DiagnosticSourceEventSource.Activity1Stop) => _eventSource.Activity1Stop,
                nameof(DiagnosticSourceEventSource.Activity2Start) => _eventSource.Activity2Start,
                nameof(DiagnosticSourceEventSource.Activity2Stop) => _eventSource.Activity2Stop,
                nameof(DiagnosticSourceEventSource.RecursiveActivity1Start) => _eventSource.RecursiveActivity1Start,
                nameof(DiagnosticSourceEventSource.RecursiveActivity1Stop) => _eventSource.RecursiveActivity1Stop,
                _ => null
            };
 
            if (writeEvent == null)
                _eventSource.Message("DiagnosticSource: Could not find Event to log Activity " + activityName);
        }
 
        writeEvent ??= _eventSource.Event;
 
        // Set up a subscription that watches for the given Diagnostic Sources and events which will call back
        // to the EventSource.
        _diagnosticsListenersSubscription = DiagnosticListener.AllListeners.Subscribe(new CallbackObserver<DiagnosticListener>(delegate (DiagnosticListener newListener)
        {
            if (listenerNameFilter == null || listenerNameFilter == newListener.Name)
            {
                _eventSource.NewDiagnosticListener(newListener.Name);
                Predicate<string>? eventNameFilterPredicate = null;
                if (eventNameFilter != null)
                    eventNameFilterPredicate = (string eventName) => eventNameFilter == eventName;
 
                [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode",
                    Justification = "DiagnosticSource.Write is marked with RequiresUnreferencedCode.")]
                void OnEventWritten(KeyValuePair<string, object?> evnt)
                {
                    // The filter given to the DiagnosticSource may not work if users don't is 'IsEnabled' as expected.
                    // Thus we look for any events that may have snuck through and filter them out before forwarding.
                    if (eventNameFilter != null && eventNameFilter != evnt.Key)
                        return;
 
                    var outputArgs = this.Morph(evnt.Value);
                    var eventName = evnt.Key;
                    writeEvent(newListener.Name, eventName, outputArgs);
                }
 
                var subscription = newListener.Subscribe(new CallbackObserver<KeyValuePair<string, object?>>(OnEventWritten), eventNameFilterPredicate);
                _liveSubscriptions = new Subscriptions(subscription, _liveSubscriptions);
            }
        }));
    }
 
    public DsesFilterAndTransform? Next { get; }
 
    // Specific ActivitySource Transforms information
 
    internal string? SourceName { get; }
    internal string? ActivityName { get; }
    internal DsesActivityEvents Events { get; }
    internal DsesSampleActivityFunc? SampleFunc { get; }
 
    private sealed class ParsedFilterAndPayloadSpecs : IDisposable
    {
        private DsesFilterAndTransform? _specList;
        private DsesActivitySourceListener? _activitySourceListener;
 
        public ParsedFilterAndPayloadSpecs(
            DsesFilterAndTransform? specList,
            DsesActivitySourceListener? activitySourceListener)
        {
            _specList = specList;
            _activitySourceListener = activitySourceListener;
        }
 
        /// <summary>
        /// This destroys (turns off) the FilterAndTransform stopping the forwarding started with CreateFilterAndTransformList
        /// </summary>
        public void Dispose()
        {
            _activitySourceListener?.Dispose();
            _activitySourceListener = null;
 
            var curSpec = _specList;
            _specList = null;            // Null out the list
            while (curSpec != null)     // Dispose everything in the list.
            {
                curSpec.Dispose();
                curSpec = curSpec.Next;
            }
        }
    }
 
    // This olds one the implicit transform for one type of object.
    // We remember this type-transform pair in the _firstImplicitTransformsEntry cache.
    private sealed class ImplicitTransformEntry
    {
        public Type? Type;
        public TransformSpec? Transforms;
    }
 
    /// <summary>
    /// Transform spec represents a string that describes how to extract a piece of data from
    /// the DiagnosticSource payload. An example string is OUTSTR=EVENT_VALUE.PROP1.PROP2.PROP3
    /// It has a Next field so they can be chained together in a linked list.
    /// </summary>
    private sealed class TransformSpec
    {
        /// <summary>
        /// parse the strings 'spec' from startIdx to endIdx (points just beyond the last considered char)
        /// The syntax is ID1=ID2.ID3.ID4 .... Where ID1= is optional.
        /// </summary>
        public TransformSpec(DiagnosticSourceEventSource eventSource, string transformSpec, int startIdx, int endIdx, TransformSpec? next = null)
        {
            Debug.Assert(transformSpec != null && startIdx >= 0 && startIdx < endIdx && endIdx <= transformSpec.Length);
            Next = next;
 
            // Pick off the Var=
            int equalsIdx = transformSpec.IndexOf('=', startIdx, endIdx - startIdx);
            if (0 <= equalsIdx)
            {
                _outputName = transformSpec.Substring(startIdx, equalsIdx - startIdx);
                startIdx = equalsIdx + 1;
            }
 
            // Working from back to front, create a PropertySpec for each .ID in the string.
            while (startIdx < endIdx)
            {
                int dotIdx = transformSpec.LastIndexOf('.', endIdx - 1, endIdx - startIdx);
                int idIdx = startIdx;
                if (0 <= dotIdx)
                    idIdx = dotIdx + 1;
 
                string propertyName = transformSpec.Substring(idIdx, endIdx - idIdx);
                _fetches = new PropertySpec(eventSource, propertyName, _fetches);
 
                // If the user did not explicitly set a name, it is the last one (first to be processed from the end).
                _outputName ??= propertyName;
 
                endIdx = dotIdx;    // This works even when LastIndexOf return -1.
            }
        }
 
        /// <summary>
        /// Given the DiagnosticSourcePayload 'obj', compute a key-value pair from it. For example
        /// if the spec is OUTSTR=EVENT_VALUE.PROP1.PROP2.PROP3 and the ultimate value of PROP3 is
        /// 10 then the return key value pair is  KeyValuePair("OUTSTR","10")
        /// </summary>
        [RequiresUnreferencedCode(DiagnosticSource.WriteRequiresUnreferencedCode)]
        public KeyValuePair<string, string?> Morph(object? obj)
        {
            for (PropertySpec? cur = _fetches; cur != null; cur = cur.Next)
            {
                if (obj != null || cur.IsStatic)
                    obj = cur.Fetch(obj);
            }
 
            return new KeyValuePair<string, string?>(_outputName, obj?.ToString());
        }
 
        /// <summary>
        /// A public field that can be used to form a linked list.
        /// </summary>
        public TransformSpec? Next;
 
        #region private
        /// <summary>
        /// A PropertySpec represents information needed to fetch a property from
        /// and efficiently. Thus it represents a '.PROP' in a TransformSpec
        /// (and a transformSpec has a list of these).
        /// </summary>
        private sealed class PropertySpec
        {
            private const string CurrentActivityPropertyName = "*Activity";
            private const string EnumeratePropertyName = "*Enumerate";
 
            /// <summary>
            /// Make a new PropertySpec for a property named 'propertyName'.
            /// For convenience you can set he 'next' field to form a linked
            /// list of PropertySpecs.
            /// </summary>
            public PropertySpec(DiagnosticSourceEventSource eventSource, string propertyName, PropertySpec? next)
            {
                _eventSource = eventSource;
                _propertyName = propertyName;
 
                Next = next;
 
                // detect well-known names that are static functions
                if (_propertyName == CurrentActivityPropertyName)
                {
                    IsStatic = true;
                }
            }
 
            public bool IsStatic { get; }
 
            public PropertySpec? Next { get; }
 
            /// <summary>
            /// Given an object fetch the property that this PropertySpec represents.
            /// obj may be null when IsStatic is true, otherwise it must be non-null.
            /// </summary>
            [RequiresUnreferencedCode(DiagnosticSource.WriteRequiresUnreferencedCode)]
            public object? Fetch(object? obj)
            {
                PropertyFetch? fetch = _fetchForExpectedType;
                Debug.Assert(obj != null || IsStatic);
                Type? objType = obj?.GetType();
                if (fetch == null || fetch.Type != objType)
                {
                    _fetchForExpectedType = fetch = PropertyFetch.FetcherForProperty(_eventSource, objType, _propertyName);
                }
                object? ret = null;
                // Avoid the exception which can be thrown during accessing the object properties.
                try { ret = fetch!.Fetch(obj); } catch (Exception e) { _eventSource.Message($"Property {objType}.{_propertyName} threw the exception {e}"); }
                return ret;
            }
 
            #region private
            /// <summary>
            /// PropertyFetch is a helper class. It takes a PropertyInfo and then knows how
            /// to efficiently fetch that property from a .NET object (See Fetch method).
            /// It hides some slightly complex generic code.
            /// </summary>
            private class PropertyFetch
            {
                public PropertyFetch(Type? type)
                {
                    Type = type;
                }
 
                /// <summary>
                /// The type of the object that the property is fetched from. For well-known static methods that
                /// aren't actually property getters this will return null.
                /// </summary>
                internal Type? Type { get; }
 
                /// <summary>
                /// Create a property fetcher for a propertyName
                /// </summary>
                [RequiresUnreferencedCode(DiagnosticSource.WriteRequiresUnreferencedCode)]
                public static PropertyFetch FetcherForProperty(DiagnosticSourceEventSource eventSource, Type? type, string propertyName)
                {
                    if (propertyName == null)
                        return new PropertyFetch(type);     // returns null on any fetch.
                    if (propertyName == CurrentActivityPropertyName)
                    {
                        return new CurrentActivityPropertyFetch();
                    }
 
                    Debug.Assert(type != null, "Type should only be null for the well-known static fetchers already checked");
                    TypeInfo typeInfo = type.GetTypeInfo();
                    if (propertyName == EnumeratePropertyName)
                    {
                        // If there are multiple implementations of IEnumerable<T>, this arbitrarily uses the first one
                        foreach (Type iFaceType in typeInfo.GetInterfaces())
                        {
                            TypeInfo iFaceTypeInfo = iFaceType.GetTypeInfo();
                            if (!iFaceTypeInfo.IsGenericType ||
                                iFaceTypeInfo.GetGenericTypeDefinition() != typeof(IEnumerable<>))
                            {
                                continue;
                            }
 
                            return CreateEnumeratePropertyFetch(type, iFaceTypeInfo);
                        }
 
                        // no implementation of IEnumerable<T> found, return a null fetcher
                        eventSource.Message($"*Enumerate applied to non-enumerable type {type}");
                        return new PropertyFetch(type);
                    }
                    else
                    {
                        PropertyInfo? propertyInfo = typeInfo.GetDeclaredProperty(propertyName);
                        if (propertyInfo == null)
                        {
                            foreach (PropertyInfo pi in typeInfo.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
                            {
                                if (pi.Name == propertyName)
                                {
                                    propertyInfo = pi;
                                    break;
                                }
                            }
                        }
 
                        if (propertyInfo == null)
                        {
                            eventSource.Message($"Property {propertyName} not found on {type}. Ensure the name is spelled correctly. If you published the application with PublishTrimmed=true, ensure the property was not trimmed away.");
                            return new PropertyFetch(type);
                        }
                        // Delegate creation below is incompatible with static properties.
                        else if (propertyInfo.GetMethod?.IsStatic == true || propertyInfo.SetMethod?.IsStatic == true)
                        {
                            eventSource.Message($"Property {propertyName} is static.");
                            return new PropertyFetch(type);
                        }
 
                        return CreatePropertyFetch(typeInfo, propertyInfo);
                    }
                }
 
                [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode",
                    Justification = "MakeGenericType is only called when IsDynamicCodeSupported is true or only with ref types.")]
                private static PropertyFetch CreateEnumeratePropertyFetch(Type type, TypeInfo enumerableOfTType)
                {
                    Type elemType = enumerableOfTType.GetGenericArguments()[0];
#if NET
                    if (!RuntimeFeature.IsDynamicCodeSupported && elemType.IsValueType)
                    {
                        return new EnumeratePropertyFetch(type);
                    }
#endif
                    Type instantiatedTypedPropertyFetcher = typeof(EnumeratePropertyFetch<>)
                        .GetTypeInfo().MakeGenericType(elemType);
                    return (PropertyFetch)Activator.CreateInstance(instantiatedTypedPropertyFetcher, type)!;
                }
 
                [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode",
                    Justification = "MakeGenericType is only called when IsDynamicCodeSupported is true or only with ref types.")]
                private static PropertyFetch CreatePropertyFetch(Type type, PropertyInfo propertyInfo)
                {
#if NET
                    if (!RuntimeFeature.IsDynamicCodeSupported && (propertyInfo.DeclaringType!.IsValueType || propertyInfo.PropertyType.IsValueType))
                    {
                        return new ReflectionPropertyFetch(type, propertyInfo);
                    }
#endif
                    Type typedPropertyFetcher = type.IsValueType ?
                        typeof(ValueTypedFetchProperty<,>) : typeof(RefTypedFetchProperty<,>);
                    Type instantiatedTypedPropertyFetcher = typedPropertyFetcher.GetTypeInfo().MakeGenericType(
                        propertyInfo.DeclaringType!, propertyInfo.PropertyType);
                    return (PropertyFetch)Activator.CreateInstance(instantiatedTypedPropertyFetcher, type, propertyInfo)!;
                }
 
                /// <summary>
                /// Given an object, fetch the property that this propertyFech represents.
                /// </summary>
                public virtual object? Fetch(object? obj) { return null; }
 
                #region private
 
                private sealed class RefTypedFetchProperty<TObject, TProperty> : PropertyFetch
                {
                    public RefTypedFetchProperty(Type type, PropertyInfo property) : base(type)
                    {
                        Debug.Assert(typeof(TObject).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()));
                        _propertyFetch = (Func<TObject, TProperty>)property.GetMethod!.CreateDelegate(typeof(Func<TObject, TProperty>));
                    }
                    public override object? Fetch(object? obj)
                    {
                        Debug.Assert(obj is TObject);
                        return _propertyFetch((TObject)obj);
                    }
                    private readonly Func<TObject, TProperty> _propertyFetch;
                }
 
                private delegate TProperty StructFunc<TStruct, TProperty>(ref TStruct thisArg);
 
                // Value types methods require that the first argument is passed by reference. This requires a different delegate signature
                // from the reference type case.
                private sealed class ValueTypedFetchProperty<TStruct, TProperty> : PropertyFetch
                {
                    public ValueTypedFetchProperty(Type type, PropertyInfo property) : base(type)
                    {
                        Debug.Assert(typeof(TStruct) == type);
                        _propertyFetch = (StructFunc<TStruct, TProperty>)property.GetMethod!.CreateDelegate(typeof(StructFunc<TStruct, TProperty>));
                    }
                    public override object? Fetch(object? obj)
                    {
                        Debug.Assert(obj is TStruct);
                        // It is uncommon for property getters to mutate the struct, but if they do the change will be lost.
                        // We are calling the getter on an unboxed copy
                        TStruct structObj = (TStruct)obj;
                        return _propertyFetch(ref structObj);
                    }
                    private readonly StructFunc<TStruct, TProperty> _propertyFetch;
                }
 
#if NET
                /// <summary>
                /// A fetcher that can be used when MakeGenericType isn't available.
                /// </summary>
                private sealed class ReflectionPropertyFetch : PropertyFetch
                {
                    private readonly MethodInvoker _getterInvoker;
                    public ReflectionPropertyFetch(Type type, PropertyInfo property) : base(type)
                    {
                        _getterInvoker = MethodInvoker.Create(property.GetMethod!);
                    }
 
                    public override object? Fetch(object? obj) => _getterInvoker.Invoke(obj);
                }
 
                /// <summary>
                /// A fetcher that enumerates and formats an IEnumerable when MakeGenericType isn't available.
                /// </summary>
                private sealed class EnumeratePropertyFetch : PropertyFetch
                {
                    public EnumeratePropertyFetch(Type type) : base(type) { }
 
                    public override object? Fetch(object? obj)
                    {
                        IEnumerable? enumerable = obj as IEnumerable;
                        Debug.Assert(enumerable is not null);
 
                        // string.Join for a non-generic IEnumerable
                        IEnumerator en = enumerable.GetEnumerator();
                        using (IDisposable? disposable = en as IDisposable)
                        {
                            if (!en.MoveNext())
                            {
                                return string.Empty;
                            }
 
                            object? currentValue = en.Current;
                            string? firstString = currentValue?.ToString();
 
                            // If there's only 1 item, simply return the ToString of that
                            if (!en.MoveNext())
                            {
                                // Only one value available
                                return firstString ?? string.Empty;
                            }
 
                            var result = new ValueStringBuilder(stackalloc char[256]);
 
                            result.Append(firstString);
 
                            do
                            {
                                currentValue = en.Current;
 
                                result.Append(",");
                                if (currentValue != null)
                                {
                                    result.Append(currentValue.ToString());
                                }
                            }
                            while (en.MoveNext());
 
                            return result.ToString();
                        }
                    }
                }
#endif
 
                /// <summary>
                /// A fetcher that returns the result of Activity.Current
                /// </summary>
                private sealed class CurrentActivityPropertyFetch : PropertyFetch
                {
                    public CurrentActivityPropertyFetch() : base(null) { }
                    public override object? Fetch(object? obj)
                    {
                        return Activity.Current;
                    }
                }
 
                /// <summary>
                /// A fetcher that enumerates and formats an IEnumerable
                /// </summary>
                private sealed class EnumeratePropertyFetch<ElementType> : PropertyFetch
                {
                    public EnumeratePropertyFetch(Type type) : base(type) { }
                    public override object? Fetch(object? obj)
                    {
                        Debug.Assert(obj is IEnumerable<ElementType>);
                        return string.Join(",", (IEnumerable<ElementType>)obj);
                    }
                }
                #endregion
            }
 
            private readonly DiagnosticSourceEventSource _eventSource;
            private readonly string _propertyName;
            private volatile PropertyFetch? _fetchForExpectedType;
            #endregion
        }
 
        private readonly string _outputName = null!;
        private readonly PropertySpec? _fetches;
        #endregion
    }
 
    /// <summary>
    /// CallbackObserver is an adapter class that creates an observer (which you can pass
    /// to IObservable.Subscribe), and calls the given callback every time the 'next'
    /// operation on the IObserver happens.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    private sealed class CallbackObserver<T> : IObserver<T>
    {
        public CallbackObserver(Action<T> callback) { _callback = callback; }
 
        #region private
        public void OnCompleted() { }
        public void OnError(Exception error) { }
        public void OnNext(T value) { _callback(value); }
 
        private readonly Action<T> _callback;
        #endregion
    }
 
    // A linked list of IObservable subscriptions (which are IDisposable).
    // We use this to keep track of the DiagnosticSource subscriptions.
    // We use this linked list for thread atomicity
    private sealed class Subscriptions
    {
        public Subscriptions(IDisposable subscription, Subscriptions? next)
        {
            Subscription = subscription;
            Next = next;
        }
        public IDisposable Subscription;
        public Subscriptions? Next;
    }
 
    // Given a type generate all the implicit transforms for type (that is for every field
    // generate the spec that fetches it).
    [RequiresUnreferencedCode(DiagnosticSource.WriteRequiresUnreferencedCode)]
    private static TransformSpec? MakeImplicitTransforms(DiagnosticSourceEventSource eventSource, Type type)
    {
        TransformSpec? newSerializableArgs = null;
        TypeInfo curTypeInfo = type.GetTypeInfo();
        foreach (PropertyInfo property in curTypeInfo.GetProperties(BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
        {
            // prevent TransformSpec from attempting to implicitly transform index properties
            if (property.GetMethod == null || property.GetMethod!.GetParameters().Length > 0)
                continue;
            newSerializableArgs = new TransformSpec(eventSource, property.Name, 0, property.Name.Length, newSerializableArgs);
        }
        return Reverse(newSerializableArgs);
    }
 
    // Reverses a linked list (of TransformSpecs) in place.
    private static TransformSpec? Reverse(TransformSpec? list)
    {
        TransformSpec? ret = null;
        while (list != null)
        {
            var next = list.Next;
            list.Next = ret;
            ret = list;
            list = next;
        }
        return ret;
    }
 
    private readonly DiagnosticSourceEventSource _eventSource;
    private IDisposable? _diagnosticsListenersSubscription; // This is our subscription that listens for new Diagnostic source to appear.
    private Subscriptions? _liveSubscriptions;              // These are the subscriptions that we are currently forwarding to the EventSource.
    private readonly bool _noImplicitTransforms;                    // Listener can say they don't want implicit transforms.
    private ImplicitTransformEntry? _firstImplicitTransformsEntry; // The transform for _firstImplicitFieldsType
    private ConcurrentDictionary<Type, TransformSpec?>? _implicitTransformsTable; // If there is more than one object type for an implicit transform, they go here.
    private readonly TransformSpec? _explicitTransforms;             // payload to include because the user explicitly indicated how to fetch the field.
}