Compare commits
8 Commits
9f37b1e21e
...
feature/de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50c10b6e75 | ||
|
|
075b6d13af | ||
|
|
324f1d9c7c | ||
|
|
c592ca32fb | ||
|
|
7ce418d474 | ||
|
|
ab260ad0a6 | ||
|
|
b3b87df320 | ||
|
|
da73324e3a |
@@ -7,6 +7,7 @@
|
||||
<Project Path="src/ClaudeDo.Installer/ClaudeDo.Installer.csproj" />
|
||||
<Project Path="src/ClaudeDo.Releases/ClaudeDo.Releases.csproj" />
|
||||
<Project Path="src/ClaudeDo.Localization/ClaudeDo.Localization.csproj" />
|
||||
<Project Path="src/ClaudeDo.Logging/ClaudeDo.Logging.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/ClaudeDo.Data.Tests/ClaudeDo.Data.Tests.csproj" />
|
||||
|
||||
@@ -24,10 +24,12 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ClaudeDo.Ui\ClaudeDo.Ui.csproj" />
|
||||
<ProjectReference Include="..\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
|
||||
<ProjectReference Include="..\ClaudeDo.Localization\ClaudeDo.Localization.csproj" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\ClaudeDo.Localization\Locales.targets" />
|
||||
|
||||
@@ -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");
|
||||
@@ -98,7 +106,9 @@ sealed class Program
|
||||
|
||||
// Services
|
||||
sc.AddSingleton<GitService>();
|
||||
sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService<AppSettings>().SignalRUrl));
|
||||
sc.AddSingleton(sp => new WorkerClient(
|
||||
sp.GetRequiredService<AppSettings>().SignalRUrl,
|
||||
sp.GetRequiredService<ILogger<WorkerClient>>()));
|
||||
sc.AddSingleton<IWorkerClient>(sp => sp.GetRequiredService<WorkerClient>());
|
||||
|
||||
// Release check + installer update
|
||||
|
||||
14
src/ClaudeDo.Logging/BuildConfig.cs
Normal file
14
src/ClaudeDo.Logging/BuildConfig.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ClaudeDo.Logging;
|
||||
|
||||
/// <summary>Runtime build-configuration detection — the replacement for #if DEBUG.
|
||||
/// Debug builds compile with the JIT optimizer disabled; Release builds enable it.</summary>
|
||||
public static class BuildConfig
|
||||
{
|
||||
public static bool IsDebug { get; } =
|
||||
Assembly.GetEntryAssembly()
|
||||
?.GetCustomAttribute<DebuggableAttribute>()
|
||||
?.IsJITOptimizerDisabled ?? false;
|
||||
}
|
||||
19
src/ClaudeDo.Logging/ClaudeDo.Logging.csproj
Normal file
19
src/ClaudeDo.Logging/ClaudeDo.Logging.csproj
Normal file
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ClaudeDo.Worker.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
15
src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs
Normal file
15
src/ClaudeDo.Logging/DefaultTaskIdEnricher.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace ClaudeDo.Logging;
|
||||
|
||||
/// <summary>Ensures every log event carries a TaskId property (defaulting to "-")
|
||||
/// so the output template's [{TaskId}] column never renders the raw token.</summary>
|
||||
public sealed class DefaultTaskIdEnricher : ILogEventEnricher
|
||||
{
|
||||
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
|
||||
{
|
||||
if (!logEvent.Properties.ContainsKey("TaskId"))
|
||||
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("TaskId", "-"));
|
||||
}
|
||||
}
|
||||
44
src/ClaudeDo.Logging/LoggingSetup.cs
Normal file
44
src/ClaudeDo.Logging/LoggingSetup.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,9 @@
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.11" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog" Version="4.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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<WorkerClient> _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<WorkerClient> 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)
|
||||
/// <summary>Invoke a task-targeted hub method under a TaskId log scope, emitting a debug trace line.</summary>
|
||||
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<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage)
|
||||
{
|
||||
@@ -264,10 +272,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId)
|
||||
=> TryInvokeAsync<MergeTargetsDto>("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<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null)
|
||||
=> TryInvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ClaudeDo.Data\ClaudeDo.Data.csproj" />
|
||||
<ProjectReference Include="..\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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<ClaudeDoDbContext>(opt =>
|
||||
opt.UseSqlite($"Data Source={cfg.DbPath}"));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<ProjectReference Include="..\..\src\ClaudeDo.Worker\ClaudeDo.Worker.csproj" />
|
||||
<ProjectReference Include="..\..\src\ClaudeDo.Data\ClaudeDo.Data.csproj" />
|
||||
<ProjectReference Include="..\..\src\ClaudeDo.Ui\ClaudeDo.Ui.csproj" />
|
||||
<ProjectReference Include="..\..\src\ClaudeDo.Logging\ClaudeDo.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
19
tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs
Normal file
19
tests/ClaudeDo.Worker.Tests/Logging/BuildConfigTests.cs
Normal file
@@ -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<DebuggableAttribute>()
|
||||
?.IsJITOptimizerDisabled ?? false;
|
||||
|
||||
Assert.Equal(expected, BuildConfig.IsDebug);
|
||||
}
|
||||
}
|
||||
@@ -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<LogEvent> 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());
|
||||
}
|
||||
}
|
||||
30
tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs
Normal file
30
tests/ClaudeDo.Worker.Tests/Logging/LoggingSetupTests.cs
Normal file
@@ -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 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user