Skip to content

Business rules

BR-LOG-01 Application Threshold Overrides Organization Wide Default

Field Value
Statement 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.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SaifLogBase.cs

BR-LOG-02 Numeric Level Strings Bypass Enum Validation

Field Value
Statement 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.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Extensions/LogLevelExtensions.cs

BR-LOG-03 Logger Type Matching Is Case Sensitive

Field Value
Statement 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.json SaifLogging: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.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Extensions/IConfigurationSectionExtensions.cs

BR-LOG-04 Missing Override Level Throws Configuration Error

Field Value
Statement SaifLogging:Loggers:{n}:Level has no default value; a missing value throws ArgumentException rather than defaulting to Info.
Trigger An appsettings.json SaifLogging: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.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Extensions/IConfigurationSectionExtensions.cs; src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Extensions/LogLevelExtensions.cs

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.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Extensions/IConfigurationSectionExtensions.cs

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.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SaifLogBase.cs

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) before LogIfConfigured/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.
Trigger Any log method overload is invoked (OP-10).
Enforcement 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.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SaifLogBase.cs

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-true IsConfigured 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.
Source 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/SaifWindowsServiceLog.cs; src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ConsoleLogService.cs

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.
Trigger SaifWindowsServiceLog.Configure runs (OP-02).
Enforcement SaifWindowsServiceLog.Configure constructs its LoggingApiSink without reading logerrorroute, passing the unset route value through.
Outcome Windows Service error log entries are always posted with a null route component in the error POST.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SaifWindowsServiceLog.cs

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

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.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ExceptionFormatter/ExceptionFormatServiceForWeb.cs

BR-LOG-12 Console Logger Threshold Ignores Environment API

Field Value
Statement The console logger's threshold comes solely from SaifLogging:BaseLevel (default Info); it never reads the Environment API threshold or SaifLogging:Loggers overrides.
Trigger ConsoleLogService.Configure runs (OP-03).
Enforcement ConsoleLogService.Configure never calls IEnvironmentClient.GetSettingValue/HasSetting, and never calls SaifLogBase.ApplyLoggingSettingOverrides.
Outcome Operators who rely on Environment-API-driven threshold changes will see no effect on console hosts.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ConsoleLogService.cs

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.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Loggers/LoggingApiSink.cs

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 null logPattern 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.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Loggers/LoggingApiSink.cs

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.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Loggers/LoggingApiSink.cs

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.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Web/WebHostBuilderExtensions.cs

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.**
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Web/WebHostBuilderExtensions.cs

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.
Source src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ConsoleLogService.cs; src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Console/LogWriter.cs

Notes

  • All rules above are verified directly from source except where noted inline.

Note: Statements marked with ** could not be confirmed; treat as unknown until verified.

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/SaifWindowsServiceLog.cs
  • src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/ConsoleLogService.cs
  • src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Extensions/IConfigurationSectionExtensions.cs
  • src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Extensions/LogLevelExtensions.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/Loggers/LoggingApiSink.cs
  • src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Console/LogWriter.cs
  • src/SAIF.NetCore.Logging/SAIF.NetCore.Logging/SAIF.NetCore.Logging/Web/WebHostBuilderExtensions.cs