File: Profiling\ProfileCaptureEnvironment.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.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.Telemetry;
using Aspire.Hosting;
 
namespace Aspire.Cli.Profiling;
 
internal sealed class ProfileCaptureEnvironment : IDisposable
{
    private readonly Dictionary<string, string?> _previousValues;
 
    private ProfileCaptureEnvironment(Dictionary<string, string?> previousValues)
    {
        _previousValues = previousValues;
    }
 
    public static ProfileCaptureEnvironment Apply(ProfileCaptureOptions options)
    {
        ArgumentNullException.ThrowIfNull(options);
 
        var values = CreateValues(options);
 
        // The CLI telemetry pipeline is configured from ambient process environment at startup.
        // Mutating process state here is intentional because this runs before DI creates
        // TelemetryManager; child AppHost processes get an explicit copy via AddCurrentToEnvironment.
        // Environment variable names are case-insensitive on Windows, so use the matching comparer
        // to avoid storing two restore entries for what the OS treats as the same variable.
        var previousValues = new Dictionary<string, string?>(StringComparers.EnvironmentVariableName);
        foreach (var (name, value) in values)
        {
            previousValues[name] = Environment.GetEnvironmentVariable(name);
            Environment.SetEnvironmentVariable(name, value);
        }
 
        return new ProfileCaptureEnvironment(previousValues);
    }
 
    public static void AddCurrentToEnvironment(IDictionary<string, string> environmentVariables)
    {
        ArgumentNullException.ThrowIfNull(environmentVariables);
 
        foreach (var name in CreateValues().Keys)
        {
            if (Environment.GetEnvironmentVariable(name) is { } value)
            {
                environmentVariables[name] = value;
            }
        }
    }
 
    public void Dispose()
    {
        foreach (var (name, value) in _previousValues)
        {
            Environment.SetEnvironmentVariable(name, value);
        }
    }
 
    private static Dictionary<string, string?> CreateValues(ProfileCaptureOptions? options = null)
    {
        var otlpGrpcUrl = options?.OtlpGrpcUrl;
        var sessionId = options?.SessionId;
 
        // Set standard OTEL variables for both this CLI process and the AppHost process it launches.
        // AppHosts support these non-prefixed variables directly, so we do not need separate
        // ASPIRE_OTEL_* copies for the profiling path.
        // OTEL env var conventions: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/
        return new Dictionary<string, string?>(StringComparers.EnvironmentVariableName)
        {
            [AspireCliTelemetry.TelemetryOptOutConfigKey] = "true",
            [KnownConfigNames.ProfilingEnabled] = "true",
            [KnownConfigNames.Legacy.StartupProfilingEnabled] = "true",
            [KnownConfigNames.ProfilingSessionId] = sessionId,
            [KnownConfigNames.Legacy.StartupOperationId] = sessionId,
            [KnownOtelConfigNames.ExporterOtlpEndpoint] = otlpGrpcUrl,
            [KnownOtelConfigNames.ExporterOtlpProtocol] = "grpc",
            [KnownOtelConfigNames.BspScheduleDelay] = "1000"
        };
    }
}