---
title: Business rules
summary: Catalog of business rules enforced by this library.
status: draft
owner: SAIF Platform Team
last_reviewed: 2026-10-02
library: SAIF.NetCore.Logging
reference_type: package
---

# 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](operations/op-04-apply-logger-overrides.md). |
| 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](#br-log-08-log-entries-drop-until-logger-is-configured)), 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](#br-log-10-exception-entries-formatted-or-defaulted-to-errorlogitem)) *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](operations/op-10-correlate-log-entry-with-activity.md)). |
| 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](operations/op-06-emit-exception-log-entry.md)). |
| 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](#br-log-10-exception-entries-formatted-or-defaulted-to-errorlogitem)) 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](operations/op-02-configure-windows-service-logger.md)). |
| 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](operations/op-06-emit-exception-log-entry.md)). |
| 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](operations/op-07-format-web-context-exception.md)). |
| 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](operations/op-03-configure-console-logger.md)). |
| 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](operations/op-08-post-log-entry-to-logging-api.md)). |
| 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](operations/op-08-post-log-entry-to-logging-api.md); 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](operations/op-11-wire-aspnet-core-logging.md)). |
| 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](operations/op-03-configure-console-logger.md)); 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.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Service registration](service-registration.md)
- [Operations and processes](operations-processes.md)
- [API reference](api/index.md)

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