---
title: "OP-06: Emit an exception/error log entry"
summary: How an exception-bearing log call is formatted and dispatched.
status: draft
owner: SAIF Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.Logging
reference_type: package
operation_id: OP-06
trigger: Caller invokes one of SaifLogBase's exception-bearing log method overloads (e.g. Error(Exception ex, string message) or Error(Exception ex, string message, params object[] propertyValues)).
---

# 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`:
   - 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](../business-rules.md#br-log-10-exception-entries-formatted-or-defaulted-to-errorlogitem)).
   - 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](op-07-format-web-context-exception.md)).
   - 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](op-07-format-web-context-exception.md) (`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](../business-rules.md#br-log-07-log-id-generation-precedes-dispatch-after-exception-formatting)) — 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](op-08-post-log-entry-to-logging-api.md) (`LoggingApiSink.Emit`) and executed on behalf of this operation.

## Process flow

```mermaid
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](../business-rules.md#br-log-08-log-entries-drop-until-logger-is-configured)).
- 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](op-07-format-web-context-exception.md)).
- `SaifWindowsServiceLog` posts error entries with a `null` error route because it never reads `logerrorroute` ([BR-LOG-09](../business-rules.md#br-log-09-windows-service-logger-never-sets-error-route)).
- `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](../business-rules.md#br-log-15-emit-is-async-void-with-no-exception-containment)).

## Related documentation
- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)

## 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`
