OP-06: Emit an exception/error log entry¶
| Field | Value |
|---|---|
| Operation ID | OP-06 |
| Trigger | Caller invokes an exception-bearing overload of Debug/Info/Warning/Error/Fatal (taking an Exception plus message, optionally with propertyValues). |
| Inputs | Exception instance, message template, optional structured propertyValues. |
| Outputs | A generated correlation log ID (string), returned to the caller once exception formatting and serialization (step 1) complete without throwing and, when configured, the subsequent ILogger/Serilog dispatch (step 4) also completes without throwing — those pre-ID steps run before ID generation and can themselves throw first; when configured, the exception/message arguments written first to ILogger<ISaifLoggable> and then, only if that write does not throw, an ErrorLogItem (serialized to JSON) pushed as a Serilog property and dispatched through the pipeline, ultimately posted to the Logging API error route. |
| Primary components | SaifLogBase log method overloads, SaifLogBase.LogIfConfiguredWithException, ExceptionFormatService / ExceptionFormatServiceForWeb |
| Entry point | SaifLogBase.Info / Warning / Debug / Error / Fatal (exception-bearing overloads) |
Steps¶
- Format the exception into an
ErrorLogItem: - 1.1. When
ExFormatServiceis non-null, callExFormatService.GetErrorDetails(message, ex), capturing application name, environment, machine name, a process-identityUserName, error date, exception info (the caller'smessageplus the formatted stack trace), exception source, and textual state info — including, for anySAIFException, a JSON-serializedLog Detail:line perLogDetailsentry, where a per-detail serialization failure is caught and recorded as that entry's exception message instead of throwing (BR-LOG-10). - 1.2. Web host only: before the above capture runs,
ExceptionFormatServiceForWeb.GetErrorDetailsfirst constructs aWebRequestDetailsand callsInitialize(_contextAccessor.HttpContext)— theHttpContextitself is not null-checked before this call, andcontext.Useris dereferenced unconditionally insideInitialize(no null-context guard), so a genuinelynullHttpContextthrows here rather than being skipped. When a context is present,Initializecopies every request header and every claim intoRequestHeaders/UserClaimswith no allow-list or redaction, then evaluates the guardcontext != null && context.Request != null && context.Request.Body != null && context.Request.Body.CanReadbefore touching the body stream at all: only when that guard is true does it seek the stream to position 0, read it to completion via a blockingStreamReader.ReadToEndAsync().Resultcall, and reset the stream back to position 0; when the guard is false,RequestBodyis left at its already-initialized empty string — it is neither read nor faulted. The body-handling block is wrapped in a try/catch that handles onlyObjectDisposedException, so any other read failure propagates out of this step. Within the web override'sBuildErrorLogItem,base.BuildErrorLogItem's object initializer runs first — including its call to the virtualBuildStateInfo, which (because of virtual dispatch) resolves to the web override and appends theWebRequestDetailstext intoErrorLogItem.StateInfo— and only after that base construction completes does the web override copy_webRequestDetails'sPageName/ClientUsernameonto the already-builtErrorLogItem's ownPageName/UserName(OP-07). - 1.3. When
ExFormatServiceisnull, substitute an emptynew ErrorLogItem()instead of calling a formatter — every field is left at its type default.
This formatting (or substitution) and the subsequent JsonConvert.SerializeObject call happen unconditionally, before the configured check in the next steps, and can themselves throw — reaching correlation-ID generation in step 3 is not guaranteed.
Origin: Steps 1.1–1.2's request-detail capture and
SAIFException.LogDetailsserialization are implemented in Format a web-context exception (ExceptionFormatServiceForWeb/ExceptionFormatService.BuildStateInfo) and executed on behalf of this operation. 2. Push the serializedErrorLogItemas a Serilog property (LogExceptionProperty), then callActivity.Current?.SetTag("message", message)and the matchingexception.message/exception.type/exception.stacktracetags — because these are null-conditional calls onActivity.Current, the right-hand arguments (ex.Message,ex.GetType().FullName,ex.ToString()) are evaluated only when anActivityis actually active; a genuinelynullexception throwsNullReferenceExceptionat this step only whenActivity.Currentis non-null, and is not dereferenced here at all when no activity is active. 3. Generate a correlation log ID — timestamp concatenated with a new GUID,:stripped (BR-LOG-07) — and push it onto the SerilogLogContext, taggingActivity.Currentas well; this happens unconditionally, before the configured check, but only once steps 1–2 have completed without throwing. 4. CheckIsConfigured. Whentrue, callILogger<ISaifLoggable>'s matching method with the exception and message/propertyValues arguments directly (not theErrorLogItemobject) first, and then, only if that call does not throw, call the static SerilogLoginstance the same way — the two calls run sequentially with no try/catch between them, not in parallel. Whenfalse, write a Debug-level diagnostic trace instead — noILogger/Serilog write occurs. 5. Return the generated log ID to the caller, provided steps 1–4 all completed without throwing — a throwingILoggercall in step 4 propagates out instead of reaching this return, even though the ID was already generated in step 3. 6. When the active pipeline includes aLoggingApiSink(web and Windows Service hosts): - 6.1. The sink checks whether the dispatched event carries both an exception and theLogExceptionDetailsproperty. - 6.2. If so, it posts the error entry (theErrorLogItemextracted viaLogEventExtensions.LogExceptionDetail) to the configured error route, suffixed?errorId={LogId}when a log ID is present; otherwise it renders the event through the configured template and posts aLogItemto the standard route instead. - 6.3. After either branch, it inspects the HTTP result and reports an unsuccessful POST toConsole/Debugonly — an exception thrown by the POST call itself is not caught anywhere in the sink.Origin: Steps 6.1–6.3 are implemented in Post a log entry to the Logging API (
LoggingApiSink.Emit) and executed on behalf of this operation.
Process flow¶
flowchart TD
A[Exception log method invoked] --> B0{ExFormatService null?}
B0 -->|Yes| B1["Substitute empty new ErrorLogItem()"]
B0 -->|No| C0{Web host?}
C0 -->|Yes| C1["Initialize WebRequestDetails:<br>HttpContext not null-checked,<br>copy all headers/claims,<br>body seek/read/reset only if<br>context/Request/Body.CanRead guard passes<br>(ObjectDisposedException only)"]
C0 -->|No| B["Format exception into ErrorLogItem via GetErrorDetails"]
C1 --> B
B --> F["Serialize ErrorLogItem,<br>push as LogExceptionProperty"]
B1 --> F
F --> F2["Activity.Current?.SetTag(...) -<br>ex.Message etc. evaluated only if Activity active"]
F2 --> G["Generate correlation log ID,<br>push to LogContext + Activity (unconditional)"]
G --> H{IsConfigured?}
H -->|No| H1["Debug.Write diagnostic trace only"]
H -->|Yes| I["Call ILogger with exception + message/propertyValues<br>(not ErrorLogItem object)"]
I --> |"no throw"| J["Call static Serilog Log the same way"]
I --> |"throws"| X["Exception propagates;<br>Serilog write and ID return skipped"]
J --> K["LoggingApiSink: IsException true -><br>posts ErrorLogItem to error route - OP-08"]
H1 --> L[Return generated log ID]
K --> L
Failure and edge-case behavior¶
- A generated correlation ID is returned only once the pre-ID steps (exception formatting/serialization in step 1, and
Activity/LogContexttagging in steps 2–3) complete without throwing, and — once configured — only once the step-4ILoggercall also completes without throwing, since that call runs before the Serilog call and the return with no surrounding try/catch; ifExFormatService.GetErrorDetails,JsonConvert.SerializeObject, the step-2 dereference, or the step-4ILoggercall throws, the exception propagates to the caller and no ID is returned at all, even though it was already generated in step 3. When unconfigured, only theILogger/Serilog write is skipped in favor of a Debug-level diagnostic trace, and the already-generated ID is still returned (BR-LOG-08). - When the logger instance has no
ExFormatServiceattached (for example, before anyConfigure/ConfigureLoggerscall has run), the serializedErrorLogItemis an empty, all-default instance rather than a populated one — the exception's own message/stack trace still reachILogger/Serilog directly through step 4, but the structured error payload posted to the Logging API error route carries no exception information. - For the web host, a genuinely
nullHttpContext(for example, a background task running outside a request) is not gracefully skipped duringWebRequestDetails.Initialize— the dereference ofcontext.Usercan throw beforeErrorLogItemconstruction even reaches step 2 (OP-07). SaifWindowsServiceLogposts error entries with anullerror route because it never readslogerrorroute(BR-LOG-09).LoggingApiSink.Emitisasync voidwith no try/catch; a failed POST to the Logging API error route is only reported via an unsuccessfulRequestResultcheck, written to Console/Debug — an exception thrown by the POST call itself is not caught anywhere in this sink (BR-LOG-15).
Related documentation¶
Sources¶
src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SaifLogBase.cssrc/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ExceptionFormatter/ExceptionFormatService.cssrc/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ExceptionFormatter/ExceptionFormatServiceForWeb.cssrc/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ExceptionFormatter/WebRequestDetails.cs