From da73324e3af33e37391bf1b87dee12c3178687e3 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 18:32:28 +0200 Subject: [PATCH 1/8] build(logging): scaffold ClaudeDo.Logging project --- ClaudeDo.slnx | 1 + src/ClaudeDo.Logging/ClaudeDo.Logging.csproj | 19 +++++++++++++++++++ src/ClaudeDo.Logging/Placeholder.cs | 3 +++ 3 files changed, 23 insertions(+) create mode 100644 src/ClaudeDo.Logging/ClaudeDo.Logging.csproj create mode 100644 src/ClaudeDo.Logging/Placeholder.cs diff --git a/ClaudeDo.slnx b/ClaudeDo.slnx index efcd91e..a8d78a5 100644 --- a/ClaudeDo.slnx +++ b/ClaudeDo.slnx @@ -7,6 +7,7 @@ + diff --git a/src/ClaudeDo.Logging/ClaudeDo.Logging.csproj b/src/ClaudeDo.Logging/ClaudeDo.Logging.csproj new file mode 100644 index 0000000..985392d --- /dev/null +++ b/src/ClaudeDo.Logging/ClaudeDo.Logging.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/src/ClaudeDo.Logging/Placeholder.cs b/src/ClaudeDo.Logging/Placeholder.cs new file mode 100644 index 0000000..c72b270 --- /dev/null +++ b/src/ClaudeDo.Logging/Placeholder.cs @@ -0,0 +1,3 @@ +namespace ClaudeDo.Logging; + +internal static class Placeholder; -- 2.49.1 From b3b87df3209f2e1410b491108f8368da994cc14d Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 19:07:38 +0200 Subject: [PATCH 2/8] feat(logging): default TaskId enricher with passing tests --- src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs | 15 ++++++ src/ClaudeDo.Logging/Placeholder.cs | 3 -- .../ClaudeDo.Worker.Tests.csproj | 1 + .../Logging/DefaultTaskIdEnricherTests.cs | 49 +++++++++++++++++++ 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs delete mode 100644 src/ClaudeDo.Logging/Placeholder.cs create mode 100644 tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs diff --git a/src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs b/src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs new file mode 100644 index 0000000..cea4b60 --- /dev/null +++ b/src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs @@ -0,0 +1,15 @@ +using Serilog.Core; +using Serilog.Events; + +namespace ClaudeDo.Logging; + +/// Ensures every log event carries a TaskId property (defaulting to "-") +/// so the output template's [{TaskId}] column never renders the raw token. +public sealed class DefaultTaskIdEnricher : ILogEventEnricher +{ + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + if (!logEvent.Properties.ContainsKey("TaskId")) + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TaskId", "-")); + } +} diff --git a/src/ClaudeDo.Logging/Placeholder.cs b/src/ClaudeDo.Logging/Placeholder.cs deleted file mode 100644 index c72b270..0000000 --- a/src/ClaudeDo.Logging/Placeholder.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace ClaudeDo.Logging; - -internal static class Placeholder; diff --git a/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj b/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj index bfa4f50..c2fb766 100644 --- a/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj +++ b/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj @@ -25,6 +25,7 @@ + diff --git a/tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs b/tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs new file mode 100644 index 0000000..d360f1f --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/Logging/DefaultTaskIdEnricherTests.cs @@ -0,0 +1,49 @@ +using ClaudeDo.Logging; +using Serilog; +using Serilog.Context; +using Serilog.Core; +using Serilog.Events; + +namespace ClaudeDo.Worker.Tests.Logging; + +public sealed class DefaultTaskIdEnricherTests +{ + private sealed class CollectingSink : ILogEventSink + { + public List Events { get; } = new(); + public void Emit(LogEvent logEvent) => Events.Add(logEvent); + } + + [Fact] + public void AddsDash_WhenNoTaskIdInScope() + { + var sink = new CollectingSink(); + using var logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .Enrich.With(new DefaultTaskIdEnricher()) + .WriteTo.Sink(sink) + .CreateLogger(); + + logger.Information("hello"); + + var prop = Assert.Single(sink.Events).Properties["TaskId"]; + Assert.Equal("\"-\"", prop.ToString()); + } + + [Fact] + public void KeepsPushedTaskId_WhenInScope() + { + var sink = new CollectingSink(); + using var logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .Enrich.With(new DefaultTaskIdEnricher()) + .WriteTo.Sink(sink) + .CreateLogger(); + + using (LogContext.PushProperty("TaskId", "task-42")) + logger.Information("hello"); + + var prop = Assert.Single(sink.Events).Properties["TaskId"]; + Assert.Equal("\"task-42\"", prop.ToString()); + } +} -- 2.49.1 From ab260ad0a626de97b691836355529857d0446242 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 19:09:49 +0200 Subject: [PATCH 3/8] feat(logging): runtime Debug-build detection via DebuggableAttribute --- src/ClaudeDo.Logging/BuildConfig.cs | 14 ++++++++++++++ .../Logging/BuildConfigTests.cs | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/ClaudeDo.Logging/BuildConfig.cs create mode 100644 tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs diff --git a/src/ClaudeDo.Logging/BuildConfig.cs b/src/ClaudeDo.Logging/BuildConfig.cs new file mode 100644 index 0000000..2e13542 --- /dev/null +++ b/src/ClaudeDo.Logging/BuildConfig.cs @@ -0,0 +1,14 @@ +using System.Diagnostics; +using System.Reflection; + +namespace ClaudeDo.Logging; + +/// Runtime build-configuration detection — the replacement for #if DEBUG. +/// Debug builds compile with the JIT optimizer disabled; Release builds enable it. +public static class BuildConfig +{ + public static bool IsDebug { get; } = + Assembly.GetEntryAssembly() + ?.GetCustomAttribute() + ?.IsJITOptimizerDisabled ?? false; +} diff --git a/tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs b/tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs new file mode 100644 index 0000000..76d9293 --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs @@ -0,0 +1,19 @@ +using System.Diagnostics; +using System.Reflection; +using ClaudeDo.Logging; + +namespace ClaudeDo.Worker.Tests.Logging; + +public sealed class BuildConfigTests +{ + [Fact] + public void IsDebug_MatchesEntryAssemblyDebuggableAttribute() + { + var entry = Assembly.GetEntryAssembly(); + var expected = entry? + .GetCustomAttribute() + ?.IsJITOptimizerDisabled ?? false; + + Assert.Equal(expected, BuildConfig.IsDebug); + } +} -- 2.49.1 From 7ce418d474578f934fe9e1c3d2d443964456d751 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 19:11:50 +0200 Subject: [PATCH 4/8] feat(logging): shared LoggingSetup with build-config sink branching --- src/ClaudeDo.Logging/LoggingSetup.cs | 44 +++++++++++++++++++ .../Logging/LoggingSetupTests.cs | 30 +++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 src/ClaudeDo.Logging/LoggingSetup.cs create mode 100644 tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs diff --git a/src/ClaudeDo.Logging/LoggingSetup.cs b/src/ClaudeDo.Logging/LoggingSetup.cs new file mode 100644 index 0000000..c93ea40 --- /dev/null +++ b/src/ClaudeDo.Logging/LoggingSetup.cs @@ -0,0 +1,44 @@ +using Serilog; +using Serilog.Events; + +namespace ClaudeDo.Logging; + +public static class LoggingSetup +{ + private const string OutputTemplate = + "[{Timestamp:HH:mm:ss.fff} {Level:u3}] {Process}/{SourceContext} [{TaskId}] {Message:lj}{NewLine}{Exception}"; + + public static LoggerConfiguration Configure(LoggerConfiguration cfg, string processTag, string logRoot) + { + Directory.CreateDirectory(logRoot); + var logFile = Path.Combine(logRoot, "claudedo-.log"); + + cfg.Enrich.FromLogContext() + .Enrich.WithProperty("Process", processTag) + .Enrich.With(new DefaultTaskIdEnricher()); + + if (BuildConfig.IsDebug) + { + cfg.MinimumLevel.Debug() + .WriteTo.Console(outputTemplate: OutputTemplate) + .WriteTo.File( + logFile, + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 2, + shared: true, + outputTemplate: OutputTemplate); + } + else + { + cfg.MinimumLevel.Warning() + .WriteTo.File( + logFile, + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 2, + shared: true, + outputTemplate: OutputTemplate); + } + + return cfg; + } +} diff --git a/tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs b/tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs new file mode 100644 index 0000000..4706c9c --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs @@ -0,0 +1,30 @@ +using ClaudeDo.Logging; +using Serilog; + +namespace ClaudeDo.Worker.Tests.Logging; + +public sealed class LoggingSetupTests +{ + [Fact] + public void Configure_WritesSharedLogFile() + { + var logRoot = Path.Combine(Path.GetTempPath(), "claudedo-logtest-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(logRoot); + try + { + var logger = LoggingSetup.Configure(new LoggerConfiguration(), "test", logRoot).CreateLogger(); + logger.Warning("marker-{Marker}", "xyz"); + logger.Dispose(); // flush + release the file handle + + var files = Directory.GetFiles(logRoot, "claudedo-*.log"); + var file = Assert.Single(files); + var contents = File.ReadAllText(file); + Assert.Contains("marker-", contents); + Assert.Contains("test/", contents); // {Process} tag in the template + } + finally + { + try { Directory.Delete(logRoot, recursive: true); } catch { /* best effort */ } + } + } +} -- 2.49.1 From c592ca32fb7baea49524d7f48aa2335927e20196 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 19:13:51 +0200 Subject: [PATCH 5/8] feat(logging): route Worker logging through shared LoggingSetup --- src/ClaudeDo.Worker/ClaudeDo.Worker.csproj | 1 + src/ClaudeDo.Worker/Program.cs | 9 ++------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj b/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj index 45c64d3..bab0594 100644 --- a/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj +++ b/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj @@ -2,6 +2,7 @@ + diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs index 3f06a19..71ebc32 100644 --- a/src/ClaudeDo.Worker/Program.cs +++ b/src/ClaudeDo.Worker/Program.cs @@ -31,13 +31,8 @@ var builder = WebApplication.CreateBuilder(args); var logRoot = cfg.LogRoot; Directory.CreateDirectory(logRoot); -builder.Host.UseSerilog((ctx, lc) => lc - .MinimumLevel.Information() - .WriteTo.File( - System.IO.Path.Combine(logRoot, "worker-.log"), - rollingInterval: RollingInterval.Day, - retainedFileCountLimit: 7, - shared: true)); +builder.Host.UseSerilog((ctx, lc) => + ClaudeDo.Logging.LoggingSetup.Configure(lc, "worker", logRoot)); builder.Services.AddDbContextFactory(opt => opt.UseSqlite($"Data Source={cfg.DbPath}")); -- 2.49.1 From 324f1d9c7c9e1d4773eee9ca2757a50aade4b4e4 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 19:15:36 +0200 Subject: [PATCH 6/8] feat(logging): wire App/Ui logging to shared LoggingSetup --- src/ClaudeDo.App/ClaudeDo.App.csproj | 2 ++ src/ClaudeDo.App/Program.cs | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/src/ClaudeDo.App/ClaudeDo.App.csproj b/src/ClaudeDo.App/ClaudeDo.App.csproj index 2b2c2b4..dd9c879 100644 --- a/src/ClaudeDo.App/ClaudeDo.App.csproj +++ b/src/ClaudeDo.App/ClaudeDo.App.csproj @@ -24,10 +24,12 @@ + + diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs index 0c68eb0..5706beb 100644 --- a/src/ClaudeDo.App/Program.cs +++ b/src/ClaudeDo.App/Program.cs @@ -13,6 +13,8 @@ using ClaudeDo.Ui.ViewModels.Modals; using ClaudeDo.Ui.ViewModels.Modals.Settings; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog; using System; using System.Globalization; using System.IO; @@ -77,6 +79,12 @@ sealed class Program var sc = new ServiceCollection(); + var logRoot = Path.Combine(Path.GetDirectoryName(dbPath)!, "logs"); + var serilogLogger = ClaudeDo.Logging.LoggingSetup + .Configure(new LoggerConfiguration(), "app", logRoot) + .CreateLogger(); + sc.AddLogging(b => b.AddSerilog(serilogLogger, dispose: true)); + // Infrastructure sc.AddSingleton(settings); var localesDir = Path.Combine(AppContext.BaseDirectory, "locales"); -- 2.49.1 From 075b6d13afdf9921953e3aabe28a1ed2fa1c512c Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 19:16:56 +0200 Subject: [PATCH 7/8] feat(logging): tag Worker task execution with TaskId for traceability --- src/ClaudeDo.Worker/Runner/TaskRunner.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ClaudeDo.Worker/Runner/TaskRunner.cs b/src/ClaudeDo.Worker/Runner/TaskRunner.cs index 53fda84..f18bb03 100644 --- a/src/ClaudeDo.Worker/Runner/TaskRunner.cs +++ b/src/ClaudeDo.Worker/Runner/TaskRunner.cs @@ -1,5 +1,6 @@ using System.Text.Json; using ClaudeDo.Data; +using Serilog.Context; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; using ClaudeDo.Worker.Config; @@ -46,6 +47,7 @@ public sealed class TaskRunner public async Task RunAsync(TaskEntity task, string slot, CancellationToken ct) { + using var _taskScope = LogContext.PushProperty("TaskId", task.Id); string? mcpToken = null; string? mcpConfigPath = null; try @@ -170,6 +172,7 @@ public sealed class TaskRunner public async Task ContinueAsync(string taskId, string followUpPrompt, string slot, CancellationToken ct) { + using var _taskScope = LogContext.PushProperty("TaskId", taskId); TaskEntity task; TaskRunEntity lastRun; ListEntity list; -- 2.49.1 From 50c10b6e7597443c4b5154c278bef86b069ca30d Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 4 Jun 2026 19:22:08 +0200 Subject: [PATCH 8/8] feat(logging): tag UI task actions with TaskId + debug trace lines --- src/ClaudeDo.App/Program.cs | 4 +- src/ClaudeDo.Ui/ClaudeDo.Ui.csproj | 2 + src/ClaudeDo.Ui/Services/WorkerClient.cs | 60 ++++++++++++------------ 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs index 5706beb..f3bda26 100644 --- a/src/ClaudeDo.App/Program.cs +++ b/src/ClaudeDo.App/Program.cs @@ -106,7 +106,9 @@ sealed class Program // Services sc.AddSingleton(); - sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService().SignalRUrl)); + sc.AddSingleton(sp => new WorkerClient( + sp.GetRequiredService().SignalRUrl, + sp.GetRequiredService>())); sc.AddSingleton(sp => sp.GetRequiredService()); // Release check + installer update diff --git a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj index 47a97fb..ac343b2 100644 --- a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj +++ b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj @@ -11,7 +11,9 @@ + + diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index 92e5813..471e03e 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -6,6 +6,8 @@ using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Serilog.Context; namespace ClaudeDo.Ui.Services; @@ -30,6 +32,7 @@ sealed class IndefiniteRetryPolicy : IRetryPolicy public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerClient { private readonly HubConnection _hub; + private readonly ILogger _logger; private CancellationTokenSource? _startCts; private Task _retryLoopTask = Task.CompletedTask; private readonly object _startLock = new(); @@ -65,8 +68,9 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC public string? LastMergeAllTarget { get; private set; } - public WorkerClient(string signalRUrl) + public WorkerClient(string signalRUrl, ILogger logger) { + _logger = logger; _hub = new HubConnectionBuilder() .WithUrl(signalRUrl) .WithAutomaticReconnect(new IndefiniteRetryPolicy()) @@ -240,20 +244,24 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC catch { return default; } } - public async Task RunNowAsync(string taskId) + /// Invoke a task-targeted hub method under a TaskId log scope, emitting a debug trace line. + private async Task InvokeForTaskAsync(string taskId, string method, params object?[] args) { - await _hub.InvokeAsync("RunNow", taskId); + using (LogContext.PushProperty("TaskId", taskId)) + { + _logger.LogDebug("UI invoking {Method} for task {TaskId}", method, taskId); + await _hub.InvokeCoreAsync(method, args); + } } - public async Task ContinueTaskAsync(string taskId, string followUpPrompt) - { - await _hub.InvokeAsync("ContinueTask", taskId, followUpPrompt); - } + public Task RunNowAsync(string taskId) + => InvokeForTaskAsync(taskId, "RunNow", taskId); - public async Task ResetTaskAsync(string taskId) - { - await _hub.InvokeAsync("ResetTask", taskId); - } + public Task ContinueTaskAsync(string taskId, string followUpPrompt) + => InvokeForTaskAsync(taskId, "ContinueTask", taskId, followUpPrompt); + + public Task ResetTaskAsync(string taskId) + => InvokeForTaskAsync(taskId, "ResetTask", taskId); public async Task MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) { @@ -264,10 +272,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC public Task GetMergeTargetsAsync(string taskId) => TryInvokeAsync("GetMergeTargets", taskId); - public async Task CancelTaskAsync(string taskId) - { - await _hub.InvokeAsync("CancelTask", taskId); - } + public Task CancelTaskAsync(string taskId) + => InvokeForTaskAsync(taskId, "CancelTask", taskId); public async Task WakeQueueAsync() { @@ -386,25 +392,17 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString()); } - public async Task ApproveReviewAsync(string taskId) - { - await _hub.InvokeAsync("ApproveReview", taskId); - } + public Task ApproveReviewAsync(string taskId) + => InvokeForTaskAsync(taskId, "ApproveReview", taskId); - public async Task RejectReviewToQueueAsync(string taskId, string feedback) - { - await _hub.InvokeAsync("RejectReviewToQueue", taskId, feedback); - } + public Task RejectReviewToQueueAsync(string taskId, string feedback) + => InvokeForTaskAsync(taskId, "RejectReviewToQueue", taskId, feedback); - public async Task RejectReviewToIdleAsync(string taskId) - { - await _hub.InvokeAsync("RejectReviewToIdle", taskId); - } + public Task RejectReviewToIdleAsync(string taskId) + => InvokeForTaskAsync(taskId, "RejectReviewToIdle", taskId); - public async Task CancelReviewAsync(string taskId) - { - await _hub.InvokeAsync("CancelReview", taskId); - } + public Task CancelReviewAsync(string taskId) + => InvokeForTaskAsync(taskId, "CancelReview", taskId); public Task CleanupFinishedWorktreesAsync(string? listId = null) => TryInvokeAsync("CleanupFinishedWorktrees", listId); -- 2.49.1