The application-named Environment API section ({ApplicationName}:logthreshold) is checked before the organization-wide tracingdefaults:logthreshold key when resolving the base log threshold for web and Windows Service hosts.
Trigger
SaifLogBase.ConfigureLoggers is called (from SaifLog.Configure or SaifWindowsServiceLog.Configure).
Enforcement
SaifLogBase.GetBaseLevel calls IEnvironmentClient.HasSetting(StringValues.LogThresholdKey, environmentClient.ApplicationName) first, falling back to GetSettingValue("logthreshold", "tracingdefaults").
Outcome
An application-specific threshold, when present, takes precedence over the organization default.
The base log threshold is parsed by title-casing the string and calling Enum.TryParse against SAIF.NetCore.Services.Contracts.LogLevel; the five named literals Debug, Info, Warn, Error, and Fatal (case-insensitive) parse to their matching enum member, but Enum.TryParse also accepts any numeric string that is representable by the enum's underlying integer type — including one with no corresponding named member (e.g. "7") — which succeeds without validation against Enum.IsDefined and later falls through to Serilog Information when converted via ToLogEventLevel. A non-numeric, unrecognized string throws ArgumentException, and so does a numeric string that overflows the underlying integer type (e.g. "2147483648"), which Enum.TryParse rejects rather than accepts.
Trigger
The resolved threshold string is parsed during Configure.
Enforcement
LogLevelExtensions.ToLogLevel calls Enum.TryParse(level.ToLogLevelString(), out LogLevel parsed) with no Enum.IsDefined check, and throws ArgumentException whenever TryParse itself returns false — which happens for a non-numeric, unrecognized string and for a numeric string that cannot fit in the underlying integer type.
Outcome
A misspelled, non-numeric, or integer-overflow threshold value aborts configuration, but a numeric string that is representable by the underlying integer type — defined or undefined for the enum — is accepted and silently resolves to an enum value, which ToLogEventLevel maps to Information whenever it does not match a named case.
SaifLogging:Loggers:{n}:Type values are matched against the LoggerType enum (ApiLog, ErrorLog, EventLog) case-sensitively; ErrorLog parses but is never applied to any standard sink.
Trigger
An appsettings.jsonSaifLogging:Loggers override entry is processed.
Enforcement
IConfigurationSectionExtensions.ToLoggerConfig calls Enum.TryParse without the ignoreCase overload.
Outcome
A lower-cased or differently-cased Type value that would otherwise match is rejected as invalid; an ErrorLog entry is parsed successfully but silently has no runtime effect.
SaifLogging:Loggers:{n}:Level has no default value; a missing value throws ArgumentException rather than defaulting to Info.
Trigger
An appsettings.jsonSaifLogging:Loggers override entry is processed during OP-04.
Enforcement
ToLoggerConfig calls level.ToLogLevel() unconditionally; ToLogLevel(null) throws ArgumentException. The Level ?? Info fallback elsewhere in SaifLogBase.InitializeLoggerInfoLookups is unreachable for override entries, and also for every standard config, since each sets Level explicitly.
Outcome
Every Loggers override entry must supply an explicit, valid Level; omitting it aborts Configure.
BR-LOG-05 Invalid Logger Type Throws Configuration Exception¶
Field
Value
Statement
An invalid or missing Type on a SaifLogging:Loggers override entry throws SaifLogConfigurationException, a library-specific exception type distinct from the generic ArgumentException used for level-parsing failures.
Trigger
An override entry's Type fails to parse as a LoggerType.
Enforcement
IConfigurationSectionExtensions.ToLoggerConfig throws SaifLogConfigurationException on a failed Enum.TryParse.
Outcome
Callers can distinguish a malformed sink selector from a malformed level value by exception type.
BR-LOG-06 ILogger And Serilog Pipelines Run Sequentially¶
Field
Value
Statement
Once the logger reports itself configured (BR-LOG-08), every log entry's dispatch delegate first calls the standard ILogger<ISaifLoggable> pipeline and then, only if that call does not throw, calls the library's own Serilog pipeline — the two are invoked sequentially with no try/catch between them, not in parallel, so a throwing ILogger call prevents the Serilog call from running. Whether a given consumer actually observes an entry that is written still depends on that consumer's own provider/sink filtering (minimum-level and category filters on ILogger, Serilog sink restrictions). A call made before the logger is configured reaches neither pipeline.
Trigger
Any SaifLogBase log method overload is invoked.
Enforcement
SaifLogBase's log methods call both the injected ILogger<ISaifLoggable> (e.g. Logger.LogInformation(message, propertyValues)) and the static Serilog Log instance (e.g. Log.Information(message, propertyValues)) in that order, inside the same delegate passed to LogIfConfigured/LogIfConfiguredWithException, but only inside the IsConfigured branch of that helper, and with no surrounding try/catch.
Outcome
An entry is offered to the ILogger pipeline, and then to the Serilog pipeline only if the ILogger call completed normally, once IsConfigured is true; a consumer whose own ILogger minimum level or whose own Serilog sink restriction excludes the entry's level will not see it, and an exception from the ILogger call stops the Serilog call from running at all. "Written to both pipelines" is not a guarantee that every consumer sees every entry, nor that every call reaches either pipeline.
BR-LOG-07 Log Id Generation Precedes Dispatch After Exception Formatting¶
Field
Value
Statement
For a non-exception-bearing call, GenerateLogId runs unconditionally, producing a correlation log ID — a timestamp concatenated with a new GUID, with : characters stripped — before any ILogger/Serilog dispatch is attempted. For an exception-bearing call, LogIfConfiguredWithException first builds/serializes an ErrorLogItem and tags Activity.Current with exception details (BR-LOG-10) beforeLogIfConfigured/GenerateLogId ever runs; a failure in that pre-processing (for example, a formatter failure) prevents an ID from being generated at all. Once generated, the ID is always pushed onto the Serilog LogContext and tagged onto Activity.Current when a trace is active, but it is returned to the caller as the log method's string result only if the subsequent dispatch (the ILogger call and, when configured, the Serilog call) completes without throwing — a throwing ILogger call propagates the exception instead of returning the ID, even though the ID was already generated.
SaifLogBase.GenerateLogId builds $"{DateTime.Now:yyyyMMddHHmmssffffzzz}{Guid.NewGuid()}".Replace(":", "") and tags it onto Activity.Current via AddTag("log.id", id); LogIfConfigured pushes it onto LogContext via StringValues.LogIdProperty unconditionally and then either invokes the log delegate (when IsConfigured) or writes a Debug diagnostic (when not), returning the ID from the using block afterward — a step that is never reached if the invoked delegate throws. For exception-bearing calls, LogIfConfiguredWithException runs formatting/serialization and Activity tagging ahead of this entire sequence, so a throw there means GenerateLogId is never reached (OP-06).
Outcome
Log entries for the same logical operation can be correlated across sinks and with distributed traces via the generated ID, and the caller receives that ID as the return value of every Debug/Info/Warning/Error/Fatal overload whose pre-ID processing and subsequent dispatch both complete normally; a pre-ID failure means no ID is ever generated, and a dispatch failure (for example, a throwing ILogger call) propagates instead of returning an ID that was already generated.
BR-LOG-08 Log Entries Drop Until Logger Is Configured¶
Field
Value
Statement
No log entry reaches ILogger/Serilog until the logger reports itself configured; for a non-exception-bearing call, every invocation made while IsConfigured is false still generates and returns a correlation ID and emits a Debug-level diagnostic trace noting the drop, but performs none of the configured write actions. For an exception-bearing call, the same holds only once the pre-ID exception-formatting/serialization steps (BR-LOG-10) complete without throwing — those steps run unconditionally, ahead of the IsConfigured check, regardless of whether the logger is configured, and a failure there propagates instead of returning an ID. IsConfigured is set only by each concrete logger's own Configure/Configure() method (SaifLog, SaifWindowsServiceLog, ConsoleLogService), never by the shared SaifLogBase.ConfigureLoggers helper those methods call; a subsequent Configure call that fails partway through (for example, during sink construction) does not reset an already-trueIsConfigured flag back to false — the instance remains considered configured from its prior successful call.
Trigger
A log method is invoked on a logger instance whose Configure call has not yet completed successfully.
Enforcement
LogIfConfigured always generates the ID and pushes LogContext/Activity properties, then checks IsConfigured: when true it invokes the log delegate — which calls the Microsoft ILogger and then the static Serilog logger sequentially, with no try/catch — and when false it calls System.Diagnostics.Debug.Write("A log request was made, but the SaifLog has not yet been configured.") instead; it returns the ID after either branch, provided the invoked delegate does not throw. A throwing ILogger call in the true branch propagates out of LogIfConfigured instead of reaching the return statement, preventing both the Serilog call and the ID's return to the caller. For exception-bearing calls, LogIfConfiguredWithException runs exception formatting and JsonConvert.SerializeObject before ever calling LogIfConfigured, so those steps can throw before ID generation is reached, independent of IsConfigured. SaifLogBase.ConfigureLoggers contains no assignment to IsConfigured; each of SaifLog.Configure, SaifWindowsServiceLog.Configure, and ConsoleLogService.Configure sets IsConfigured = true as its own final statement, after calling ConfigureLoggers (or the console logger's inline equivalent).
Outcome
Misconfiguration (for example, an invalid threshold) silently drops the intended ILogger/Serilog writes, but is not entirely invisible: a .NET Debug listener attached to the process surfaces the diagnostic trace, and the caller still receives a correlation ID back from the call provided the dispatch itself does not throw; a logger that was successfully configured once and later fails to reconfigure keeps logging under its prior configuration state rather than being marked unconfigured.
BR-LOG-09 Windows Service Logger Never Sets Error Route¶
Field
Value
Statement
SaifWindowsServiceLog never reads tracingdefaults:logerrorroute and always posts error entries with a null error route, regardless of what the Environment API holds for that key.
BR-LOG-10 Exception Entries Formatted Or Defaulted To ErrorLogItem¶
Field
Value
Statement
When ExFormatService is non-null, every exception log entry is formatted into an ErrorLogItem via ExceptionFormatService.BuildErrorLogItem, populating application name, environment, machine name, a process-identity UserName ({Environment.UserName}@{Environment.UserDomainName}), error date, exception info (the caller's log message plus FormatStackTrace(ex)), exception source, and textual state info; PageName is left unassigned (null) by the base implementation. When ExFormatService is null, LogIfConfiguredWithException substitutes an empty new ErrorLogItem() instead of calling a formatter. This formatting (or substitution) and the subsequent JsonConvert.SerializeObject call happen unconditionally, before the IsConfigured guard is checked, for every host style.
Trigger
An exception-bearing log method overload is invoked (OP-06).
Enforcement
SaifLogBase.LogIfConfiguredWithException evaluates this.ExFormatService != null ? this.ExFormatService.GetErrorDetails(message, ex) : new ErrorLogItem(), then calls JsonConvert.SerializeObject unconditionally — before delegating to LogIfConfigured, which is the method that checks IsConfigured.
Outcome
Error records are consistently structured across all three host styles whenever a formatter is attached, but a logger instance with no ExFormatService (for example, one not yet fully constructed) produces an empty, all-default ErrorLogItem instead of a populated one, and the serialization/formatting cost is paid even for calls made before Configure completes.
BR-LOG-11 Only Web Logger Captures Unredacted Request Details¶
Field
Value
Statement
Only the web logger augments the error payload with WebRequestDetails (captured HTTP request state — all headers, all claims, request body, with no redaction), embedded as text in ErrorLogItem.StateInfo; the Windows Service and console loggers never capture this data.
Trigger
An exception-bearing log method overload is invoked on SaifLog specifically (OP-07).
Enforcement
ExceptionFormatServiceForWeb overrides GetErrorDetails/BuildStateInfo to construct and render a WebRequestDetails from _contextAccessor.HttpContext and prepend it into the textual StateInfo; the base ExceptionFormatService used by the other hosts does not.
Outcome
Error records from web hosts carry unredacted request context (headers, claims, body) that error records from other host styles never contain.
The console logger's threshold comes solely from SaifLogging:BaseLevel (default Info); it never reads the Environment API threshold or SaifLogging:Loggers overrides.
BR-LOG-13 Sink Routes Exceptions By Event Properties¶
Field
Value
Statement
LoggingApiSink.Emit routes to the error branch (SendException) only when the LogEvent carries both a non-null Exception and the LogExceptionDetails property; every other event goes through the normal branch (Send), regardless of whether the payload is conceptually an ErrorLogItem or a plain message.
Trigger
Emit(LogEvent logEvent) runs for every event dispatched to this sink (OP-08).
Enforcement
IsException(logEvent) returns logEvent.Exception != null && logEvent.Properties.ContainsKey(StringValues.LogExceptionProperty); Emit calls await SendException(logEvent) when true and await Send(logEvent) otherwise.
Outcome
Routing is a property/exception check on the LogEvent itself, not a switch on an incoming DTO type; SendException builds the error-route URI (suffixed ?errorId={LogId} when a log ID is present) and posts logEvent.LogExceptionDetail(), while Send posts a LogItem built from the rendered message template to the non-error route.
BR-LOG-14 Message Formatter Built Once At Construction¶
Field
Value
Statement
LoggingApiSink's constructor builds its MessageTemplateTextFormatter once from the configured logPattern (_messageFormatter = new MessageTemplateTextFormatter(logPattern, null)), before any event is dispatched to either branch. Only the normal (Send) branch later uses that formatter to render a log event through the configured output template at emit time; the error branch (SendException) posts the already-structured ErrorLogItem returned by LogEventExtensions.LogExceptionDetail and never calls the formatter, appending the errorId query parameter only when a log ID is present on the event.
Trigger
A log event is posted via LoggingApiSink.Emit; the formatter itself is built once, at sink construction, from whichever logPattern was passed to LoggingApiSink's constructor.
Enforcement
LoggingApiSink's constructor runs _messageFormatter = new MessageTemplateTextFormatter(logPattern, null) unconditionally; Send formats via _messageFormatter.Format(logEvent, writer) at emit time; SendException instead calls logEvent.LogExceptionDetail() and conditionally appends ?errorId={errorLogId} only when logEvent.LogId() is non-blank.
Outcome
An override without LogPattern passes a nulllogPattern into the sink's constructor at the moment it is built — before either branch ever runs — not merely into the normal branch's per-event rendering step; whether MessageTemplateTextFormatter's own constructor tolerates a null template argument is not confirmed in this library's source.** If that constructor call throws, the sink itself fails construction and neither Send nor SendException ever runs for that attempt. If construction instead succeeds, the error branch's payload shape never depends on the configured pattern, regardless of what the constructor received.
BR-LOG-15 Emit Is Async Void With No Exception Containment¶
Field
Value
Statement
LoggingApiSink.Emit is async void with no try/catch of its own; it never contains or suppresses a thrown exception, but it does inspect IHttpJsonClient.RequestResult after the awaited Send/SendException call completes and reports an unsuccessful HTTP result to Console/Debug only — this is a result-inspection path, not universal failure containment.
Trigger
IHttpJsonClient's POST call returns a non-success RequestResult during OP-08; a thrown exception from PostAsync itself is a separate, unhandled case.
Enforcement
Emit awaits Send/SendException and then runs HandleHttpPostError, which checks _httpJsonClient.RequestResult.IsSuccessful and, when false, writes a diagnostic to Console/Debug only; no try/catch surrounds any of this.
Outcome
An unsuccessful HTTP result is silent from the application's perspective and visible only in local Console/Debug output, but an exception thrown out of PostAsync (network failure, serialization error, etc.) is not caught anywhere in this sink. Because Emit is async void rather than async Task, that exception is not captured in a Task for later observation — it is instead dispatched directly to the SynchronizationContext captured when Emit starts, or the thread pool if none was captured, bypassing this library's own calling code entirely, and can surface as an unhandled exception that terminates the process rather than one the original caller can catch.
BR-LOG-16 UseSaifLog Delegates Entirely To UseSerilog¶
Field
Value
Statement
UseSaifLog(this IWebHostBuilder builder) is a single-statement delegation: its entire body is return builder.UseSerilog();. It adds no provider-clearing call, no duplicate-output elimination logic, and no ordering check of its own.
Trigger
The host calls UseSaifLog() during startup (OP-11).
Enforcement
WebHostBuilderExtensions.UseSaifLog is declared only for IWebHostBuilder and contains the one return statement; no ClearProviders() call exists anywhere in this library.
Outcome
Whether default logging providers are cleared, and whether output is duplicated across providers, is determined entirely by the pinned Serilog.AspNetCore package's own UseSerilog() implementation, not by local code.
BR-LOG-17 UseSaifLog Imposes No Call Order Requirement¶
Field
Value
Statement
UseSaifLog() imposes no call-order requirement relative to a logger's Configure method; the source neither reads nor writes Log.Logger itself, so any ordering sensitivity comes from Serilog.AspNetCore's own UseSerilog() behavior, not from code in this library.
Trigger
The host calls UseSaifLog().
Enforcement
WebHostBuilderExtensions.UseSaifLog contains no reference to Log.Logger or to any Configure method; its only action is the delegated UseSerilog() call.
Outcome
This library establishes no documented ordering constraint between UseSaifLog() and Configure; verify the pinned Serilog.AspNetCore version's behavior before assuming one.**
BR-LOG-18 Console File Sinks Default To Local Temp Path¶
Field
Value
Statement
Each of the five auxiliary console file sinks (file0–file4) defaults to C:/temp/logs/file{n}.txt when its corresponding IConfiguration key is unset, and is created on disk the first time either of two triggers occurs: an explicit call to the writer's own public LogWriter.Configure() method, or any access of its Logger property (which lazily calls Configure() on first read, whether that read comes from an explicit Write* call or any other direct access of ILogWriter.Logger) — not on first write specifically, and not only through the Logger getter.
Trigger
ConsoleLogService.RegisterLogFiles constructs each LogWriter during Configure (OP-03); the file itself is not created at that point — only the later Configure() call or Logger access creates it.
Enforcement
ConsoleLogService.RegisterLogFiles passes whatever IConfiguration value it reads (or an empty string, via its own GetArg helper's defString: "" default, when the key is unset) straight into the LogWriter constructor — it applies no Windows-style default path of its own. The Windows-style default path (C:/temp/logs/file{(int)LogType}.txt) is applied only inside LogWriter.Configure() itself, when FilePath is whitespace at the time that method runs; LogWriter.Logger's getter calls Configure() lazily the first time it is read, wherever that read comes from, but Configure() can also be called directly without ever going through the Logger getter.
Outcome
An unconfigured console host writing to a non-Windows filesystem may fail to write file logs, or may write them to an unexpected location; a file sink that is never explicitly Configure()-d and whose Logger property is never accessed — whether through Write* or directly — never creates a file.