File: Telemetry\TelemetryErrorRecorderTests.cs
Web Access
Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.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.Runtime.ExceptionServices;
using Aspire.Dashboard.Telemetry;
using Aspire.Dashboard.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
 
namespace Aspire.Dashboard.Tests.Telemetry;
 
public class TelemetryErrorRecorderTests
{
    [Fact]
    public void RecordError_Exception_RecordsOriginalException()
    {
        using var fixture = new DashboardTelemetryFixture();
        var recorder = CreateRecorder(fixture);
        var exception = CreateException(new InvalidOperationException("Test error"), "original stack");
 
        recorder.RecordError("Test message", exception);
 
        AssertError(fixture, Assert.Single(ReadErrors(fixture)), exception);
    }
 
    [Fact]
    public void RecordError_AggregateException_RecordsDistinctLeafExceptions()
    {
        using var fixture = new DashboardTelemetryFixture();
        var recorder = CreateRecorder(fixture);
        var exception = CreateException(new InvalidOperationException("Test error"), "original stack");
        var duplicate = CreateException(new InvalidOperationException("Test error"), "original stack");
        var differentType = CreateException(new ArgumentException("Test error"), "original stack");
        var differentMessage = CreateException(new InvalidOperationException("Different error"), "original stack");
        var differentStack = CreateException(new InvalidOperationException("Test error"), "different stack");
        var aggregate = new AggregateException(
            "Exceptions were encountered while disposing components.",
            exception,
            duplicate,
            differentType,
            differentMessage,
            differentStack,
            new AggregateException(duplicate, new AggregateException(exception)));
        Assert.Null(aggregate.StackTrace);
 
        recorder.RecordError("Test message", aggregate);
 
        Assert.Collection(ReadErrors(fixture),
            log => AssertError(fixture, log, exception),
            log => AssertError(fixture, log, differentType),
            log => AssertError(fixture, log, differentMessage),
            log => AssertError(fixture, log, differentStack));
    }
 
    [Fact]
    public void RecordError_AggregateExceptionWithoutStacks_DeduplicatesExceptions()
    {
        using var fixture = new DashboardTelemetryFixture();
        var recorder = CreateRecorder(fixture);
        var exception = new InvalidOperationException("Test error");
        var aggregate = new AggregateException(exception, new InvalidOperationException("Test error"));
 
        recorder.RecordError("Test message", aggregate);
 
        AssertError(fixture, Assert.Single(ReadErrors(fixture)), exception);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void RecordError_EmptyAggregateException_RecordsOriginalException(bool nested)
    {
        using var fixture = new DashboardTelemetryFixture();
        var recorder = CreateRecorder(fixture);
        var exception = nested ? new AggregateException(new AggregateException()) : new AggregateException();
 
        recorder.RecordError("Test message", exception);
 
        AssertError(fixture, Assert.Single(ReadErrors(fixture)), exception);
    }
 
    [Fact]
    public void RecordError_RepeatedCalls_RecordsEachOccurrence()
    {
        using var fixture = new DashboardTelemetryFixture();
        var recorder = CreateRecorder(fixture);
        var exception = CreateException(new InvalidOperationException("Test error"), "original stack");
        var aggregate = new AggregateException(exception, exception);
 
        recorder.RecordError("Test message", aggregate);
        recorder.RecordError("Test message", aggregate);
 
        Assert.Collection(ReadErrors(fixture),
            log => AssertError(fixture, log, exception),
            log => AssertError(fixture, log, exception));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public void RecordError_WriteToLogging_LogsOriginalExceptionOnce(bool writeToLogging)
    {
        using var fixture = new DashboardTelemetryFixture();
        var sink = new TestSink();
        using var loggerFactory = new TestLoggerFactory(sink, enabled: true);
        var logger = new TestLogger<TelemetryErrorRecorder>(loggerFactory);
        var recorder = new TelemetryErrorRecorder(fixture.Telemetry, logger);
        var exception = new InvalidOperationException("Test error");
        var aggregate = new AggregateException(exception, exception);
 
        recorder.RecordError("Test message", aggregate, writeToLogging);
 
        var logs = sink.Writes;
        if (writeToLogging)
        {
            var log = Assert.Single(logs);
            Assert.Same(aggregate, log.Exception);
            Assert.Equal("Test message", log.Message);
        }
        else
        {
            Assert.Empty(logs);
        }
 
        Assert.Empty(fixture.LocalLogSink.Writes);
        AssertError(fixture, Assert.Single(ReadErrors(fixture)), exception);
    }
 
    [Fact]
    public void RecordError_TelemetryDisabled_DoesNotExportLogs()
    {
        using var fixture = new DashboardTelemetryFixture(reportedTelemetryEnabled: false);
        var recorder = CreateRecorder(fixture);
 
        recorder.RecordError("Test message", new AggregateException(new InvalidOperationException("Test error")));
 
        Assert.Empty(ReadErrors(fixture));
        Assert.False(fixture.ActivityChannel.Reader.TryPeek(out _));
    }
 
    private static TelemetryErrorRecorder CreateRecorder(DashboardTelemetryFixture fixture) =>
        new(fixture.Telemetry, NullLogger<TelemetryErrorRecorder>.Instance);
 
    private static Exception CreateException(Exception exception, string stackTrace)
    {
        ExceptionDispatchInfo.SetRemoteStackTrace(exception, stackTrace);
 
        return exception;
    }
 
    internal static void AssertError(DashboardTelemetryFixture fixture, TestDashboardTelemetryLog log, Exception exception)
    {
        Assert.Equal(TelemetryEventKeys.Error, log.Message);
        Assert.Equal(LogLevel.Information, log.Level);
        Assert.Equal(DashboardTelemetryService.EventLogCategoryName, log.CategoryName);
        fixture.AssertAttributes(log.Attributes,
            new(TelemetryPropertyKeys.ExceptionType, exception.GetType().FullName),
            new("microsoft.operation_name", TelemetryEventKeys.Error),
            new(TelemetryPropertyKeys.ExceptionRuntimeVersion, VersionHelpers.RuntimeVersion?.ToString() ?? string.Empty),
            new("{OriginalFormat}", TelemetryEventKeys.Error));
    }
 
    private static List<TestDashboardTelemetryLog> ReadErrors(DashboardTelemetryFixture fixture)
    {
        Assert.False(fixture.ActivityChannel.Reader.TryPeek(out _));
        var logs = new List<TestDashboardTelemetryLog>();
        while (fixture.LogChannel.Reader.TryRead(out var log))
        {
            logs.Add(log);
        }
 
        return logs;
    }
}