File: Telemetry\OpenTelemetryManager.cs
Web Access
Project: ..\..\..\src\Framework\Microsoft.Build.Framework.csproj (Microsoft.Build.Framework)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#if NETFRAMEWORK
using Microsoft.VisualStudio.OpenTelemetry.ClientExtensions;
using Microsoft.VisualStudio.OpenTelemetry.ClientExtensions.Exporters;
using Microsoft.VisualStudio.OpenTelemetry.Collector.Interfaces;
using Microsoft.VisualStudio.OpenTelemetry.Collector.Settings;
using OpenTelemetry;
using OpenTelemetry.Trace;
#endif
using System;
using System.Diagnostics;
using System.Threading;
using System.Globalization;
using System.Runtime.CompilerServices;
 
namespace Microsoft.Build.Framework.Telemetry
{
 
    /// <summary>
    /// Singleton class for configuring and managing the telemetry infrastructure with System.Diagnostics.Activity,
    /// OpenTelemetry SDK, and VS OpenTelemetry Collector.
    /// </summary>
    internal class OpenTelemetryManager
    {
        // Lazy<T> provides thread-safe lazy initialization.
        private static readonly Lazy<OpenTelemetryManager> s_instance =
            new Lazy<OpenTelemetryManager>(() => new OpenTelemetryManager(), LazyThreadSafetyMode.ExecutionAndPublication);
 
        /// <summary>
        /// Globally accessible instance of <see cref="OpenTelemetryManager"/>.
        /// </summary>
        public static OpenTelemetryManager Instance => s_instance.Value;
 
        private TelemetryState _telemetryState = TelemetryState.Uninitialized;
        private readonly object _initializeLock = new();
        private double _sampleRate = TelemetryConstants.DefaultSampleRate;
 
#if NETFRAMEWORK
        private TracerProvider? _tracerProvider;
        private IOpenTelemetryCollector? _collector;
#endif
 
        /// <summary>
        /// Optional activity source for MSBuild or other telemetry usage.
        /// </summary>
        public MSBuildActivitySource? DefaultActivitySource { get; private set; }
 
        private OpenTelemetryManager()
        {
        }
 
        /// <summary>
        /// Initializes the telemetry infrastructure. Multiple invocations are no-op, thread-safe.
        /// </summary>
        /// <param name="isStandalone">Differentiates between executing as MSBuild.exe or from VS/API.</param>
        public void Initialize(bool isStandalone)
        {
            // for lock free early exit
            if (_telemetryState != TelemetryState.Uninitialized)
            {
                return;
            }
 
            lock (_initializeLock)
            {
                // for correctness
                if (_telemetryState != TelemetryState.Uninitialized)
                {
                    return;
                }
 
                if (IsOptOut())
                {
                    _telemetryState = TelemetryState.OptOut;
                    return;
                }
 
                // TODO: temporary until we have green light to enable telemetry perf-wise
                if (!IsOptIn())
                {
                    _telemetryState = TelemetryState.Unsampled;
                    return;
                }
 
                if (!IsSampled())
                {
                    _telemetryState = TelemetryState.Unsampled;
                    return;
                }
 
                InitializeActivitySources();
            }
#if NETFRAMEWORK
            try
            {
                InitializeTracerProvider();
 
                // TODO: Enable commented logic when Collector is present in VS
                // if (isStandalone)
                InitializeCollector();
 
                // }
            }
            catch (Exception ex) when (ex is System.IO.FileNotFoundException or System.IO.FileLoadException)
            {
                // catch exceptions from loading the OTel SDK or Collector to maintain usability of Microsoft.Build.Framework package in our and downstream tests in VS.
                _telemetryState = TelemetryState.Unsampled;
                return;
            }
#endif
        }
 
        [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads
        private void InitializeActivitySources()
        {
            _telemetryState = TelemetryState.TracerInitialized;
            DefaultActivitySource = new MSBuildActivitySource(TelemetryConstants.DefaultActivitySourceNamespace, _sampleRate);
        }
 
#if NETFRAMEWORK
        /// <summary>
        /// Initializes the OpenTelemetry SDK TracerProvider with VS default exporter settings.
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads
        private void InitializeTracerProvider()
        {
            var exporterSettings = OpenTelemetryExporterSettingsBuilder
                .CreateVSDefault(TelemetryConstants.VSMajorVersion)
                .Build();
 
            TracerProviderBuilder tracerProviderBuilder = Sdk
                .CreateTracerProviderBuilder()
                                // this adds listeners to ActivitySources with the prefix "Microsoft.VisualStudio.OpenTelemetry."
                                .AddVisualStudioDefaultTraceExporter(exporterSettings);
 
            _tracerProvider = tracerProviderBuilder.Build();
            _telemetryState = TelemetryState.ExporterInitialized;
        }
 
        /// <summary>
        /// Initializes the VS OpenTelemetry Collector with VS default settings.
        /// </summary>
        [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads
        private void InitializeCollector()
        {
            IOpenTelemetryCollectorSettings collectorSettings = OpenTelemetryCollectorSettingsBuilder
                .CreateVSDefault(TelemetryConstants.VSMajorVersion)
                .Build();
 
            _collector = OpenTelemetryCollectorProvider.CreateCollector(collectorSettings);
            _collector.StartAsync().GetAwaiter().GetResult();
 
            _telemetryState = TelemetryState.CollectorInitialized;
        }
#endif
        [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads
        private void ForceFlushInner()
        {
#if NETFRAMEWORK
            _tracerProvider?.ForceFlush();
#endif
        }
 
        /// <summary>
        /// Flush the telemetry in TracerProvider/Exporter.
        /// </summary>
        public void ForceFlush()
        {
            if (ShouldBeCleanedUp())
            {
                ForceFlushInner();
            }
        }
 
        // to avoid assembly loading OpenTelemetry in tests
        [MethodImpl(MethodImplOptions.NoInlining)] // avoid assembly loads
        private void ShutdownInner()
        {
#if NETFRAMEWORK
            _tracerProvider?.Shutdown();
            // Dispose stops the collector, with a default drain timeout of 10s
            _collector?.Dispose();
#endif
        }
 
        /// <summary>
        /// Shuts down the telemetry infrastructure.
        /// </summary>
        public void Shutdown()
        {
            lock (_initializeLock)
            {
                if (ShouldBeCleanedUp())
                {
                    ShutdownInner();
                }
 
                _telemetryState = TelemetryState.Disposed;
            }
        }
 
        /// <summary>
        /// Determines if the user has explicitly opted out of telemetry.
        /// </summary>
        private bool IsOptOut() => Traits.Instance.FrameworkTelemetryOptOut || Traits.Instance.SdkTelemetryOptOut || !ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_14);
 
        /// <summary>
        /// TODO: Temporary until perf of loading OTel is agreed to in VS.
        /// </summary>
        private bool IsOptIn() => !IsOptOut() && (Traits.Instance.TelemetryOptIn || Traits.Instance.TelemetrySampleRateOverride.HasValue);
 
        /// <summary>
        /// Determines if telemetry should be initialized based on sampling and environment variable overrides.
        /// </summary>
        private bool IsSampled()
        {
            double? overrideRate = Traits.Instance.TelemetrySampleRateOverride;
            if (overrideRate.HasValue)
            {
                _sampleRate = overrideRate.Value;
            }
            else
            {
#if !NETFRAMEWORK
                // In core, OTel infrastructure is not initialized by default.
                return false;
#endif
            }
 
            // Simple random sampling, this method is called once, no need to save the Random instance.
            Random random = new();
            return random.NextDouble() < _sampleRate;
        }
 
        private bool ShouldBeCleanedUp() => _telemetryState == TelemetryState.CollectorInitialized || _telemetryState == TelemetryState.ExporterInitialized;
 
        internal bool IsActive() => _telemetryState == TelemetryState.TracerInitialized || _telemetryState == TelemetryState.CollectorInitialized || _telemetryState == TelemetryState.ExporterInitialized;
 
        /// <summary>
        /// State of the telemetry infrastructure.
        /// </summary>
        internal enum TelemetryState
        {
            /// <summary>
            /// Initial state.
            /// </summary>
            Uninitialized,
 
            /// <summary>
            /// Opt out of telemetry.
            /// </summary>
            OptOut,
 
            /// <summary>
            /// Run not sampled for telemetry.
            /// </summary>
            Unsampled,
 
            /// <summary>
            /// For core hook, ActivitySource is created.
            /// </summary>
            TracerInitialized,
 
            /// <summary>
            /// For VS scenario with a collector. ActivitySource, OTel TracerProvider are created.
            /// </summary>
            ExporterInitialized,
 
            /// <summary>
            /// For standalone, ActivitySource, OTel TracerProvider, VS OpenTelemetry Collector are created.
            /// </summary>
            CollectorInitialized,
 
            /// <summary>
            /// End state.
            /// </summary>
            Disposed
        }
    }
}