using Serilog;
using Serilog.Events;
namespace ZB.MOM.WW.ScadaBridge.Host;
///
/// Builds the Serilog for the Host process.
///
/// REQ-HOST-8 / Host-011: the configured minimum level comes from
/// ScadaBridge:Logging:MinimumLevel (bound to ) so an
/// operator editing that key changes the effective log level.
///
/// REQ-HOST-8 / Host-014: the console and file sinks are read from the standard
/// Serilog configuration section via ReadFrom.Configuration — the sink
/// set, console output template, file path and rolling interval are all
/// configuration-driven (defined in appsettings.json), not hard-coded. The
/// explicit MinimumLevel.Is below pins the floor from .
///
/// Host-020: ScadaBridge:Logging:MinimumLevel is the single source of truth
/// for the floor — the explicit MinimumLevel.Is call deliberately runs
/// AFTER ReadFrom.Configuration so a Serilog:MinimumLevel entry in
/// configuration is overridden. To make that precedence visible (so an operator
/// who sets Serilog:MinimumLevel does not wonder why the change had no
/// effect), writes a one-shot warning to
/// when both keys are present. Pick one path —
/// editing Serilog:MinimumLevel alone has no effect.
///
public static class LoggerConfigurationFactory
{
/// Builds a enriched with node-identity properties and a configured minimum level.
/// Application configuration supplying the Serilog section and logging options.
/// Role label (e.g., central-a) added as a log enrichment property.
/// Site identifier added as a log enrichment property.
/// Hostname added as a log enrichment property.
/// The configured .
public static LoggerConfiguration Build(
IConfiguration configuration,
string nodeRole,
string siteId,
string nodeHostname)
=> Build(configuration, nodeRole, siteId, nodeHostname, Console.Error);
///
/// Test-visible overload of
/// that routes the Host-020 precedence warning through a caller-supplied
/// writer so unit tests can capture it. Production calls the four-arg
/// overload which uses .
///
/// Application configuration supplying the Serilog section and logging options.
/// Role label added as a log enrichment property.
/// Site identifier added as a log enrichment property.
/// Hostname added as a log enrichment property.
/// Writer that receives the one-shot Host-020 override-warning when both keys are present.
internal static LoggerConfiguration Build(
IConfiguration configuration,
string nodeRole,
string siteId,
string nodeHostname,
TextWriter warningWriter)
{
var loggingOptions = new LoggingOptions();
configuration.GetSection("ScadaBridge:Logging").Bind(loggingOptions);
var minimumLevel = ParseLevel(loggingOptions.MinimumLevel, warningWriter);
// Host-020: warn once if the operator also set a Serilog:MinimumLevel —
// they almost certainly expected it to take effect, but the explicit
// MinimumLevel.Is call below silently overrides it. The warning is
// emitted only when the conflicting key is actually present (a bare
// "Default" value is what ReadFrom.Configuration reads); a missing /
// empty Serilog:MinimumLevel section is silent.
var serilogMinimumLevel = configuration["Serilog:MinimumLevel"]
?? configuration["Serilog:MinimumLevel:Default"];
if (!string.IsNullOrWhiteSpace(serilogMinimumLevel))
{
warningWriter.WriteLine(
$"warning: Serilog:MinimumLevel ('{serilogMinimumLevel}') is being overridden by " +
$"ScadaBridge:Logging:MinimumLevel ('{loggingOptions.MinimumLevel ?? "Information (default)"}'). " +
"ScadaBridge:Logging:MinimumLevel is the documented source of truth for the floor (Host-011); " +
"remove the Serilog:MinimumLevel entry to silence this warning.");
}
return new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.MinimumLevel.Is(minimumLevel)
.Enrich.WithProperty("SiteId", siteId)
.Enrich.WithProperty("NodeHostname", nodeHostname)
.Enrich.WithProperty("NodeRole", nodeRole);
}
///
/// Parses a Serilog name, falling back to
/// for null/blank/unrecognised values.
///
/// Host-022: when an operator sets ScadaBridge:Logging:MinimumLevel to a
/// value that doesn't parse (e.g. the typo "Informaiton"), the helper must NOT
/// throw — startup has to succeed so the rest of the system can come up — but
/// it MUST make the silent fallback visible. The logger is not yet built at
/// this point, so the warning is written directly to
/// using ; non-null/non-blank values that fail
/// to parse are reported once, naming the offending value and the fallback.
/// Null/blank values are treated as "unset" and silently default — only
/// explicit-but-invalid values trigger the warning.
///
internal static LogEventLevel ParseLevel(string? level)
=> ParseLevel(level, Console.Error);
///
/// Test-visible overload of that routes the
/// one-shot warning through a caller-supplied writer (
/// in production) so unit tests can capture the warning output.
///
/// Configured level string, possibly null/blank/invalid.
/// Writer that receives a single warning line if the value is non-blank but unparseable.
internal static LogEventLevel ParseLevel(string? level, TextWriter warningWriter)
{
if (Enum.TryParse(level, ignoreCase: true, out var parsed))
return parsed;
if (!string.IsNullOrWhiteSpace(level))
warningWriter.WriteLine(
$"warning: ScadaBridge:Logging:MinimumLevel value '{level}' is not a recognised Serilog LogEventLevel; falling back to Information.");
return LogEventLevel.Information;
}
}