File: src\roslyn\src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Log\RoslynTelemetry.cs
Web Access
Project: Microsoft.CodeAnalysis.AnalyzerUtilities.csproj (Microsoft.CodeAnalysis.AnalyzerUtilities)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;

namespace Microsoft.CodeAnalysis.Internal.Log;

/// <summary>
/// Telemetry entry point.  Events / metrics are recorded here and fan out to the respective
/// <see cref="IEventSink"/> or <see cref="IMetricSink"/>.  When no sinks are registered calls are
/// cheap no-ops.
/// </summary>
internal sealed partial class RoslynTelemetry
{
    private static readonly AsyncLocal<RoslynTelemetry?> s_current = new();
    private static readonly RoslynTelemetry s_default = new();

    /// <summary>
    /// The registered <see cref="IEventSink"/> each event fans out to.
    /// </summary>
    private ImmutableArray<IEventSink> _eventSinks = [];

    /// <summary>
    /// next unique block id that will be given to each LogBlock
    /// </summary>
    private static int s_lastUniqueBlockId;

    public static RoslynTelemetry Current
        => s_current.Value ?? s_default;

    /// <summary>
    /// Sets the telemetry instance for the current asynchronous control flow. Disposing the result
    /// restores the previous instance.
    /// </summary>
    public static IDisposable SetCurrent(RoslynTelemetry telemetry)
    {
        Contract.ThrowIfNull(telemetry);

        var previous = s_current.Value;
        s_current.Value = telemetry;
        return new Registration(() => s_current.Value = previous);
    }

    public static bool IsDefault(RoslynTelemetry instance) => instance == s_default;

    /// <summary>
    /// Registers <paramref name="sink"/> to receive events. A sink instance may have only one active
    /// registration. Dispose the result to unregister it; a host that keeps its sinks for the life of
    /// the process can simply never dispose.
    /// </summary>
    public IDisposable AddEventSink(IEventSink sink)
    {
        ImmutableInterlocked.Update(ref _eventSinks, static (sinks, sink) => AddSink(sinks, sink), sink);
        return new Registration(() => ImmutableInterlocked.Update(ref _eventSinks, static (sinks, sink) => sinks.Remove(sink, ReferenceEqualityComparer.Instance), sink));
    }

    private static ImmutableArray<TSink> AddSink<TSink>(ImmutableArray<TSink> sinks, TSink sink)
        where TSink : class
    {
        foreach (var registeredSink in sinks)
            Contract.ThrowIfTrue(ReferenceEquals(registeredSink, sink), "The sink instance is already registered.");

        return sinks.Add(sink);
    }

    private sealed class Registration(Action unregister) : IDisposable
    {
        private Action? _unregister = unregister;

        public void Dispose()
            => Interlocked.Exchange(ref _unregister, null)?.Invoke();
    }

    /// <summary>
    /// Whether any registered sink wants <paramref name="functionId"/>. Checked before a
    /// <see cref="LogMessage"/> is constructed, so that logging costs nothing when everything is
    /// disabled.
    /// </summary>
    private bool TryGetEnabledSinks(FunctionId functionId, out ImmutableArray<IEventSink> sinks)
    {
        sinks = _eventSinks;

        foreach (var sink in sinks)
        {
            if (sink.IsEnabled(functionId))
                return true;
        }

        return false;
    }

    private static void LogToSinks(ImmutableArray<IEventSink> sinks, FunctionId functionId, LogMessage logMessage)
    {
        foreach (var sink in sinks)
        {
            if (sink.IsEnabled(functionId))
                sink.Log(functionId, logMessage);
        }
    }

    internal static class TestAccessor
    {
        /// <summary>
        /// Resets the default sinks and ambient state so one test cannot leak telemetry into the next.
        /// </summary>
        public static void RemoveAllSinks()
        {
            ImmutableInterlocked.InterlockedExchange(ref s_default._eventSinks, []);
            ImmutableInterlocked.InterlockedExchange(ref s_default._metricSinks, []);
            s_current.Value = null;
        }
    }

    /// <summary>
    /// log a specific event with a simple context message which should be very cheap to create
    /// </summary>
    public void Log(FunctionId functionId, string? message = null, LogLevel logLevel = LogLevel.Debug)
    {
        if (TryGetEnabledSinks(functionId, out var sinks))
        {
            var logMessage = LogMessage.Create(message ?? "", logLevel: logLevel);
            LogToSinks(sinks, functionId, logMessage);

            logMessage.Free();
        }
    }

    /// <summary>
    /// log a specific event with a context message that will only be created when it is needed.
    /// the messageGetter should be cheap to create. in another word, it shouldn't capture any locals
    /// </summary>
    public void Log(FunctionId functionId, Func<string> messageGetter, LogLevel logLevel = LogLevel.Debug)
    {
        if (TryGetEnabledSinks(functionId, out var sinks))
        {
            var logMessage = LogMessage.Create(messageGetter, logLevel);
            LogToSinks(sinks, functionId, logMessage);

            logMessage.Free();
        }
    }

    /// <summary>
    /// log a specific event with a context message that requires some arguments to be created when requested.
    /// given arguments will be passed to the messageGetter so that it can create the context message without requiring lifted locals
    /// </summary>
    public void Log<TArg>(FunctionId functionId, Func<TArg, string> messageGetter, TArg arg, LogLevel logLevel = LogLevel.Debug)
    {
        if (TryGetEnabledSinks(functionId, out var sinks))
        {
            var logMessage = LogMessage.Create(messageGetter, arg, logLevel);
            LogToSinks(sinks, functionId, logMessage);
            logMessage.Free();
        }
    }

    /// <summary>
    /// log a specific event with a context message that requires some arguments to be created when requested.
    /// given arguments will be passed to the messageGetter so that it can create the context message without requiring lifted locals
    /// </summary>
    public void Log<TArg0, TArg1>(FunctionId functionId, Func<TArg0, TArg1, string> messageGetter, TArg0 arg0, TArg1 arg1, LogLevel logLevel = LogLevel.Debug)
    {
        if (TryGetEnabledSinks(functionId, out var sinks))
        {
            var logMessage = LogMessage.Create(messageGetter, arg0, arg1, logLevel);
            LogToSinks(sinks, functionId, logMessage);
            logMessage.Free();
        }
    }

    /// <summary>
    /// log a specific event with a context message that requires some arguments to be created when requested.
    /// given arguments will be passed to the messageGetter so that it can create the context message without requiring lifted locals
    /// </summary>
    public void Log<TArg0, TArg1, TArg2>(FunctionId functionId, Func<TArg0, TArg1, TArg2, string> messageGetter, TArg0 arg0, TArg1 arg1, TArg2 arg2, LogLevel logLevel = LogLevel.Debug)
    {
        if (TryGetEnabledSinks(functionId, out var sinks))
        {
            var logMessage = LogMessage.Create(messageGetter, arg0, arg1, arg2, logLevel);
            LogToSinks(sinks, functionId, logMessage);
            logMessage.Free();
        }
    }

    /// <summary>
    /// log a specific event with a context message that requires some arguments to be created when requested.
    /// given arguments will be passed to the messageGetter so that it can create the context message without requiring lifted locals
    /// </summary>
    public void Log<TArg0, TArg1, TArg2, TArg3>(FunctionId functionId, Func<TArg0, TArg1, TArg2, TArg3, string> messageGetter, TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, LogLevel logLevel = LogLevel.Debug)
    {
        if (TryGetEnabledSinks(functionId, out var sinks))
        {
            var logMessage = LogMessage.Create(messageGetter, arg0, arg1, arg2, arg3, logLevel);
            LogToSinks(sinks, functionId, logMessage);
            logMessage.Free();
        }
    }

    /// <summary>
    /// log a specific event with a context message.
    /// </summary>
    public void Log(FunctionId functionId, LogMessage logMessage)
    {
        if (TryGetEnabledSinks(functionId, out var sinks))
        {
            LogToSinks(sinks, functionId, logMessage);
        }

        // Freed unconditionally: the caller handed over ownership, so returning it to the pool cannot
        // depend on whether a sink happened to be listening.
        logMessage.Free();
    }

    /// <summary>
    /// return next unique pair id
    /// </summary>
    private static int GetNextUniqueBlockId()
        => Interlocked.Increment(ref s_lastUniqueBlockId);

    /// <summary>
    /// simplest way to log a start and end pair
    /// </summary>
    public IDisposable LogBlock(FunctionId functionId, CancellationToken token, LogLevel logLevel = LogLevel.Trace)
        => LogBlock(functionId, string.Empty, token, logLevel);

    /// <summary>
    /// simplest way to log a start and end pair with a simple context message which should be very cheap to create
    /// </summary>
    public IDisposable LogBlock(FunctionId functionId, string? message, CancellationToken token, LogLevel logLevel = LogLevel.Trace)
        => TryGetEnabledSinks(functionId, out var sinks)
            ? CreateLogBlock(sinks, functionId, LogMessage.Create(message ?? "", logLevel), GetNextUniqueBlockId(), token)
            : EmptyLogBlock.Instance;

    /// <summary>
    /// log a start and end pair with a context message that will only be created when it is needed.
    /// the messageGetter should be cheap to create. in another word, it shouldn't capture any locals
    /// </summary>
    public IDisposable LogBlock(FunctionId functionId, Func<string> messageGetter, CancellationToken token, LogLevel logLevel = LogLevel.Trace)
        => TryGetEnabledSinks(functionId, out var sinks)
            ? CreateLogBlock(sinks, functionId, LogMessage.Create(messageGetter, logLevel), GetNextUniqueBlockId(), token)
            : EmptyLogBlock.Instance;

    /// <summary>
    /// log a start and end pair with a context message that requires some arguments to be created when requested.
    /// given arguments will be passed to the messageGetter so that it can create the context message without requiring lifted locals
    /// </summary>
    public IDisposable LogBlock<TArg>(FunctionId functionId, Func<TArg, string> messageGetter, TArg arg, CancellationToken token, LogLevel logLevel = LogLevel.Trace)
        => TryGetEnabledSinks(functionId, out var sinks)
            ? CreateLogBlock(sinks, functionId, LogMessage.Create(messageGetter, arg, logLevel), GetNextUniqueBlockId(), token)
            : EmptyLogBlock.Instance;

    /// <summary>
    /// log a start and end pair with a context message that requires some arguments to be created when requested.
    /// given arguments will be passed to the messageGetter so that it can create the context message without requiring lifted locals
    /// </summary>
    public IDisposable LogBlock<TArg0, TArg1>(FunctionId functionId, Func<TArg0, TArg1, string> messageGetter, TArg0 arg0, TArg1 arg1, CancellationToken token, LogLevel logLevel = LogLevel.Trace)
        => TryGetEnabledSinks(functionId, out var sinks)
            ? CreateLogBlock(sinks, functionId, LogMessage.Create(messageGetter, arg0, arg1, logLevel), GetNextUniqueBlockId(), token)
            : EmptyLogBlock.Instance;

    /// <summary>
    /// log a start and end pair with a context message that requires some arguments to be created when requested.
    /// given arguments will be passed to the messageGetter so that it can create the context message without requiring lifted locals
    /// </summary>
    public IDisposable LogBlock<TArg0, TArg1, TArg2>(FunctionId functionId, Func<TArg0, TArg1, TArg2, string> messageGetter, TArg0 arg0, TArg1 arg1, TArg2 arg2, CancellationToken token, LogLevel logLevel = LogLevel.Trace)
        => TryGetEnabledSinks(functionId, out var sinks)
            ? CreateLogBlock(sinks, functionId, LogMessage.Create(messageGetter, arg0, arg1, arg2, logLevel), GetNextUniqueBlockId(), token)
            : EmptyLogBlock.Instance;

    /// <summary>
    /// log a start and end pair with a context message that requires some arguments to be created when requested.
    /// given arguments will be passed to the messageGetter so that it can create the context message without requiring lifted locals
    /// </summary>
    public IDisposable LogBlock<TArg0, TArg1, TArg2, TArg3>(FunctionId functionId, Func<TArg0, TArg1, TArg2, TArg3, string> messageGetter, TArg0 arg0, TArg1 arg1, TArg2 arg2, TArg3 arg3, CancellationToken token, LogLevel logLevel = LogLevel.Trace)
        => TryGetEnabledSinks(functionId, out var sinks)
            ? CreateLogBlock(sinks, functionId, LogMessage.Create(messageGetter, arg0, arg1, arg2, arg3, logLevel), GetNextUniqueBlockId(), token)
            : EmptyLogBlock.Instance;

    /// <summary>
    /// log a start and end pair with a context message. Takes ownership of <paramref name="logMessage"/>
    /// whether or not anything is listening.
    /// </summary>
    public IDisposable LogBlock(FunctionId functionId, LogMessage logMessage, CancellationToken token)
    {
        if (TryGetEnabledSinks(functionId, out var sinks))
            return CreateLogBlock(sinks, functionId, logMessage, GetNextUniqueBlockId(), token);

        logMessage.Free();
        return EmptyLogBlock.Instance;
    }
}