diff --git a/src/Aspire.Hosting/Dashboard/DashboardService.cs b/src/Aspire.Hosting/Dashboard/DashboardService.cs
index 393e4f1077e..b57d519b1c9 100644
--- a/src/Aspire.Hosting/Dashboard/DashboardService.cs
+++ b/src/Aspire.Hosting/Dashboard/DashboardService.cs
@@ -6,6 +6,7 @@
using Grpc.Core;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
namespace Aspire.Hosting.Dashboard;
@@ -17,7 +18,7 @@ namespace Aspire.Hosting.Dashboard;
/// required beyond a single request. Longer-scoped data is stored in .
///
[Authorize(Policy = ResourceServiceApiKeyAuthorization.PolicyName)]
-internal sealed partial class DashboardService(DashboardServiceData serviceData, IHostEnvironment hostEnvironment, IHostApplicationLifetime hostApplicationLifetime)
+internal sealed partial class DashboardService(DashboardServiceData serviceData, IHostEnvironment hostEnvironment, IHostApplicationLifetime hostApplicationLifetime, ILogger logger)
: Aspire.ResourceService.Proto.V1.DashboardService.DashboardServiceBase
{
// Calls that consume or produce streams must create a linked cancellation token
@@ -53,18 +54,11 @@ public override async Task WatchResources(
IServerStreamWriter responseStream,
ServerCallContext context)
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping, context.CancellationToken);
+ await ExecuteAsync(
+ WatchResourcesInternal,
+ context).ConfigureAwait(false);
- try
- {
- await WatchResourcesInternal().ConfigureAwait(false);
- }
- catch (Exception ex) when (ex is OperationCanceledException or IOException && cts.Token.IsCancellationRequested)
- {
- // Ignore cancellation and just return. Note that cancelled writes throw IOException.
- }
-
- async Task WatchResourcesInternal()
+ async Task WatchResourcesInternal(CancellationToken cancellationToken)
{
var (initialData, updates) = serviceData.SubscribeResources();
@@ -75,9 +69,9 @@ async Task WatchResourcesInternal()
data.Resources.Add(Resource.FromSnapshot(resource));
}
- await responseStream.WriteAsync(new() { InitialData = data }).ConfigureAwait(false);
+ await responseStream.WriteAsync(new() { InitialData = data }, cancellationToken).ConfigureAwait(false);
- await foreach (var batch in updates.WithCancellation(cts.Token).ConfigureAwait(false))
+ await foreach (var batch in updates.WithCancellation(cancellationToken).ConfigureAwait(false))
{
WatchResourcesChanges changes = new();
@@ -101,7 +95,7 @@ async Task WatchResourcesInternal()
changes.Value.Add(change);
}
- await responseStream.WriteAsync(new() { Changes = changes }, cts.Token).ConfigureAwait(false);
+ await responseStream.WriteAsync(new() { Changes = changes }, cancellationToken).ConfigureAwait(false);
}
}
}
@@ -111,18 +105,11 @@ public override async Task WatchResourceConsoleLogs(
IServerStreamWriter responseStream,
ServerCallContext context)
{
- using var cts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping, context.CancellationToken);
+ await ExecuteAsync(
+ WatchResourceConsoleLogsInternal,
+ context).ConfigureAwait(false);
- try
- {
- await WatchResourceConsoleLogsInternal().ConfigureAwait(false);
- }
- catch (Exception ex) when (ex is OperationCanceledException or IOException && cts.Token.IsCancellationRequested)
- {
- // Ignore cancellation and just return. Note that cancelled writes throw IOException.
- }
-
- async Task WatchResourceConsoleLogsInternal()
+ async Task WatchResourceConsoleLogsInternal(CancellationToken cancellationToken)
{
var subscription = serviceData.SubscribeConsoleLogs(request.ResourceName);
@@ -131,7 +118,7 @@ async Task WatchResourceConsoleLogsInternal()
return;
}
- await foreach (var group in subscription.WithCancellation(cts.Token).ConfigureAwait(false))
+ await foreach (var group in subscription.WithCancellation(cancellationToken).ConfigureAwait(false))
{
var update = new WatchResourceConsoleLogsUpdate();
@@ -140,8 +127,31 @@ async Task WatchResourceConsoleLogsInternal()
update.LogLines.Add(new ConsoleLogLine() { LineNumber = lineNumber, Text = content, IsStdErr = isErrorMessage });
}
- await responseStream.WriteAsync(update, cts.Token).ConfigureAwait(false);
+ await responseStream.WriteAsync(update, cancellationToken).ConfigureAwait(false);
}
}
}
+
+ private async Task ExecuteAsync(Func execute, ServerCallContext serverCallContext)
+ {
+ using var cts = CancellationTokenSource.CreateLinkedTokenSource(hostApplicationLifetime.ApplicationStopping, serverCallContext.CancellationToken);
+
+ try
+ {
+ await execute(cts.Token).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException) when (cts.Token.IsCancellationRequested)
+ {
+ // Ignore cancellation and just return.
+ }
+ catch (IOException) when (cts.Token.IsCancellationRequested)
+ {
+ // Ignore cancellation and just return. Cancelled writes throw IOException.
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, $"Error executing service method '{serverCallContext.Method}'.");
+ throw;
+ }
+ }
}
diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs
index 73b9a68c624..5c8b03299de 100644
--- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs
+++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs
@@ -127,6 +127,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
_innerBuilder.Logging.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Warning);
_innerBuilder.Logging.AddFilter("Microsoft.AspNetCore.Server.Kestrel", LogLevel.Error);
_innerBuilder.Logging.AddFilter("Aspire.Hosting.Dashboard", LogLevel.Error);
+ _innerBuilder.Logging.AddFilter("Grpc.AspNetCore.Server.ServerCallHandler", LogLevel.Error);
// This is so that we can see certificate errors in the resource server in the console logs.
// See: https://github.com/dotnet/aspire/issues/2914