Skip to content

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

  1. Format the exception into an ErrorLogItem:
  2. 1.1. When ExFormatService is non-null, call ExFormatService.GetErrorDetails(message, ex), capturing application name, environment, machine name, a process-identity UserName, error date, exception info (the caller's message plus the formatted stack trace), exception source, and textual state info — including, for any SAIFException, a JSON-serialized Log Detail: line per LogDetails entry, where a per-detail serialization failure is caught and recorded as that entry's exception message instead of throwing (BR-LOG-10).
  3. 1.2. Web host only: before the above capture runs, ExceptionFormatServiceForWeb.GetErrorDetails first constructs a WebRequestDetails and calls Initialize(_contextAccessor.HttpContext) — the HttpContext itself is not null-checked before this call, and context.User is dereferenced unconditionally inside Initialize (no null-context guard), so a genuinely null HttpContext throws here rather than being skipped. When a context is present, Initialize copies every request header and every claim into RequestHeaders/UserClaims with no allow-list or redaction, then evaluates the guard context != null && context.Request != null && context.Request.Body != null && context.Request.Body.CanRead before 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 blocking StreamReader.ReadToEndAsync().Result call, and reset the stream back to position 0; when the guard is false, RequestBody is 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 only ObjectDisposedException, so any other read failure propagates out of this step. Within the web override's BuildErrorLogItem, base.BuildErrorLogItem's object initializer runs first — including its call to the virtual BuildStateInfo, which (because of virtual dispatch) resolves to the web override and appends the WebRequestDetails text into ErrorLogItem.StateInfo — and only after that base construction completes does the web override copy _webRequestDetails's PageName/ClientUsername onto the already-built ErrorLogItem's own PageName/UserName (OP-07).
  4. 1.3. When ExFormatService is null, substitute an empty new 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.LogDetails serialization are implemented in Format a web-context exception (ExceptionFormatServiceForWeb/ExceptionFormatService.BuildStateInfo) and executed on behalf of this operation. 2. Push the serialized ErrorLogItem as a Serilog property (LogExceptionProperty), then call Activity.Current?.SetTag("message", message) and the matching exception.message/exception.type/exception.stacktrace tags — because these are null-conditional calls on Activity.Current, the right-hand arguments (ex.Message, ex.GetType().FullName, ex.ToString()) are evaluated only when an Activity is actually active; a genuinely null exception throws NullReferenceException at this step only when Activity.Current is 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 Serilog LogContext, tagging Activity.Current as well; this happens unconditionally, before the configured check, but only once steps 1–2 have completed without throwing. 4. Check IsConfigured. When true, call ILogger<ISaifLoggable>'s matching method with the exception and message/propertyValues arguments directly (not the ErrorLogItem object) first, and then, only if that call does not throw, call the static Serilog Log instance the same way — the two calls run sequentially with no try/catch between them, not in parallel. When false, write a Debug-level diagnostic trace instead — no ILogger/Serilog write occurs. 5. Return the generated log ID to the caller, provided steps 1–4 all completed without throwing — a throwing ILogger call 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 a LoggingApiSink (web and Windows Service hosts): - 6.1. The sink checks whether the dispatched event carries both an exception and the LogExceptionDetails property. - 6.2. If so, it posts the error entry (the ErrorLogItem extracted via LogEventExtensions.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 a LogItem to the standard route instead. - 6.3. After either branch, it inspects the HTTP result and reports an unsuccessful POST to Console/Debug only — 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/LogContext tagging in steps 2–3) complete without throwing, and — once configured — only once the step-4 ILogger call also completes without throwing, since that call runs before the Serilog call and the return with no surrounding try/catch; if ExFormatService.GetErrorDetails, JsonConvert.SerializeObject, the step-2 dereference, or the step-4 ILogger call 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 the ILogger/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 ExFormatService attached (for example, before any Configure/ConfigureLoggers call has run), the serialized ErrorLogItem is an empty, all-default instance rather than a populated one — the exception's own message/stack trace still reach ILogger/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 null HttpContext (for example, a background task running outside a request) is not gracefully skipped during WebRequestDetails.Initialize — the dereference of context.User can throw before ErrorLogItem construction even reaches step 2 (OP-07).
  • SaifWindowsServiceLog posts error entries with a null error route because it never reads logerrorroute (BR-LOG-09).
  • LoggingApiSink.Emit is async void with no try/catch; a failed POST to the Logging API error route is only reported via an unsuccessful RequestResult check, written to Console/Debug — an exception thrown by the POST call itself is not caught anywhere in this sink (BR-LOG-15).

Sources

  • src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SaifLogBase.cs
  • src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ExceptionFormatter/ExceptionFormatService.cs
  • src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ExceptionFormatter/ExceptionFormatServiceForWeb.cs
  • src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ExceptionFormatter/WebRequestDetails.cs