--- status: Active lastUpdated: 2026-09-16 applyTo: - "Fuchs/Program.cs" - "Fuchs/Logging/**" - "Fuchs/code/FuchsIntranet.cs" - "Fuchs/Controllers/**" - "Fuchs/Observability/FuchsTelemetry.cs" relatedDecisions: - "0003-structured-diagnostic-logging.md" - "0003-interrupting-exceptions-notify-the-user.md" - "0004-opentelemetry-observability.md" - "0023-user-action-failures-persisted-to-db-log.md" - "0024-multilogger-sql-log-sink.md" --- # Error logging pipeline ## Summary A `_logger.LogWarning`/`-Error`/`-Critical` call anywhere in the app — or in OCORE code called from it — reaches three places automatically: the always-on file sink, a DB row in `[dbo].[fds__debuglog]` (what support actually queries), and, for a user-initiated action that threw unhandled, a SignalR toast to the user. One `ILogger` call is the single thing a call site writes; nothing calls a stored procedure directly for attribution logging. ## How it works ### The two `ILoggerProvider`s - **`FuchsLoggerProvider`** (`Fuchs/Logging/FuchsLoggerProvider.cs`, [0003](0003-structured-diagnostic-logging.md)): every level ≥ Debug → `Debug.WriteLine` + a rolling-free text file (`logs/AppLog.txt` / `logs/ErrorLog.txt`). Always on, no DB dependency. - **`OCORE.MultiLogger.SqlLoggerProvider`** ([0024](0024-multilogger-sql-log-sink.md)): registered in `Program.cs` via `builder.Logging.AddSqlLogger()`, gated by `Fuchs:Logging:SqlSink:Enabled` (default on), filtered to `LogLevel.Warning`+. Writes via `EXECUTE [dbo].[fds__admin_logdebug_ext]` into `[dbo].[fds__debuglog]` — the same table the legacy, now-unused `fds__admin_logdebug` proc wrote to. ### Attribution: EventId, category, scope - **`CodeReference`** (the row's short label) is `COALESCE(EventId.Name, ILogger category, 'MultiLogger')`, computed in the stored procedure. Give a log call an explicit `EventId` when the call site should be individually greppable: ```csharp _logger.LogError(new EventId(0, "SQL Error in getSQLDataSet"), ex, "SQL error in {Procedure}: ...", ...); ``` See `FIS_SQLOptions.OnError`, `IntranetController.Do`'s catch-all, `PdfPreviewAsync`, and the `bam/up` (banking upload) handlers for the pattern — each preserves the exact string the old, explicit `debug_log()` calls used, so existing saved queries over `CodeReference` still match. - **`RequestLogScopeMiddleware`** (`Fuchs/Logging/RequestLogScopeMiddleware.cs`, registered after `app.UseAuthentication()`) pushes `{requestId, path, authuser}` as an `ILogger` scope for the whole request. Every log line written while handling it — including ones written deep inside OCORE's SQL helpers — carries that automatically, without the call site doing anything. `authuser` specifically lands in the dedicated `fds__debuglog.AuthUser` column (OCORE's `MultiLogger_base.Log` picks the `"authuser"` scope key out into `LogEntry.AuthUser`); the rest of the scope + any `{Placeholder}` args + an explicit `data:` object are merged into one redacted JSON blob (`fds__debuglog.Data`). - **No request/service-scoped logger available** (`new FIS_SQLOptions()` — ~15 call sites in `Services/*.cs` construct it this way): `FIS_SQLOptions.OnError` falls back to the static `OCORE.Logging.Logger` (set once in `Program.cs` to `app.Logger`), so those SQL errors are still attributed, just under a less specific category. ### Placeholder naming Always `{Foo}`, never `{@Foo}`. Plain `Microsoft.Extensions.Logging` (not Serilog) does not treat `@` as a destructuring operator — it stays a literal leading character of the resulting property name (`"@Foo"`), which is confusing in the serialized `Data` column. ### Redaction and cost `OCORE.MultiLogger.LogRedaction.SerializeRedacted` masks property names matching known-sensitive fragments/segments (`password`, `secret`, `token`, `pin`, `otp` as a whole identifier segment — not substrings like `Mapping`/`HotPath`) at any nesting depth, degrades one unserializable context entry to its `ToString()` instead of losing the whole payload, and tolerates self-referencing graphs. `LogEntry.dataSerialized` is computed lazily — at most once, only if a sink actually reads it — so a Warning+ call that no sink is currently listening for costs nothing extra to serialize. ### User-visible failure surface (unrelated to the DB write, [0023](0023-user-action-failures-persisted-to-db-log.md)) - `IntranetController.Do`'s catch-all publishes a SignalR toast (`IEventService.UserIssueAsync`) for any unmodeled exception, for authenticated users only. - A 500 response carries its message in the `x-ocms-desc` header (`ServerError(msg)` → `EncodeResponseDescription`), which the frontend's default failure handler (`$ocms.failure`/`oci_basic.js`) reads. - PDF-preview endpoints go through the shared `PdfPreviewAsync` guard: logs, counts `fuchs.pdf.previews{what,outcome}` (`success`/`gone`/`failure` — an expired-draft `410` is its own `gone` outcome, not a render failure), toasts an authenticated caller, and returns a message with the innermost exception text (omitted for the handful of preview endpoints reachable without authentication — see `_allowedGet` in `IntranetController.cs`). ## Configuration - `Fuchs:Logging:SqlSink:Enabled` (bool, default `true`) — off switch for the SQL sink without a redeploy. - SQL sink connects via `fuchs_fds_ConnectionString` (same connection as everything else), proc `dbo.fds__admin_logdebug_ext`, `TruncateTextAt = 4000`. ## Extending this - Adding a new sink (email/SMS on Critical, a rolling file replacing `FuchsLoggerProvider`) is a registration in `Program.cs` — `OCORE.MultiLogger` already ships `EmailLoggerOptions`/ `SMSLoggerOptions`/`FileLoggerProvider`. - A new place that needs DB attribution just calls `_logger.LogWarning`/`-Error` with an `EventId` if it wants a specific `CodeReference` — never add a second, explicit stored-procedure call next to it; that duplicates the row once the SQL sink picks the `ILogger` call up.