feat: migrate DB error logging onto the generic OCORE MultiLogger SQL sink
Playwright Tests / test (push) Canceled after 0s
Playwright Tests / test (push) Canceled after 0s
Retires Fuchs_intranet.debug_log() and every direct EXECUTE [dbo].[fds__admin_logdebug] call in favor of a single path: ILogger. A new SQL log sink (OCORE.MultiLogger.SqlLoggerProvider, gated by Fuchs:Logging:SqlSink:Enabled, default on) mirrors every _logger.LogWarning/-Error/ -Critical call into [dbo].[fds__debuglog] via a new fds__admin_logdebug_ext proc, at Warning+. - Program.cs registers the sink and RequestLogScopeMiddleware (request id/path/authenticated user, pushed after UseAuthentication) so every log line in a request — including ones written deep inside OCORE's SQL helpers — is attributed automatically. - FIS_SQLOptions, IntranetController.Do's catch-all, PdfPreviewAsync, and the banking-upload handlers now log once, with an explicit EventId preserving the exact CodeReference text the retired debug_log() calls used to write, so existing dashboards/queries over fds__debuglog keep working unchanged. - Fuchs_DataService/FdsMfr.cs's FdsSqlOptions had the identical double-write hazard (it's hosted in the same process per ADR 0010) — the original 2026-09-16 log review's mfr__getSchema/ fds__getUpdateableRequests deadlock rows came from exactly this class. Fixed the same way. - fds__debuglog gains LogLevel/Machine/Category/Account/AuthUser/IsDebug columns; the legacy fds__admin_logdebug proc is left in place unused, not dropped. - FdsDebug.DebugLog (Fuchs_DataService's other ~9 call sites) deliberately left on its direct DB-write path — no ILogger call sits alongside it there, so no duplication risk today. See ADR 0024 (supersedes 0023) and Docs/Concepts/error-logging-pipeline.md. New tests: ErrorLoggingTests (FIS_SQLOptions), FdsSqlOptionsTests, RequestLogScopeMiddlewareTests. 694/694 pass (the 4 PdfPipelineTests failures seen under full-suite parallel runs are a pre-existing, unrelated PdfSharp global-font-resolver test-isolation flake — they pass both individually and in most full-suite runs). Bumps the OCORE submodule to 3137b56 (SQL sink hardening: redaction matches short fragments as whole identifier segments, per-entry-tolerant serialization, lazy dataSerialized, opt-in SqlLoggerOptions.IncludeCategory, LogEntry.Category/AuthUser capture). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a1e6002c3b
commit
bcf1f2dbb6
+15
-56
@@ -153,25 +153,6 @@ public class Fuchs_intranet
|
||||
return val.Exception != "" ? -1 : val.Result;
|
||||
}
|
||||
|
||||
// -- Logging ---------------------------------------------------------------
|
||||
public void debug_log(string procedure, Exception? ex = null, string authuser = "", object? data = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pl = new List<SqlParameter>
|
||||
{
|
||||
SQL_VarChar("@CodeReference", procedure.Left(200)),
|
||||
SQL_NVarChar("@ExceptionMessage", ex?.Message ?? "", dbNull_IfEmpty: true),
|
||||
SQL_NVarChar("@StackTrace", ex?.StackTrace ?? "", dbNull_IfEmpty: true),
|
||||
SQL_NVarChar("@Data", data != null ? Newtonsoft.Json.JsonConvert.SerializeObject(data) : "", dbNull_IfEmpty: true)
|
||||
};
|
||||
string dbEx = ""; int? dbCode = null;
|
||||
setSQLValue("EXECUTE [dbo].[fds__admin_logdebug] @CodeReference, @ExceptionMessage, @StackTrace, @Data;",
|
||||
Intranet_SqlCon(), ref dbEx, ref dbCode, pl, Security: GetDbSecurity(authuser));
|
||||
}
|
||||
catch { /* swallow logging errors */ }
|
||||
}
|
||||
|
||||
// -- PDF license -----------------------------------------------------------
|
||||
public static void SetPdfLicense()
|
||||
{
|
||||
@@ -219,52 +200,30 @@ public class FuchsUserIdentity
|
||||
// --------------------------- SQL options -------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Fuchs-specific SQL options — logs every SQL error both to the app's structured
|
||||
/// <see cref="ILogger"/>/OpenTelemetry pipeline and to the <c>fds__admin_logdebug</c> SQL
|
||||
/// table (via <see cref="Fuchs_intranet.debug_log"/>). Handlers that don't separately inspect
|
||||
/// the result's <c>.Exception</c> would otherwise return an empty/200 response on a failing
|
||||
/// stored procedure with zero application-log signal — the DB table alone requires someone to
|
||||
/// go looking for it.
|
||||
/// Fuchs-specific SQL options — logs every SQL error through the app's structured
|
||||
/// <see cref="ILogger"/> pipeline (which, at Warning+, also lands in <c>fds__debuglog</c> via
|
||||
/// the SQL log sink — see ADR 0024). Handlers that don't separately inspect the result's
|
||||
/// <c>.Exception</c> would otherwise return an empty/200 response on a failing stored procedure
|
||||
/// with zero log signal.
|
||||
/// </summary>
|
||||
public class FIS_SQLOptions : sqloptions
|
||||
{
|
||||
public FIS_SQLOptions(Dictionary<string, object>? context = null, ILogger? logger = null)
|
||||
: this(context, logger, dbLog: (procedure, ex, data) => FuchsOcmsIntranet.Instance.debug_log(procedure, ex, data: data))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test seam: <paramref name="dbLog"/> receives what would otherwise go to
|
||||
/// <see cref="Fuchs_intranet.debug_log"/> (code reference, exception, serialisable data).
|
||||
/// </summary>
|
||||
internal FIS_SQLOptions(Dictionary<string, object>? context, ILogger? logger,
|
||||
Action<string, Exception?, object?> dbLog)
|
||||
{
|
||||
OnError = (procedure, ex, data) =>
|
||||
{
|
||||
// OCORE hands the failing statement in `data` (new { sql = ... }); `context` is the
|
||||
// caller's fn/id/code. Both are needed to attribute a timeout in the DB log — logging
|
||||
// only the (usually null) context left every "SQL Error in getSQLDataSet" row without
|
||||
// the SQL that timed out.
|
||||
var logData = BuildLogData(context, data);
|
||||
logger?.LogError(ex, "SQL error in {Procedure}: {Message} — sql={@Sql} context={@Context}",
|
||||
// caller's fn/id/code. Both are needed to attribute a timeout in the log — logging only
|
||||
// the (usually null) context left every "SQL Error in getSQLDataSet" row without the
|
||||
// SQL that timed out. Falls back to the static OCORE.Logging.Logger (set once in
|
||||
// Program.cs) when no request/service-scoped logger was supplied — several callers
|
||||
// construct `new FIS_SQLOptions()` with neither logger nor context, and those must
|
||||
// still reach the log, not silently drop it. EventId.Name keeps the exact
|
||||
// "SQL Error in {procedure}" CodeReference the DB log used before this pipeline existed.
|
||||
var lg = logger ?? OCORE.Logging.Logger;
|
||||
lg?.LogError(new EventId(0, $"SQL Error in {procedure}"), ex,
|
||||
"SQL error in {Procedure}: {Message} — sql={Sql} context={Context}",
|
||||
procedure, ex.Message, data, context);
|
||||
try { dbLog($"SQL Error in {procedure}", ex, logData); }
|
||||
catch { }
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Merges the OCORE error payload (the SQL) with the caller context into one JSON-able object.</summary>
|
||||
internal static Dictionary<string, object?> BuildLogData(IReadOnlyDictionary<string, object>? context, object? data)
|
||||
{
|
||||
var d = new Dictionary<string, object?>();
|
||||
if (data != null)
|
||||
{
|
||||
// OCORE passes new { sql = "..." }; unwrap so the log column reads as the plain statement.
|
||||
var sqlProp = data.GetType().GetProperty("sql");
|
||||
d["sql"] = sqlProp?.GetValue(data) ?? data;
|
||||
}
|
||||
if (context is { Count: > 0 }) d["context"] = context;
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user