---
title: SAIF.NetCore.Logging architecture
summary: Architectural responsibilities, boundaries, and interaction patterns.
status: draft
owner: SAIF Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.Logging
reference_type: package
---

# SAIF.NetCore.Logging architecture

SAIF.NetCore.Logging is a Framework.NCore implementation-layer library that supplies the SAIF platform's structured logging engine for three host styles — Web (`SaifLog`), Windows Service (`SaifWindowsServiceLog`), and Console (`ConsoleLogService`) — all derived from a shared `SaifLogBase`, writing each log entry sequentially to Microsoft's `ILogger<ISaifLoggable>` first and then to a process-global Serilog pipeline, with no try/catch between the two calls, so a throwing `ILogger` call prevents the Serilog call from running*; it ships structured log and error records to the central SAIF Logging API over HTTP, depends only on the foundation package `SAIF.NetCore.ServiceContracts`, and is itself consumed by `SAIF.NetCore.ServiceExtensions` and, as one of the three documented implementation-to-implementation exceptions, by `SAIF.NetCore.Services.Messaging`.

## Responsibilities
- Implement the `ServiceContracts` logging interfaces (`ISaifLogService`, `ISaifWindowsServiceLogService`) and the standalone `IConsoleLogService` contract for three host styles.
- Generate a correlation log ID per entry and propagate it to the Serilog `LogContext` and to `Activity.Current` tags.
- Format exceptions into an `ErrorLogItem` carrying machine, user, stack trace, `SAIFException.LogDetails`, and — for the web logger — captured HTTP request details.
- Resolve log thresholds, patterns, and the Logging API base URL/routes from the SAIF Environment API (`IEnvironmentClient`), with appsettings overrides.
- Ship non-error and error log records to the central SAIF Logging API via `IHttpJsonClient`, through a custom Serilog sink (`LoggingApiSink`).

## Component boundaries
- `SaifLogBase`: shared abstract engine — level switch, per-sink levels/patterns, the 20 overloaded log methods, and the configured guard (`IsConfigured`).
- `SaifLog`, `SaifWindowsServiceLog`, `ConsoleLogService`: host-specific subclasses, each supplying its own sink set. `SaifLog` builds an Event Log sink (level from `EventLogConfig`), a `LoggingApiSink`, and a Console sink. `SaifWindowsServiceLog` builds an Event Log sink (`manageEventSource: true`), a `LoggingApiSink`, and a Console sink as well — both concrete loggers chain `.WriteTo.Console()` onto their `LoggerConfiguration`. `ConsoleLogService` builds an Event Log sink hardcoded to `LogEventLevel.Fatal` and a Console sink — no `LoggingApiSink` — plus, as a separate, independently-thresholded channel, up to five auxiliary `LogWriter` file writers reached only through explicit `WriteToAppLog` or `GetLog` calls.
- `ExceptionFormatter/*`: builds the `ErrorLogItem`/`WebRequestDetails` payloads consumed by the error-logging path; `ExceptionFormatServiceForWeb` adds captured HTTP request state for the web logger only.
- `Loggers/LoggingApiSink`: the Serilog `ILogEventSink` boundary that owns the only outbound network call this library makes, via `ServiceContracts`' `IHttpJsonClient`.
- `Extensions/*` and `Web/WebHostBuilderExtensions`: configuration-mapping and host-wiring helpers (`ToLoggerConfig`, `UseSaifLog`).

## Interaction model
- A host (through `SAIF.NetCore.ServiceExtensions` registration) resolves `ISaifLogService`, `ISaifWindowsServiceLogService`, or `IConsoleLogService` and calls `Configure(environmentClient, httpJsonClient[, httpContextAccessor])`.
- `SaifLogBase.ConfigureLoggers` resolves the base threshold, builds/overrides the standard sink configs, and populates `Levels`/`Patterns`, but does **not** read the Logging API URL/routes, does **not** build or assign the Serilog `LoggerConfiguration` itself, and does **not** set `IsConfigured`; each concrete logger's own `Configure` method (`SaifLog.Configure`, `SaifWindowsServiceLog.Configure`) reads `tracingdefaults:loggingapiurl`/`logroute`/`logerrorroute` from `IEnvironmentClient` after calling `ConfigureLoggers`, then builds the Serilog `LoggerConfiguration`, assigns it to the process-global `Serilog.Log.Logger` (replacing any prior configuration), and only then sets `IsConfigured = true` — `ConfigureLoggers` alone never makes the logger ready to dispatch.
- Each non-exception-bearing log call (`Info`, `Error`, `Debug`, `Warning`, or `Fatal` without an `Exception` argument) generates a correlation ID first and, only when `IsConfigured` is true, writes sequentially to the Microsoft `ILogger<ISaifLoggable>` and then, if that call did not throw, to the configured Serilog sinks for that host style — Event Log, `LoggingApiSink`, and Console for both `SaifLog` and `SaifWindowsServiceLog`, or Event Log (Fatal-only) and Console for `ConsoleLogService`; there is no try/catch around either call, so an exception from the `ILogger` write propagates out and prevents both the Serilog write and the normal return of the generated ID to the caller. For exception-bearing overloads, `LogIfConfiguredWithException` formats/serializes the exception and tags `Activity.Current` *before* ID generation runs, so a failure in that pre-processing (for example, a formatter failure) prevents an ID from being generated at all, independent of `IsConfigured`. Whether a given consumer actually observes an entry that is written still depends on that consumer's own provider/sink filtering.
- `LoggingApiSink.Emit` is the sole outbound call path: it POSTs an `ErrorLogItem` (to the error route, when an exception and `LogExceptionDetails` are present) or a `LogItem` (to the standard route) to the SAIF Logging API through `IHttpJsonClient.PostAsync`, and inspects `IHttpJsonClient.RequestResult` to detect failures.
- `SAIF.NetCore.Services.Messaging` consumes this package only for an in-process string-to-enum conversion utility (`LogLevelExtensions.ToLogLevel`); it reaches the actual logging/outbound-call behavior exclusively through the `ServiceContracts` interfaces, never through a direct call into this library's implementation types*.

## Architecture diagram
The following diagram shows how a host wires this library and how a log entry ultimately reaches the SAIF Logging API.

```mermaid
flowchart TB
    Host["Host app<br>(via ServiceExtensions registration)"]
    SLB["SaifLogBase<br>(SaifLog / SaifWindowsServiceLog / ConsoleLogService)"]
    ENV["IEnvironmentClient<br>(ServiceContracts)"]
    SER["Serilog.Log<br>(process-global)"]
    MSL["ILogger&lt;ISaifLoggable&gt;"]
    SINK["LoggingApiSink"]
    HTTP["IHttpJsonClient<br>(ServiceContracts)"]
    API["SAIF Logging API"]

    Host -->|"Configure(env, http)"| SLB
    SLB -->|thresholds/patterns/URLs| ENV
    SLB --> SER
    SLB --> MSL
    SER --> SINK
    SINK -->|"PostAsync"| HTTP
    HTTP -->|"HTTP POST"| API
```

## Sequence diagram
The following diagram shows the primary request/response sequence for an exception-logging call that is shipped to the SAIF Logging API.

```mermaid
sequenceDiagram
	participant Caller
	participant SaifLogBase
	participant ExceptionFormatService
	participant LoggingApiSink
	participant SaifLoggingApi as SAIF Logging API

	Caller->>SaifLogBase: Error(message, ex)
	SaifLogBase->>ExceptionFormatService: GetErrorDetails(message, ex)
	ExceptionFormatService-->>SaifLogBase: ErrorLogItem
	SaifLogBase->>SaifLogBase: GenerateLogId, push LogContext
	alt IsConfigured
		SaifLogBase->>LoggingApiSink: Emit(LogEvent)
		LoggingApiSink->>SaifLoggingApi: PostAsync(errorRoute?errorId, ErrorLogItem)
		SaifLoggingApi-->>LoggingApiSink: HTTP response
		LoggingApiSink-->>SaifLogBase: (fire-and-forget)
	else not configured
		SaifLogBase->>SaifLogBase: Debug.Write (skip log action)
	end
	SaifLogBase-->>Caller: log ID
```

## Design constraints
- `Configure` is not idempotent — a second call causes `Levels.Add` to throw a duplicate-key `ArgumentException`*, and each call replaces the process-global `Serilog.Log.Logger`, so multiple logger instances in one process overwrite each other's sinks.
- `LoggingApiSink.Emit` is `async void` (fire-and-forget) with no try/catch anywhere in the class; because it is `async void` rather than `async Task`, an exception thrown by the POST call itself is not captured on a `Task` for later observation — it is dispatched directly to the SynchronizationContext captured when Emit starts, or the thread pool if none was captured, and can surface as an unhandled exception that terminates the process, rather than something the caller can catch. Delivery before process shutdown is not guaranteed either way.
- The Windows-only `EventLog` sink ships on cross-platform target frameworks with no `[SupportedOSPlatform]` guard, and the console logger's file sink defaults to a hard-coded `C:/temp/logs/` path when unconfigured.
- An unsuccessful HTTP result (`RequestResult.IsSuccessful == false`) is reported only to `Console`/`Debug`, with no retry and no propagation to the caller; this is a best-effort result check, not a guarantee that every failure mode is caught or contained.
- `SaifWindowsServiceLog` never sets `LoggingApiErrorRoute`, so its error-route POSTs carry a null route.
- `ExceptionFormatServiceForWeb` stores the per-request `WebRequestDetails` it builds in a mutable instance field (`_webRequestDetails`), but `SAIF.NetCore.ServiceExtensions` registers the owning `SaifLog` instance as a singleton (`services.AddSingleton<ISaifLogService, SaifLog>()`); because a singleton `SaifLog` holds a single `ExceptionFormatServiceForWeb` instance across all requests, concurrent exception-formatting calls on different threads can race on `_webRequestDetails` between `GetErrorDetails` assigning it and `BuildErrorLogItem`/`BuildStateInfo` reading it back, potentially attributing one request's captured details to another request's error entry*.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Service registration](service-registration.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [API reference](api/index.md)
- [SAIF.NetCore.ServiceContracts](../saif-netcore-servicecontracts/overview.md)

> **Note:** Statements marked with `*` were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts.

## Sources
- `src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SaifLogBase.cs`
- `src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SaifLog.cs`
- `src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ExceptionFormatter/ExceptionFormatServiceForWeb.cs`
- `src/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cs`
- `src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SaifWindowsServiceLog.cs`
- `src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ConsoleLogService.cs`
- `src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Loggers/LoggingApiSink.cs`
- `src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ExceptionFormatter/ExceptionFormatService.cs`
- `Architecture.md`
