File: parent\dotnet\Telemetry\InternalMicrosoft\InternalMicrosoftTelemetry.cs
Web Access
Project: dotnet-aot.csproj (dotnet-aot)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using Microsoft.DotNet.Cli.InternalMicrosoft;
using OpenTelemetry;

namespace Microsoft.DotNet.Cli.Telemetry;

/// <summary>
/// Starts detection once and supplies the resolved classification to CLI telemetry.
/// <see cref="TelemetryClient"/> owns one instance for the process lifetime.
/// </summary>
internal sealed class InternalMicrosoftTelemetry
{
    private static readonly TimeSpan s_completionTimeout = TimeSpan.FromSeconds(20);

    private readonly object _lock = new();
    private Task? _completionTask;
    private KeyValuePair<string, object?>[] _resolvedTags = [];

    /// <summary>
    /// Starts detection once. Later calls keep the first detector task.
    /// </summary>
    public void Start(
        IInternalMicrosoftDetector detector,
        CancellationToken cancellationToken)
    {
        lock (_lock)
        {
            _completionTask ??= RunAsync(detector, cancellationToken);
        }
    }

    /// <summary>
    /// Waits for a bounded time so shutdown can enrich the final activities.
    /// </summary>
    public void WaitForCompletion()
    {
        Task? completionTask;
        lock (_lock)
        {
            completionTask = _completionTask;
        }

        if (completionTask is null)
        {
            return;
        }

        try
        {
            completionTask.Wait(s_completionTimeout);
        }
        catch
        {
            // Telemetry must never affect command completion.
        }
    }

    /// <summary>
    /// Gets the cached, privacy-filtered tags from the completed detector run.
    /// </summary>
    public IReadOnlyList<KeyValuePair<string, object?>> GetResolvedTags() =>
        Volatile.Read(ref _resolvedTags);

    private static KeyValuePair<string, object?>[] CreateResolvedTags(InternalMicrosoftDetectionResult result)
    {
        var tags = new List<KeyValuePair<string, object?>>
        {
            new(InternalMicrosoftTelemetryConstants.IsInternal, result.IsInternalMicrosoft)
        };

        if (result.IsInternalMicrosoft && !string.IsNullOrEmpty(result.Source))
        {
            tags.Add(new(InternalMicrosoftTelemetryConstants.Source, result.Source));
        }

        if (!result.IsCIEnvironment && result.IsInternalMicrosoft && !string.IsNullOrEmpty(result.Alias))
        {
            tags.Add(new(InternalMicrosoftTelemetryConstants.Alias, result.Alias));
        }

        if (!result.IsCIEnvironment && result.IsInternalMicrosoft && !string.IsNullOrEmpty(result.Domain))
        {
            tags.Add(new(InternalMicrosoftTelemetryConstants.Domain, result.Domain));
        }

        return [.. tags];
    }

    private async Task RunAsync(
        IInternalMicrosoftDetector detector,
        CancellationToken cancellationToken)
    {
        InternalMicrosoftDetectionResult result;
        try
        {
            result = await detector.IsInternalMicrosoftMachineAsync(cancellationToken).ConfigureAwait(false);
        }
        catch
        {
            result = new InternalMicrosoftDetectionResult(
                IsInternalMicrosoft: false,
                Source: null,
                Alias: null,
                Domain: null,
                IsCIEnvironment: false,
                Outcome: InternalMicrosoftDetectorOutcome.Failed,
                CacheStatus: InternalMicrosoftDetectorCacheStatus.Miss,
                Duration: TimeSpan.Zero,
                ProbeDiagnostics: []);
        }

        Volatile.Write(ref _resolvedTags, CreateResolvedTags(result));
    }
}

/// <summary>
/// Adds the resolved classification to activities when OpenTelemetry ends them.
/// A tracer provider owns this processor for the provider lifetime.
/// </summary>
internal sealed class InternalMicrosoftTelemetryProcessor(InternalMicrosoftTelemetry telemetry) : BaseProcessor<Activity>
{
    public override void OnEnd(Activity activity)
    {
        foreach (var tag in telemetry.GetResolvedTags())
        {
            activity.SetTag(tag.Key, tag.Value);
        }
    }
}

/// <summary>
/// Defines activity names and tag keys shared by the telemetry adapter and its tests.
/// </summary>
internal static class InternalMicrosoftTelemetryConstants
{
    public const string IsInternal = "dotnet.cli.is_microsoft_internal";
    public const string Source = "dotnet.cli.microsoft_internal_source";
    public const string Alias = "dotnet.cli.microsoft_internal_alias";
    public const string Domain = "dotnet.cli.microsoft_internal_domain";
}