Skip to content

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.

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.

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*.

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