feat(claude-do): UI-Prozess loggt auf Platte (ui-.log) + fehlgeschlagene Hub-
## Problem
Beim DeleteTask-Bug (Fix 352aaa52) gab es null Spuren: Der UI-Prozess (ClaudeDo.App) hat gar kein File-Logging (weder Serilog noch `Microsoft.Extensions.Logging` sind in App/Ui referenziert), und im Worker loggt SignalR fehlgeschlagene Hub-Invocations nur auf Debug (`Microsoft.AspNetCore` steht in `src/ClaudeDo.Worker/Program.cs` Z. 60 auf Warning). Die Ursache ließ sich nur finden, ind
ClaudeDo-Task: d2ae3831-733f-4d40-93d7-70c93da11590
This commit is contained in:
@@ -52,6 +52,7 @@ public partial class App : Application
|
|||||||
Dispatcher.UIThread.UnhandledException += (_, e) =>
|
Dispatcher.UIThread.UnhandledException += (_, e) =>
|
||||||
{
|
{
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
|
Serilog.Log.Error(e.Exception, "Unhandled exception on the UI dispatcher");
|
||||||
shell.FlashFooterError(Loc.T("vm.shell.unexpectedError", e.Exception.Message));
|
shell.FlashFooterError(Loc.T("vm.shell.unexpectedError", e.Exception.Message));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
|
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using ClaudeDo.Ui.ViewModels.Modals;
|
|||||||
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Serilog;
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -20,6 +21,7 @@ using System.Linq;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ClaudeDo.App;
|
namespace ClaudeDo.App;
|
||||||
|
|
||||||
@@ -34,6 +36,35 @@ sealed class Program
|
|||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
SetCurrentProcessExplicitAppUserModelID("ClaudeDo.App");
|
SetCurrentProcessExplicitAppUserModelID("ClaudeDo.App");
|
||||||
|
|
||||||
|
var logRoot = Path.Combine(Paths.AppDataRoot(), "logs");
|
||||||
|
Directory.CreateDirectory(logRoot);
|
||||||
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
.MinimumLevel.Information()
|
||||||
|
.WriteTo.File(
|
||||||
|
Path.Combine(logRoot, "ui-.log"),
|
||||||
|
rollingInterval: RollingInterval.Day,
|
||||||
|
retainedFileCountLimit: 7,
|
||||||
|
shared: true)
|
||||||
|
.CreateLogger();
|
||||||
|
|
||||||
|
var version = Assembly.GetEntryAssembly()?
|
||||||
|
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion
|
||||||
|
?? "0.0.0";
|
||||||
|
Log.Information("ClaudeDo.App starting (version: {Version})", version);
|
||||||
|
|
||||||
|
// Last-resort net: any exception that misses every other handler still gets one
|
||||||
|
// Fatal line in the log before the process dies.
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
|
||||||
|
{
|
||||||
|
Log.Fatal(e.ExceptionObject as Exception, "Unhandled exception, terminating (IsTerminating: {IsTerminating})", e.IsTerminating);
|
||||||
|
Log.CloseAndFlush();
|
||||||
|
};
|
||||||
|
TaskScheduler.UnobservedTaskException += (_, e) =>
|
||||||
|
{
|
||||||
|
Log.Error(e.Exception, "Unobserved task exception");
|
||||||
|
e.SetObserved();
|
||||||
|
};
|
||||||
|
|
||||||
var services = BuildServices();
|
var services = BuildServices();
|
||||||
|
|
||||||
using (var scope = services.CreateScope())
|
using (var scope = services.CreateScope())
|
||||||
@@ -62,6 +93,8 @@ sealed class Program
|
|||||||
{
|
{
|
||||||
/* best effort on shutdown */
|
/* best effort on shutdown */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.CloseAndFlush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||||
<PackageReference Include="Duende.IdentityModel.OidcClient" Version="7.1.0" />
|
<PackageReference Include="Duende.IdentityModel.OidcClient" Version="7.1.0" />
|
||||||
<PackageReference Include="Iciclecreek.Avalonia.Terminal" Version="2.0.3" />
|
<PackageReference Include="Iciclecreek.Avalonia.Terminal" Version="2.0.3" />
|
||||||
|
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -304,7 +304,11 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
ok = true;
|
ok = true;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
catch { return default; }
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Serilog.Log.Warning(ex, "Hub call {Method} failed", method);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
|
finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,6 +322,11 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
await invoke();
|
await invoke();
|
||||||
ok = true;
|
ok = true;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Serilog.Log.Warning(ex, "Hub call {Method} failed", method);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
|
finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,6 +341,11 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
ok = true;
|
ok = true;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Serilog.Log.Warning(ex, "Hub call {Method} failed", method);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
|
finally { OperationTiming.Shared.Record("hub", method, sw.Elapsed, ok); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -208,6 +208,9 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
private void FlashFooter(string message, WorkerLogLevel level)
|
private void FlashFooter(string message, WorkerLogLevel level)
|
||||||
{
|
{
|
||||||
|
if (level == WorkerLogLevel.Error)
|
||||||
|
Serilog.Log.Warning("Footer error: {Message}", message);
|
||||||
|
|
||||||
WorkerLogText = $"{DateTime.Now:HH:mm} · {message}";
|
WorkerLogText = $"{DateTime.Now:HH:mm} · {message}";
|
||||||
WorkerLogLevel = level;
|
WorkerLogLevel = level;
|
||||||
IsWorkerLogVisible = true;
|
IsWorkerLogVisible = true;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Hub;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs a Warning for any exception thrown out of a hub method body, so a failed UI action
|
||||||
|
/// leaves a trace in <c>worker-.log</c> instead of only a generic error at the caller.
|
||||||
|
/// Does NOT see an argument-binding failure (e.g. a stray CancellationToken parameter) — SignalR
|
||||||
|
/// rejects those while parsing the invocation message, before a hub method is ever dispatched.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class HubExceptionLoggingFilter(ILogger<HubExceptionLoggingFilter> logger) : IHubFilter
|
||||||
|
{
|
||||||
|
public async ValueTask<object?> InvokeMethodAsync(
|
||||||
|
HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object?>> next)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await next(invocationContext);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Hub method {Method} failed", invocationContext.HubMethodName);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ using ClaudeDo.Worker.Usage.Interfaces;
|
|||||||
using ClaudeDo.Worker.Usage.TokenTracker;
|
using ClaudeDo.Worker.Usage.TokenTracker;
|
||||||
using ClaudeDo.Worker.Usage.TokenTracker.Interfaces;
|
using ClaudeDo.Worker.Usage.TokenTracker.Interfaces;
|
||||||
using ClaudeDo.Worker.Worktrees;
|
using ClaudeDo.Worker.Worktrees;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
@@ -58,6 +59,11 @@ builder.Host.UseSerilog((ctx, lc) => lc
|
|||||||
.MinimumLevel.Information()
|
.MinimumLevel.Information()
|
||||||
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", Serilog.Events.LogEventLevel.Warning)
|
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", Serilog.Events.LogEventLevel.Warning)
|
||||||
.MinimumLevel.Override("Microsoft.AspNetCore", Serilog.Events.LogEventLevel.Warning)
|
.MinimumLevel.Override("Microsoft.AspNetCore", Serilog.Events.LogEventLevel.Warning)
|
||||||
|
// SignalR logs an argument-binding failure (e.g. a stray CancellationToken parameter, see
|
||||||
|
// fix 352aaa52) at Debug on this specific source context — HubExceptionLoggingFilter can't
|
||||||
|
// see it either, since SignalR rejects the invocation while parsing the message, before any
|
||||||
|
// hub method (and its filter pipeline) is dispatched.
|
||||||
|
.MinimumLevel.Override("Microsoft.AspNetCore.SignalR.Internal.DefaultHubDispatcher", Serilog.Events.LogEventLevel.Debug)
|
||||||
.WriteTo.File(
|
.WriteTo.File(
|
||||||
System.IO.Path.Combine(logRoot, "worker-.log"),
|
System.IO.Path.Combine(logRoot, "worker-.log"),
|
||||||
rollingInterval: RollingInterval.Day,
|
rollingInterval: RollingInterval.Day,
|
||||||
@@ -90,7 +96,11 @@ builder.Services.AddHostedService<UntrackedWorktreeRecovery>();
|
|||||||
// Default MaximumParallelInvocationsPerClient=1 serializes every hub call on one connection —
|
// Default MaximumParallelInvocationsPerClient=1 serializes every hub call on one connection —
|
||||||
// a slow one (e.g. GetTicketProjects hitting an unreachable ticket system) blocks unrelated
|
// a slow one (e.g. GetTicketProjects hitting an unreachable ticket system) blocks unrelated
|
||||||
// calls like UpdateList/UpdateListConfig behind it for the same 10s.
|
// calls like UpdateList/UpdateListConfig behind it for the same 10s.
|
||||||
builder.Services.AddSignalR(options => options.MaximumParallelInvocationsPerClient = 10)
|
builder.Services.AddSignalR(options =>
|
||||||
|
{
|
||||||
|
options.MaximumParallelInvocationsPerClient = 10;
|
||||||
|
options.AddFilter<HubExceptionLoggingFilter>();
|
||||||
|
})
|
||||||
.AddJsonProtocol(options =>
|
.AddJsonProtocol(options =>
|
||||||
{
|
{
|
||||||
options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
|
options.PayloadSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
|
||||||
|
|||||||
Reference in New Issue
Block a user