From fbcff1f8f234183fccbb24f67362c44e77bd8b77 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 13 Jun 2026 13:03:35 +0800 Subject: [PATCH 1/4] Add troubleshooting link to dashboard resource service connection errors --- .../ServiceClient/DashboardClient.cs | 6 +-- .../Model/DashboardClientTests.cs | 45 ++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs b/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs index e07d1459036..b1936b9b5da 100644 --- a/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs +++ b/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs @@ -339,7 +339,7 @@ await Task.WhenAll( } catch (Exception ex) { - _logger.LogError(ex, "Error loading data from the resource service."); + _logger.LogError(ex, "Error loading data from the resource service. For troubleshooting, see https://aka.ms/aspire/dashboard-connection-failed"); throw; } } @@ -411,7 +411,7 @@ private async Task ConnectWithRetryAsync(CancellationToken cancellationToken) catch (Exception ex) { errorCount++; - _logger.LogError(ex, "Error #{ErrorCount} connecting to the resource service.", errorCount); + _logger.LogError(ex, "Error #{ErrorCount} connecting to the resource service. For troubleshooting, see https://aka.ms/aspire/dashboard-connection-failed", errorCount); } } } @@ -500,7 +500,7 @@ private async Task WatchWithRecoveryAsync(Func= 3, $"Expected at least 3 Disconnected events but got {disconnectedCount}."); } + [Fact] + public async Task ConnectWithRetry_LogsErrorWithTroubleshootingLink() + { + var testSink = new TestSink(); + var loggerFactory = LoggerFactory.Create(b => b.AddProvider(new TestLoggerProvider(testSink))); + + await using var instance = new DashboardClient(loggerFactory, _configuration, _dashboardOptions, new MockKnownPropertyLookup()); + instance.SetDashboardServiceClient(new MockDashboardServiceClient { FailOnGetApplicationInformation = true }); + + IDashboardClient client = instance; + var disconnectedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + client.ConnectionStateChanged += state => + { + if (state == DashboardConnectionState.Disconnected) + { + disconnectedTcs.TrySetResult(); + } + }; + + // Trigger the connection attempt which will fail on GetApplicationInformationAsync. + _ = client.WhenConnected; + + // Wait for the first Disconnected event which means the error has been logged. + await disconnectedTcs.Task.DefaultTimeout(); + + var errorLog = testSink.Writes.FirstOrDefault(w => w.LogLevel == LogLevel.Error); + Assert.NotNull(errorLog); + Assert.Contains("https://aka.ms/aspire/dashboard-connection-failed", errorLog.Message); + } + private sealed class MockDashboardServiceClient : Aspire.DashboardService.Proto.V1.DashboardService.DashboardServiceClient { public bool FailOnWatchResources { get; init; } + public bool FailOnGetApplicationInformation { get; init; } public override AsyncDuplexStreamingCall WatchInteractions(CallOptions options) { @@ -382,6 +415,16 @@ public override AsyncDuplexStreamingCall GetApplicationInformationAsync(ApplicationInformationRequest request, CallOptions options) { + if (FailOnGetApplicationInformation) + { + return new AsyncUnaryCall( + Task.FromException(new RpcException(new Status(StatusCode.Unavailable, "Service unavailable"))), + Task.FromResult(new Metadata()), + () => new Status(StatusCode.Unavailable, "Service unavailable"), + () => new Metadata(), + () => { }); + } + return new AsyncUnaryCall( Task.FromResult(new ApplicationInformationResponse { From 960dbe3dffa2ab95fc56424a613dc9ed511bbbfe Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 13 Jun 2026 13:34:10 +0800 Subject: [PATCH 2/4] Rename aka.ms link to dashboard-apphost-connection-failed --- src/Aspire.Dashboard/ServiceClient/DashboardClient.cs | 6 +++--- tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs b/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs index b1936b9b5da..5185a374c12 100644 --- a/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs +++ b/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs @@ -339,7 +339,7 @@ await Task.WhenAll( } catch (Exception ex) { - _logger.LogError(ex, "Error loading data from the resource service. For troubleshooting, see https://aka.ms/aspire/dashboard-connection-failed"); + _logger.LogError(ex, "Error loading data from the resource service. For troubleshooting, see https://aka.ms/aspire/dashboard-apphost-connection-failed"); throw; } } @@ -411,7 +411,7 @@ private async Task ConnectWithRetryAsync(CancellationToken cancellationToken) catch (Exception ex) { errorCount++; - _logger.LogError(ex, "Error #{ErrorCount} connecting to the resource service. For troubleshooting, see https://aka.ms/aspire/dashboard-connection-failed", errorCount); + _logger.LogError(ex, "Error #{ErrorCount} connecting to the resource service. For troubleshooting, see https://aka.ms/aspire/dashboard-apphost-connection-failed", errorCount); } } } @@ -500,7 +500,7 @@ private async Task WatchWithRecoveryAsync(Func w.LogLevel == LogLevel.Error); Assert.NotNull(errorLog); - Assert.Contains("https://aka.ms/aspire/dashboard-connection-failed", errorLog.Message); + Assert.Contains("https://aka.ms/aspire/dashboard-apphost-connection-failed", errorLog.Message); } private sealed class MockDashboardServiceClient : Aspire.DashboardService.Proto.V1.DashboardService.DashboardServiceClient From 7213b1fc53134e500e3334cc841c7fc4f3252e33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Jun 2026 09:20:45 +0000 Subject: [PATCH 3/4] Extract troubleshooting URL into a private const Co-authored-by: JamesNK <303201+JamesNK@users.noreply.github.com> --- src/Aspire.Dashboard/ServiceClient/DashboardClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs b/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs index 5185a374c12..80ddc403b3b 100644 --- a/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs +++ b/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs @@ -41,6 +41,7 @@ namespace Aspire.Dashboard.ServiceClient; internal sealed class DashboardClient : IDashboardClient { private const string ApiKeyHeaderName = "x-resource-service-api-key"; + private const string TroubleshootingUrl = "https://aka.ms/aspire/dashboard-apphost-connection-failed"; private readonly Dictionary _resourceByName = new(StringComparers.ResourceName); private readonly InteractionCollection _pendingInteractionCollection = new(); From 2b89b69a0b4e2b6a526c7ebd860e1e4b57a32fda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Jun 2026 09:22:02 +0000 Subject: [PATCH 4/4] Extract repeated troubleshooting URL into a private const field Co-authored-by: JamesNK <303201+JamesNK@users.noreply.github.com> --- src/Aspire.Dashboard/ServiceClient/DashboardClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs b/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs index 80ddc403b3b..f2f06c0a53f 100644 --- a/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs +++ b/src/Aspire.Dashboard/ServiceClient/DashboardClient.cs @@ -340,7 +340,7 @@ await Task.WhenAll( } catch (Exception ex) { - _logger.LogError(ex, "Error loading data from the resource service. For troubleshooting, see https://aka.ms/aspire/dashboard-apphost-connection-failed"); + _logger.LogError(ex, "Error loading data from the resource service. For troubleshooting, see {TroubleshootingUrl}", TroubleshootingUrl); throw; } } @@ -412,7 +412,7 @@ private async Task ConnectWithRetryAsync(CancellationToken cancellationToken) catch (Exception ex) { errorCount++; - _logger.LogError(ex, "Error #{ErrorCount} connecting to the resource service. For troubleshooting, see https://aka.ms/aspire/dashboard-apphost-connection-failed", errorCount); + _logger.LogError(ex, "Error #{ErrorCount} connecting to the resource service. For troubleshooting, see {TroubleshootingUrl}", errorCount, TroubleshootingUrl); } } } @@ -501,7 +501,7 @@ private async Task WatchWithRecoveryAsync(Func