File: Telemetry\TelemetryTagsSource.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 System.Diagnostics;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Cli.Telemetry;
 
/// <summary>
/// Holds the background task that calculates default telemetry tags (machine ID, OS info, etc.).
/// Shared between <see cref="AspireCliTelemetry"/> (which starts the calculation) and
/// <see cref="CliTagEnrichmentProcessor"/> (which applies the tags to activities before export).
/// </summary>
internal sealed class TelemetryTagsSource
{
    private volatile Task<IReadOnlyList<KeyValuePair<string, object?>>>? _tagsTask;
    private readonly ILogger<TelemetryTagsSource> _logger;
 
    public TelemetryTagsSource(ILogger<TelemetryTagsSource> logger)
    {
        _logger = logger;
    }
 
    /// <summary>
    /// Gets the task that resolves to the calculated tags. Returns an empty list if
    /// calculation has not been started yet.
    /// </summary>
    public Task<IReadOnlyList<KeyValuePair<string, object?>>> TagsTask =>
        _tagsTask ?? Task.FromResult<IReadOnlyList<KeyValuePair<string, object?>>>(Array.Empty<KeyValuePair<string, object?>>());
 
    /// <summary>
    /// Returns the resolved tags, blocking if the background calculation has not yet completed.
    /// </summary>
    public IReadOnlyList<KeyValuePair<string, object?>> GetResolvedTags()
    {
        // This is called from an OTEL processor which isn't async, so we block here if the background calculation hasn't completed yet.
        // This should be rare in practice since the tags are calculated at startup and cached for the lifetime of the process.
        //
        // If blocking here is a problem then I think the best option is to make StartActivity on AspireCliTelemetry return activities that are
        // wrapped in a proxy that implements IAsyncDisposable that sets tags in dispose.
        // Activity events also need the tags so their information could be added to the proxy and added to the activity in DisposeAsync with all tags.
 
        var tagsTask = TagsTask;
        if (tagsTask.IsCompletedSuccessfully)
        {
            return tagsTask.Result;
        }
 
        var stopwatch = Stopwatch.StartNew();
        var tags = tagsTask.GetAwaiter().GetResult();
        stopwatch.Stop();
 
        _logger.LogDebug("TelemetryTagsSource: blocked {ElapsedMilliseconds}ms waiting for telemetry tags to be calculated.", stopwatch.ElapsedMilliseconds);
 
        return tags;
    }
 
    /// <summary>
    /// Starts the background tag calculation. Only the first call takes effect; subsequent
    /// calls are ignored.
    /// </summary>
    public void StartCalculation(Func<Task<IReadOnlyList<KeyValuePair<string, object?>>>> factory)
    {
        _tagsTask ??= Task.Run(factory);
    }
}