docs(logging): runtime build-config detection, Warning in Release, retain 2

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-04 18:20:37 +02:00
parent f1316dfd0e
commit e547921fdd

View File

@@ -5,16 +5,16 @@
## Goal ## Goal
Make debug logging rich enough to diagnose problems across the UI↔Worker boundary, while keeping the installed (production) build near-silent. Verbosity is decided by **build configuration** — no runtime knob, no config field: Make debug logging rich enough to diagnose problems across the UI↔Worker boundary, while keeping the installed (production) build near-silent. Verbosity is decided by **build configuration, detected at runtime** — no runtime knob, no config field, no `#if DEBUG`:
- **Debug build** (Rider run button) → verbose, console + file. - **Debug build** (Rider run button) → verbose, console + file.
- **Release build** (installed app) → minimal, file only. - **Release build** (installed app) → minimal, file only.
## Decisions (from brainstorming) ## Decisions (from brainstorming)
1. **Mechanism:** `#if DEBUG` build-config split. Rider builds `Debug`; the installer ships `Release`. 1. **Mechanism:** runtime build-config detection via the entry assembly's `DebuggableAttribute` (JIT optimizer disabled ⇒ Debug build). A single `BuildConfig.IsDebug` helper drives ordinary `if` branching — no `#if DEBUG` directives. Rider's run button builds `Debug`; the installer ships `-c Release`.
2. **Scope:** Worker **and** App/Ui. The desktop side currently has no log sink at all — UI/IPC failures vanish today. 2. **Scope:** Worker **and** App/Ui. The desktop side currently has no log sink at all — UI/IPC failures vanish today.
3. **Release behavior:** App/Ui log `Warning`+ to file (not silent — capture crashes). Worker unchanged at `Information`. 3. **Release behavior:** all three log `Warning`+ to file (not silent — capture crashes). Worker drops from its current `Information` to `Warning`.
4. **One shared log file** across both processes, unified timeline. 4. **One shared log file** across both processes, unified timeline.
5. **Correlation:** TaskId-based (option A). Enrich log lines with `TaskId` when one is in scope. No changes to the SignalR contract (`IWorkerClient`/`WorkerHub` untouched → test fakes untouched). 5. **Correlation:** TaskId-based (option A). Enrich log lines with `TaskId` when one is in scope. No changes to the SignalR contract (`IWorkerClient`/`WorkerHub` untouched → test fakes untouched).
@@ -22,14 +22,14 @@ Make debug logging rich enough to diagnose problems across the UI↔Worker bound
| Process | Debug build | Release build | | Process | Debug build | Release build |
|---|---|---| |---|---|---|
| Worker | `Debug` level, console + shared file | `Information` level, shared file | | Worker | `Debug` level, console + shared file | `Warning` level, shared file |
| App/Ui | `Debug` level, console + shared file | `Warning` level, shared file | | App/Ui | `Debug` level, console + shared file | `Warning` level, shared file |
## Shared log file ## Shared log file
- Single daily-rolling file: `~/.todo-app/logs/claudedo-.log` (Serilog appends the date). - Single daily-rolling file: `~/.todo-app/logs/claudedo-.log` (Serilog appends the date).
- `shared: true` on both processes' file sinks → Serilog coordinates multi-process writes via a global mutex. - `shared: true` on both processes' file sinks → Serilog coordinates multi-process writes via a global mutex.
- `retainedFileCountLimit: 7` (matches current Worker retention). - `retainedFileCountLimit: 2`.
- Each line is tagged with a `Process` property (`"worker"` / `"app"`) so the two sides are distinguishable in the interleaved timeline. - Each line is tagged with a `Process` property (`"worker"` / `"app"`) so the two sides are distinguishable in the interleaved timeline.
> The existing `worker-.log` is replaced by `claudedo-.log`. Task-run NDJSON (`{taskId}_run{n}.ndjson`) and `daily-prep.log` are **out of scope** — they are data streams, not diagnostic logs, and stay exactly as they are. > The existing `worker-.log` is replaced by `claudedo-.log`. Task-run NDJSON (`{taskId}_run{n}.ndjson`) and `daily-prep.log` are **out of scope** — they are data streams, not diagnostic logs, and stay exactly as they are.
@@ -61,9 +61,11 @@ This adds **no parameters** to the SignalR surface — correlation rides on the
A single shared helper keeps the two processes' Serilog setup from drifting. A single shared helper keeps the two processes' Serilog setup from drifting.
- **New:** `ClaudeDo.Data/Logging/LoggingSetup.cs` (or similar shared location reachable by both App and Worker) exposing the output template, the default-TaskId enricher, and a `ConfigureLogger(LoggerConfiguration, Process, logRoot)` that applies the `#if DEBUG` level/sink choices. Both processes call it so level/template/retention stay in sync. - **New project:** `ClaudeDo.Logging` — a small library both `ClaudeDo.App` and `ClaudeDo.Worker` reference (keeps `ClaudeDo.Data` free of any Serilog dependency). Contains:
- *Placement note:* must live in a project both `ClaudeDo.App` and `ClaudeDo.Worker` reference. `ClaudeDo.Data` qualifies; if it should not take a Serilog dependency, use a tiny new shared project. Decide during planning. - `BuildConfig.IsDebug` — checks the entry assembly's `DebuggableAttribute` (`IsJITOptimizerDisabled` ⇒ Debug build). Cached static.
- **Worker `Program.cs:34`:** replace the inline `UseSerilog` body with a call into the shared helper (`Process = "worker"`). - The output template and the default-TaskId enricher.
- `ConfigureLogger(LoggerConfiguration, processTag, logRoot)` — applies level/sink choices by branching on `BuildConfig.IsDebug` (Debug ⇒ `Debug` level + console + file; Release ⇒ `Warning` level + file only). Both processes call it so level/template/retention stay in sync.
- **Worker `Program.cs:34`:** replace the inline `UseSerilog` body with a call into the shared helper (`processTag = "worker"`).
- **App `Program.cs`:** add Serilog packages; build a logger via the shared helper (`Process = "app"`) and register it with `sc.AddLogging(b => b.AddSerilog(logger, dispose: true))`. App currently registers **no** logging at all, so this also makes `ILogger<T>` injection actually work UI-side. Remove/keep `.LogToTrace()` as appropriate (Avalonia internal trace, separate concern — leave it). - **App `Program.cs`:** add Serilog packages; build a logger via the shared helper (`Process = "app"`) and register it with `sc.AddLogging(b => b.AddSerilog(logger, dispose: true))`. App currently registers **no** logging at all, so this also makes `ILogger<T>` injection actually work UI-side. Remove/keep `.LogToTrace()` as appropriate (Avalonia internal trace, separate concern — leave it).
- **App shutdown:** flush/close the logger (`Log.CloseAndFlush()` or dispose via the container's existing `finally`). - **App shutdown:** flush/close the logger (`Log.CloseAndFlush()` or dispose via the container's existing `finally`).
@@ -77,7 +79,7 @@ A single shared helper keeps the two processes' Serilog setup from drifting.
## Testing ## Testing
- This is logging wiring; per project policy, no tests that spawn the real Claude CLI and no heavy test scaffolding for log output. - This is logging wiring; per project policy, no tests that spawn the real Claude CLI and no heavy test scaffolding for log output.
- Light verification: a unit-level check that the shared helper produces the expected minimum level per build config (Debug vs Release) and that the default enricher yields `-` when no `TaskId` is pushed. If asserting on `#if DEBUG` is awkward in a single test run, verify the Release path (the one that matters for prod) and smoke-test Debug manually from Rider. - Light verification: a unit-level check that the default enricher yields `-` when no `TaskId` is pushed, and (if practical) that `ConfigureLogger` wires the expected sinks. `BuildConfig.IsDebug` reflects the test assembly's own build config, so it can't be flipped within one run — assert each branch by passing the level/flag explicitly rather than relying on the ambient value, or verify the Release path and smoke-test Debug manually from Rider.
- Manual smoke test (documented, not automated): run from Rider, confirm console + `claudedo-.log` show `Debug` lines with `Process`/`SourceContext`; run a task and confirm both `app` and `worker` lines share the same `[TaskId]`. - Manual smoke test (documented, not automated): run from Rider, confirm console + `claudedo-.log` show `Debug` lines with `Process`/`SourceContext`; run a task and confirm both `app` and `worker` lines share the same `[TaskId]`.
## Out of scope ## Out of scope