Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 0 additions & 31 deletions src/Aspire.Dashboard/Components/Controls/TelemetryErrorBoundary.cs

This file was deleted.

30 changes: 14 additions & 16 deletions src/Aspire.Dashboard/Components/Routes.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,20 @@
return;
}

<TelemetryErrorBoundary>
<CascadingValue Value="@_viewportInformation">
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<!-- AuthorizeRouteView protects pages from being accessed when authorization is required -->
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
</Found>
<NotFound>
<PageTitle>@Loc[nameof(Resources.Routes.NotFoundPageTitle)]</PageTitle>
<LayoutView Layout="@typeof(Layout.MainLayout)">
<Aspire.Dashboard.Components.Pages.NotFound />
</LayoutView>
</NotFound>
</Router>
</CascadingValue>
</TelemetryErrorBoundary>
<CascadingValue Value="@_viewportInformation">
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<!-- AuthorizeRouteView protects pages from being accessed when authorization is required -->
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
</Found>
<NotFound>
<PageTitle>@Loc[nameof(Resources.Routes.NotFoundPageTitle)]</PageTitle>
<LayoutView Layout="@typeof(Layout.MainLayout)">
<Aspire.Dashboard.Components.Pages.NotFound />
</LayoutView>
</NotFound>
</Router>
</CascadingValue>

@code {
private ViewportInformation? _viewportInformation;
Expand Down
1 change: 1 addition & 0 deletions src/Aspire.Dashboard/DashboardWebApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ public DashboardWebApplication(
builder.Services.TryAddScoped<ComponentTelemetryContextProvider>();
builder.Services.TryAddSingleton<DashboardTelemetryService>();
builder.Services.TryAddSingleton<IDashboardTelemetrySender, DashboardTelemetrySender>();
builder.Services.AddSingleton<ILoggerProvider, TelemetryLoggerProvider>();

// OTLP services.
builder.Services.AddGrpc();
Expand Down
73 changes: 73 additions & 0 deletions src/Aspire.Dashboard/Telemetry/TelemetryLoggerProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Dashboard.Telemetry;

/// <summary>
/// Log an error to dashboard telemetry when there is an unhandled Blazor error.
/// </summary>
public sealed class TelemetryLoggerProvider : ILoggerProvider
{
// Log when an unhandled error is caught by Blazor.
// https://github.com/dotnet/aspnetcore/blob/0230498dfccaef6f782a5e37c60ea505081b72bf/src/Components/Server/src/Circuits/CircuitHost.cs#L695
public const string CircuitHostLogCategory = "Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost";
public static readonly EventId CircuitUnhandledExceptionEventId = new EventId(111, "CircuitUnhandledException");

private readonly IServiceProvider _serviceProvider;

public TelemetryLoggerProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public ILogger CreateLogger(string categoryName) => new TelemetryLogger(_serviceProvider, categoryName);

public void Dispose()
{
}

private sealed class TelemetryLogger : ILogger
{
private readonly IServiceProvider _serviceProvider;
private readonly bool _isCircuitHostLogger;

public TelemetryLogger(IServiceProvider serviceProvider, string categoryName)
{
_serviceProvider = serviceProvider;
_isCircuitHostLogger = categoryName == CircuitHostLogCategory;
}

public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;

public bool IsEnabled(LogLevel logLevel) => true;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (_isCircuitHostLogger && eventId == CircuitUnhandledExceptionEventId && exception != null)
{
try
{
// Get the telemetry service lazily to avoid a circular reference between resolving telemetry service and logging.
var telemetryService = _serviceProvider.GetRequiredService<DashboardTelemetryService>();

telemetryService.PostFault(
TelemetryEventKeys.Error,
$"{exception.GetType().FullName}: {exception.Message}",
FaultSeverity.Critical,
new Dictionary<string, AspireTelemetryProperty>
{
[TelemetryPropertyKeys.ExceptionType] = new AspireTelemetryProperty(exception.GetType().FullName!),
[TelemetryPropertyKeys.ExceptionMessage] = new AspireTelemetryProperty(exception.Message),
[TelemetryPropertyKeys.ExceptionStackTrace] = new AspireTelemetryProperty(exception.StackTrace ?? string.Empty)
}
);
}
catch
{
// We should never throw an error out of logging.
// Logging the error to telemetry shouldn't throw. But, for extra safety, send error to telemetry is inside a try/catch.
}
}
}
}
}
16 changes: 16 additions & 0 deletions tests/Aspire.Dashboard.Tests/Integration/StartupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Text.Json.Nodes;
using Aspire.Dashboard.Configuration;
using Aspire.Dashboard.Otlp.Http;
using Aspire.Dashboard.Telemetry;
using Aspire.Hosting;
using Aspire.Tests.Shared.Telemetry;
using Google.Protobuf;
Expand All @@ -15,6 +16,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
Expand Down Expand Up @@ -765,6 +767,20 @@ public async Task Configuration_DisableResourceGraph_EnsureValueSetOnOptions(boo
// Assert
Assert.Equal(value, app.DashboardOptionsMonitor.CurrentValue.UI.DisableResourceGraph);
}
[Fact]
public async Task ServiceProvider_AppCreated_LoggerProvidersRegistered()
{
// Arrange
await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(testOutputHelper);

// Act
var loggerProviders = app.Services.GetServices<ILoggerProvider>();
var loggerProviderTypes = loggerProviders.Select(p => p.GetType()).ToList();

// Assert
Assert.Contains(typeof(TelemetryLoggerProvider), loggerProviderTypes);
Assert.Contains(typeof(ConsoleLoggerProvider), loggerProviderTypes);
}

private static void AssertIPv4OrIPv6Endpoint(Func<EndpointInfo> endPointAccessor)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Dashboard.Telemetry;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;

namespace Aspire.Dashboard.Tests.Telemetry;

public class TelemetryLoggerProviderTests
{
[Fact]
public async Task Log_DifferentCategoryAndEventIds_WriteTelemetryForBlazorUnhandedErrorAsync()
{
// Arrange
var telemetrySender = new TestDashboardTelemetrySender { IsTelemetryEnabled = true };
await telemetrySender.TryStartTelemetrySessionAsync();

var serviceProvider = new ServiceCollection()
.AddSingleton<DashboardTelemetryService>()
.AddSingleton<IDashboardTelemetrySender>(telemetrySender)
.AddLogging()
.AddSingleton<ILoggerProvider, TelemetryLoggerProvider>()
.BuildServiceProvider();

var loggerProvider = serviceProvider.GetRequiredService<ILoggerFactory>();

// Act & assert 1
var testLogger = loggerProvider.CreateLogger("testLogger");
testLogger.Log(LogLevel.Error, TelemetryLoggerProvider.CircuitUnhandledExceptionEventId, "Test message");
Assert.False(telemetrySender.ContextChannel.Reader.TryPeek(out _));

// Act & assert 2
var circuitHostLogger = loggerProvider.CreateLogger(TelemetryLoggerProvider.CircuitHostLogCategory);
circuitHostLogger.LogInformation("Test log message");
Assert.False(telemetrySender.ContextChannel.Reader.TryPeek(out _));

// Act & assert 3
circuitHostLogger.Log(LogLevel.Error, TelemetryLoggerProvider.CircuitUnhandledExceptionEventId, "Test message");
Assert.False(telemetrySender.ContextChannel.Reader.TryPeek(out _));

// Act & assert 4
circuitHostLogger.Log(LogLevel.Error, TelemetryLoggerProvider.CircuitUnhandledExceptionEventId, new InvalidOperationException("Exception message"), "Test message");
Assert.True(telemetrySender.ContextChannel.Reader.TryPeek(out var context));
Assert.Equal("/telemetry/fault - $aspire/dashboard/error", context.Name);
}
}