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
ServiceContractslogging interfaces (ISaifLogService,ISaifWindowsServiceLogService) and the standaloneIConsoleLogServicecontract for three host styles. - Generate a correlation log ID per entry and propagate it to the Serilog
LogContextand toActivity.Currenttags. - Format exceptions into an
ErrorLogItemcarrying 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.SaifLogbuilds an Event Log sink (level fromEventLogConfig), aLoggingApiSink, and a Console sink.SaifWindowsServiceLogbuilds an Event Log sink (manageEventSource: true), aLoggingApiSink, and a Console sink as well — both concrete loggers chain.WriteTo.Console()onto theirLoggerConfiguration.ConsoleLogServicebuilds an Event Log sink hardcoded toLogEventLevel.Fataland a Console sink — noLoggingApiSink— plus, as a separate, independently-thresholded channel, up to five auxiliaryLogWriterfile writers reached only through explicitWriteToAppLogorGetLogcalls.ExceptionFormatter/*: builds theErrorLogItem/WebRequestDetailspayloads consumed by the error-logging path;ExceptionFormatServiceForWebadds captured HTTP request state for the web logger only.Loggers/LoggingApiSink: the SerilogILogEventSinkboundary that owns the only outbound network call this library makes, viaServiceContracts'IHttpJsonClient.Extensions/*andWeb/WebHostBuilderExtensions: configuration-mapping and host-wiring helpers (ToLoggerConfig,UseSaifLog).
Interaction model¶
- A host (through
SAIF.NetCore.ServiceExtensionsregistration) resolvesISaifLogService,ISaifWindowsServiceLogService, orIConsoleLogServiceand callsConfigure(environmentClient, httpJsonClient[, httpContextAccessor]). SaifLogBase.ConfigureLoggersresolves the base threshold, builds/overrides the standard sink configs, and populatesLevels/Patterns, but does not read the Logging API URL/routes, does not build or assign the SerilogLoggerConfigurationitself, and does not setIsConfigured; each concrete logger's ownConfiguremethod (SaifLog.Configure,SaifWindowsServiceLog.Configure) readstracingdefaults:loggingapiurl/logroute/logerrorroutefromIEnvironmentClientafter callingConfigureLoggers, then builds the SerilogLoggerConfiguration, assigns it to the process-globalSerilog.Log.Logger(replacing any prior configuration), and only then setsIsConfigured = true—ConfigureLoggersalone never makes the logger ready to dispatch.- Each non-exception-bearing log call (
Info,Error,Debug,Warning, orFatalwithout anExceptionargument) generates a correlation ID first and, only whenIsConfiguredis true, writes sequentially to the MicrosoftILogger<ISaifLoggable>and then, if that call did not throw, to the configured Serilog sinks for that host style — Event Log,LoggingApiSink, and Console for bothSaifLogandSaifWindowsServiceLog, or Event Log (Fatal-only) and Console forConsoleLogService; there is no try/catch around either call, so an exception from theILoggerwrite propagates out and prevents both the Serilog write and the normal return of the generated ID to the caller. For exception-bearing overloads,LogIfConfiguredWithExceptionformats/serializes the exception and tagsActivity.Currentbefore ID generation runs, so a failure in that pre-processing (for example, a formatter failure) prevents an ID from being generated at all, independent ofIsConfigured. Whether a given consumer actually observes an entry that is written still depends on that consumer's own provider/sink filtering. LoggingApiSink.Emitis the sole outbound call path: it POSTs anErrorLogItem(to the error route, when an exception andLogExceptionDetailsare present) or aLogItem(to the standard route) to the SAIF Logging API throughIHttpJsonClient.PostAsync, and inspectsIHttpJsonClient.RequestResultto detect failures.SAIF.NetCore.Services.Messagingconsumes this package only for an in-process string-to-enum conversion utility (LogLevelExtensions.ToLogLevel); it reaches the actual logging/outbound-call behavior exclusively through theServiceContractsinterfaces, 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<ISaifLoggable>"]
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¶
Configureis not idempotent — a second call causesLevels.Addto throw a duplicate-keyArgumentException*, and each call replaces the process-globalSerilog.Log.Logger, so multiple logger instances in one process overwrite each other's sinks.LoggingApiSink.Emitisasync void(fire-and-forget) with no try/catch anywhere in the class; because it isasync voidrather thanasync Task, an exception thrown by the POST call itself is not captured on aTaskfor 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
EventLogsink ships on cross-platform target frameworks with no[SupportedOSPlatform]guard, and the console logger's file sink defaults to a hard-codedC:/temp/logs/path when unconfigured. - An unsuccessful HTTP result (
RequestResult.IsSuccessful == false) is reported only toConsole/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. SaifWindowsServiceLognever setsLoggingApiErrorRoute, so its error-route POSTs carry a null route.ExceptionFormatServiceForWebstores the per-requestWebRequestDetailsit builds in a mutable instance field (_webRequestDetails), butSAIF.NetCore.ServiceExtensionsregisters the owningSaifLoginstance as a singleton (services.AddSingleton<ISaifLogService, SaifLog>()); because a singletonSaifLogholds a singleExceptionFormatServiceForWebinstance across all requests, concurrent exception-formatting calls on different threads can race on_webRequestDetailsbetweenGetErrorDetailsassigning it andBuildErrorLogItem/BuildStateInforeading it back, potentially attributing one request's captured details to another request's error entry*.
Related documentation¶
- Index
- Overview
- Dependencies
- Configuration
- Service registration
- Operations and processes
- Business rules
- API reference
- SAIF.NetCore.ServiceContracts
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.cssrc/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SaifLog.cssrc/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ExceptionFormatter/ExceptionFormatServiceForWeb.cssrc/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/SAIF.NetCore.ServiceExtensions/IServiceCollectionExtensions.cssrc/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SaifWindowsServiceLog.cssrc/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ConsoleLogService.cssrc/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Loggers/LoggingApiSink.cssrc/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ExceptionFormatter/ExceptionFormatService.csArchitecture.md