Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 3 additions & 3 deletions src/Aspire.Dashboard/ServiceClient/DashboardClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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-apphost-connection-failed");
throw;
}
}
Expand Down Expand Up @@ -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-apphost-connection-failed", errorCount);
}
}
}
Expand Down Expand Up @@ -500,7 +500,7 @@ private async Task WatchWithRecoveryAsync(Func<RetryContext, CancellationToken,
{
retryContext.ErrorCount++;

_logger.LogError(ex, "Error #{ErrorCount} watching {WatchName}.", retryContext.ErrorCount, actionName);
_logger.LogError(ex, "Error #{ErrorCount} watching {WatchName}. For troubleshooting, see https://aka.ms/aspire/dashboard-apphost-connection-failed", retryContext.ErrorCount, actionName);
}
}

Expand Down
45 changes: 44 additions & 1 deletion tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
using Grpc.Core;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Xunit;

Expand Down Expand Up @@ -359,15 +361,46 @@ public async Task WatchWithRecovery_RepeatedFailures_FiresMultipleDisconnectedEv
// Without the Connecting transition between retries, only 1 Disconnected event would fire.
for (var i = 0; i < 3; i++)
{
await disconnectedSemaphore.WaitAsync(TimeSpan.FromSeconds(30)).DefaultTimeout();
await disconnectedSemaphore.WaitAsync().DefaultTimeout();
}

Assert.True(disconnectedCount >= 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-apphost-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<WatchInteractionsRequestUpdate, WatchInteractionsResponseUpdate> WatchInteractions(CallOptions options)
{
Expand All @@ -382,6 +415,16 @@ public override AsyncDuplexStreamingCall<WatchInteractionsRequestUpdate, WatchIn

public override AsyncUnaryCall<ApplicationInformationResponse> GetApplicationInformationAsync(ApplicationInformationRequest request, CallOptions options)
{
if (FailOnGetApplicationInformation)
{
return new AsyncUnaryCall<ApplicationInformationResponse>(
Task.FromException<ApplicationInformationResponse>(new RpcException(new Status(StatusCode.Unavailable, "Service unavailable"))),
Task.FromResult(new Metadata()),
() => new Status(StatusCode.Unavailable, "Service unavailable"),
() => new Metadata(),
() => { });
}

return new AsyncUnaryCall<ApplicationInformationResponse>(
Task.FromResult(new ApplicationInformationResponse
{
Expand Down
Loading