File: Telemetry\TelemetryManager.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.Tool.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Cli.Utils;
using Azure.Monitor.OpenTelemetry.Exporter;
using Microsoft.Extensions.Configuration;
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
 
namespace Aspire.Cli.Telemetry;
 
/// <summary>
/// Manages OpenTelemetry TracerProvider instances for the CLI.
/// Maintains separate providers for Azure Monitor (production telemetry) and debug exporters (OTLP/console).
/// </summary>
internal sealed class TelemetryManager
{
    // Remote export connection string for Application Insights. Intentionally hard-coded.
    private const string ApplicationInsightsConnectionString = "InstrumentationKey=e39510fc-95a1-423d-9f33-6121bf0d2113;IngestionEndpoint=https://centralus-2.in.applicationinsights.azure.com/;LiveEndpoint=https://centralus.livediagnostics.monitor.azure.com/;ApplicationId=4d8bb9db-b7ab-49f9-978b-80ae1e83f6da";
 
#if DEBUG
    // No timeout in debug builds
    private const int ShutDownTimeoutMilliseconds = -1;
#else
    // Chosen to provide time to send remaining telemetry without noticeably delaying exit.
    private const int ShutDownTimeoutMilliseconds = 100;
#endif
 
    private readonly TracerProvider? _azureMonitorProvider;
#if DEBUG
    private readonly TracerProvider? _diagnosticProvider;
#endif
 
    /// <summary>
    /// Initializes a new instance of the <see cref="TelemetryManager"/> class.
    /// </summary>
    /// <param name="configuration">The configuration to read telemetry settings from.</param>
    public TelemetryManager(IConfiguration configuration)
    {
        var telemetryOptOut = configuration.GetBool(AspireCliTelemetry.TelemetryOptOutConfigKey, defaultValue: false);
 
#if DEBUG
        var useOtlpExporter = !string.IsNullOrEmpty(configuration[AspireCliTelemetry.OtlpExporterEndpointConfigKey]);
        var consoleExporterLevel = configuration.GetEnum<ConsoleExporterLevel>(AspireCliTelemetry.ConsoleExporterLevelConfigKey, defaultValue: null);
#else
        var useOtlpExporter = false;
        ConsoleExporterLevel? consoleExporterLevel = null;
#endif
 
        // Don't create any providers if nothing is enabled
        if (telemetryOptOut && !useOtlpExporter && consoleExporterLevel is null)
        {
            return;
        }
 
        var resource = ResourceBuilder.CreateDefault().AddService(
            serviceName: "aspire-cli",
            serviceVersion: VersionHelper.GetDefaultTemplateVersion());
 
        // Create Azure Monitor provider if connection string is provided.
        // The Azure Monitor only exports telemetry from the Reported activity source.
        if (!telemetryOptOut)
        {
            var azureMonitorBuilder = Sdk.CreateTracerProviderBuilder()
                .AddSource(AspireCliTelemetry.ReportedActivitySourceName)
                .SetResourceBuilder(resource)
                .AddAzureMonitorTraceExporter(o =>
                {
                    o.ConnectionString = ApplicationInsightsConnectionString;
                    o.EnableLiveMetrics = false;
                    o.StorageDirectory = GetTelemetryStoragePath();
                });
 
#if DEBUG
            if (consoleExporterLevel == ConsoleExporterLevel.Reported)
            {
                azureMonitorBuilder.AddConsoleExporter();
            }
#endif
 
            _azureMonitorProvider = azureMonitorBuilder.Build();
        }
 
#if DEBUG
        // Create diagnostic provider if any diagnostic exporter is enabled.
        // The diagnostic provider exports diagnostic telemetry.
        if (useOtlpExporter || consoleExporterLevel == ConsoleExporterLevel.Diagnostic)
        {
            var diagnosticBuilder = Sdk.CreateTracerProviderBuilder()
                .AddSource(AspireCliTelemetry.DiagnosticsActivitySourceName)
                .AddSource(AspireCliTelemetry.ReportedActivitySourceName)
                .SetResourceBuilder(resource);
 
            if (consoleExporterLevel == ConsoleExporterLevel.Diagnostic)
            {
                diagnosticBuilder.AddConsoleExporter();
            }
 
            if (useOtlpExporter)
            {
                diagnosticBuilder.AddOtlpExporter();
            }
 
            _diagnosticProvider = diagnosticBuilder.Build();
        }
#endif
    }
 
    /// <summary>
    /// Gets whether Azure Monitor telemetry is enabled.
    /// </summary>
    public bool HasAzureMonitor => _azureMonitorProvider is not null;
 
#if DEBUG
    /// <summary>
    /// Gets whether any diagnostic exporter is enabled.
    /// </summary>
    public bool HasDiagnosticProvider => _diagnosticProvider is not null;
#endif
 
    /// <summary>
    /// Shuts down the telemetry providers, flushing any pending telemetry.
    /// </summary>
    public Task ShutdownAsync()
    {
        return Task.Run(() =>
        {
            _azureMonitorProvider?.Shutdown(ShutDownTimeoutMilliseconds);
#if DEBUG
            _diagnosticProvider?.Shutdown(ShutDownTimeoutMilliseconds);
#endif
        });
    }
 
    private static string GetTelemetryStoragePath()
    {
        var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
        return Path.Combine(homeDirectory, ".aspire", "cli", "telemetrystorage");
    }
}