| File: Telemetry\TelemetryFixture.cs | Web Access |
| Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.Tests) |
// 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 Aspire.Cli.Telemetry; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; namespace Aspire.Cli.Tests.Telemetry; /// <summary> /// A test fixture that sets up an <see cref="ActivityListener"/> and <see cref="AspireCliTelemetry"/> /// for testing telemetry-related functionality. /// </summary> internal sealed class TelemetryFixture : IDisposable { private readonly ActivityListener _listener; /// <summary> /// Creates a new telemetry fixture with unique activity source names. /// </summary> /// <param name="machineInfoProvider">Optional machine information provider. Uses a default test provider if not specified.</param> /// <param name="ciEnvironmentDetector">Optional CI environment detector. Uses a default test detector if not specified.</param> /// <param name="codingAgentDetector">Optional coding agent detector. Uses a default test detector if not specified.</param> /// <param name="internalMicrosoftDetector">Optional internal Microsoft detector. Uses a default test detector if not specified.</param> /// <param name="logger">Optional logger. Uses <see cref="NullLogger"/> if not specified.</param> /// <param name="sampleResult">The sampling result for the activity listener. Defaults to <see cref="ActivitySamplingResult.AllDataAndRecorded"/>.</param> /// <param name="executionContext">Optional CLI execution context. Defaults to a local-identity context so the telemetry's required context is always satisfied.</param> /// <param name="telemetryConfiguration">Optional telemetry configuration. Uses reported telemetry defaults if not specified.</param> /// <param name="initialize">Whether to initialize telemetry and wait for completion before returning.</param> public TelemetryFixture( IMachineInformationProvider? machineInfoProvider = null, ICIEnvironmentDetector? ciEnvironmentDetector = null, ICodingAgentDetector? codingAgentDetector = null, IInternalMicrosoftDetector? internalMicrosoftDetector = null, ILogger<AspireCliTelemetry>? logger = null, ActivitySamplingResult sampleResult = ActivitySamplingResult.AllDataAndRecorded, CliExecutionContext? executionContext = null, TelemetryConfiguration? telemetryConfiguration = null, bool initialize = true) { ReportedSourceName = $"Test.{Path.GetRandomFileName()}"; DiagnosticsSourceName = $"Test.{Path.GetRandomFileName()}"; machineInfoProvider ??= new TestMachineInformationProvider(); ciEnvironmentDetector ??= new TestCIEnvironmentDetector(); codingAgentDetector ??= new TestCodingAgentDetector(); internalMicrosoftDetector ??= new TestInternalMicrosoftDetector(); logger ??= NullLogger<AspireCliTelemetry>.Instance; executionContext ??= Utils.TestExecutionContextHelper.CreateExecutionContext(new DirectoryInfo(AppContext.BaseDirectory)); TagsSource = new TelemetryTagsSource(NullLogger<TelemetryTagsSource>.Instance); // Simulate CliTagEnrichmentProcessor behavior: in production, tags are added // in OnEnd before export. Tests assert on live activities before they // stop, so we add tags in ActivityStarted instead to make them visible immediately. _listener = new ActivityListener { ShouldListenTo = source => source.Name == ReportedSourceName || source.Name == DiagnosticsSourceName, Sample = (ref ActivityCreationOptions<ActivityContext> _) => sampleResult, ActivityStarted = activity => { if (TagsSource.TagsTask is { IsCompletedSuccessfully: true } tagsTask) { foreach (var tag in tagsTask.Result) { if (activity.OperationName == TelemetryConstants.Activities.InternalMicrosoftDetector && tag.Key is TelemetryConstants.Tags.InternalMicrosoftAlias or TelemetryConstants.Tags.InternalMicrosoftDomain) { continue; } activity.SetTag(tag.Key, tag.Value); } } }, ActivityStopped = activity => CapturedActivity = activity }; ActivitySource.AddActivityListener(_listener); Telemetry = telemetryConfiguration is null ? new AspireCliTelemetry(logger, machineInfoProvider, ciEnvironmentDetector, codingAgentDetector, internalMicrosoftDetector, ReportedSourceName, DiagnosticsSourceName, executionContext, TagsSource) : new AspireCliTelemetry(logger, machineInfoProvider, ciEnvironmentDetector, codingAgentDetector, internalMicrosoftDetector, telemetryConfiguration, ReportedSourceName, DiagnosticsSourceName, executionContext, TagsSource); if (initialize) { Telemetry.Initialize(); // Wait for background tag calculation to complete so tests can assert on tags. Telemetry.GetDefaultTagsAsync().GetAwaiter().GetResult(); } } /// <summary> /// Gets the name of the reported activity source. /// </summary> public string ReportedSourceName { get; } /// <summary> /// Gets the name of the diagnostics activity source. /// </summary> public string DiagnosticsSourceName { get; } /// <summary> /// Gets the tags source used by this fixture. /// </summary> public TelemetryTagsSource TagsSource { get; } /// <summary> /// Gets the initialized telemetry instance. /// </summary> public AspireCliTelemetry Telemetry { get; } /// <summary> /// Gets the last activity that was stopped by the listener. /// </summary> public Activity? CapturedActivity { get; private set; } /// <inheritdoc/> public void Dispose() => _listener.Dispose(); /// <summary> /// A test implementation of <see cref="IMachineInformationProvider"/> with configurable values. /// </summary> internal sealed class TestMachineInformationProvider : IMachineInformationProvider { public string? DeviceId { get; set; } = "test-device-id"; public string MacAddressHash { get; set; } = "test-mac-hash"; public string UserName { get; set; } = string.Empty; public string UserDomainName { get; set; } = string.Empty; public Func<Task<string?>>? GetDeviceIdCallback { get; set; } public Func<Task<string>>? GetMacAddressHashCallback { get; set; } public Task<string?> GetOrCreateDeviceId() => GetDeviceIdCallback?.Invoke() ?? Task.FromResult(DeviceId); public Task<string> GetMacAddressHash() => GetMacAddressHashCallback?.Invoke() ?? Task.FromResult(MacAddressHash); } /// <summary> /// A test implementation of <see cref="ICIEnvironmentDetector"/> with configurable result. /// </summary> internal sealed class TestCIEnvironmentDetector : ICIEnvironmentDetector { public bool IsCIEnvironmentResult { get; set; } public bool IsCIEnvironment() => IsCIEnvironmentResult; } /// <summary> /// A test implementation of <see cref="ICodingAgentDetector"/> with configurable result. /// </summary> internal sealed class TestCodingAgentDetector : ICodingAgentDetector { public string? CodingAgent { get; set; } public string? GetCodingAgent() => CodingAgent; } /// <summary> /// A test implementation of <see cref="IInternalMicrosoftDetector"/> with configurable result. /// </summary> internal sealed class TestInternalMicrosoftDetector : IInternalMicrosoftDetector { public bool IsInternalMicrosoft { get; set; } public string? Source { get; set; } public string? Alias { get; set; } public string? Domain { get; set; } public IReadOnlyList<InternalMicrosoftProbeDiagnostic> ProbeDiagnostics { get; set; } = []; public Exception? ExceptionToThrow { get; set; } public Func<CancellationToken, Task<InternalMicrosoftDetectionResult>>? DetectionCallback { get; set; } public int InvocationCount { get; private set; } public Task<InternalMicrosoftDetectionResult> IsInternalMicrosoftMachineAsync(CancellationToken cancellationToken = default) { InvocationCount++; if (ExceptionToThrow is not null) { return Task.FromException<InternalMicrosoftDetectionResult>(ExceptionToThrow); } if (DetectionCallback is not null) { return DetectionCallback(cancellationToken); } return Task.FromResult(new InternalMicrosoftDetectionResult( IsInternalMicrosoft, Source, Alias, Domain, IsInternalMicrosoft ? InternalMicrosoftDetectorOutcome.Detected : InternalMicrosoftDetectorOutcome.NotDetected, InternalMicrosoftDetectorCacheStatus.Miss, TimeSpan.FromMilliseconds(1), ProbeDiagnostics)); } } }