7b0b9c7365
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
127 lines
6.9 KiB
C#
127 lines
6.9 KiB
C#
using Serilog;
|
|
using Serilog.Events;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Host;
|
|
|
|
/// <summary>
|
|
/// Builds the Serilog <see cref="LoggerConfiguration"/> for the Host process.
|
|
///
|
|
/// REQ-HOST-8 / Host-011: the configured minimum level comes from
|
|
/// <c>ScadaBridge:Logging:MinimumLevel</c> (bound to <see cref="LoggingOptions"/>) 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
|
|
/// <c>Serilog</c> configuration section via <c>ReadFrom.Configuration</c> — the sink
|
|
/// set, console output template, file path and rolling interval are all
|
|
/// configuration-driven (defined in <c>appsettings.json</c>), not hard-coded. The
|
|
/// explicit <c>MinimumLevel.Is</c> below pins the floor from <see cref="LoggingOptions"/>.
|
|
///
|
|
/// Host-020: <c>ScadaBridge:Logging:MinimumLevel</c> is the single source of truth
|
|
/// for the floor — the explicit <c>MinimumLevel.Is</c> call deliberately runs
|
|
/// AFTER <c>ReadFrom.Configuration</c> so a <c>Serilog:MinimumLevel</c> entry in
|
|
/// configuration is overridden. To make that precedence visible (so an operator
|
|
/// who sets <c>Serilog:MinimumLevel</c> does not wonder why the change had no
|
|
/// effect), <see cref="Build"/> writes a one-shot warning to
|
|
/// <see cref="Console.Error"/> when both keys are present. Pick one path —
|
|
/// editing <c>Serilog:MinimumLevel</c> alone has no effect.
|
|
/// </summary>
|
|
public static class LoggerConfigurationFactory
|
|
{
|
|
/// <summary>Builds a <see cref="LoggerConfiguration"/> enriched with node-identity properties and a configured minimum level.</summary>
|
|
/// <param name="configuration">Application configuration supplying the Serilog section and logging options.</param>
|
|
/// <param name="nodeRole">Role label (e.g., <c>central-a</c>) added as a log enrichment property.</param>
|
|
/// <param name="siteId">Site identifier added as a log enrichment property.</param>
|
|
/// <param name="nodeHostname">Hostname added as a log enrichment property.</param>
|
|
/// <returns>The configured <see cref="LoggerConfiguration"/>.</returns>
|
|
public static LoggerConfiguration Build(
|
|
IConfiguration configuration,
|
|
string nodeRole,
|
|
string siteId,
|
|
string nodeHostname)
|
|
=> Build(configuration, nodeRole, siteId, nodeHostname, Console.Error);
|
|
|
|
/// <summary>
|
|
/// Test-visible overload of <see cref="Build(IConfiguration, string, string, string)"/>
|
|
/// 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 <see cref="Console.Error"/>.
|
|
/// </summary>
|
|
/// <param name="configuration">Application configuration supplying the Serilog section and logging options.</param>
|
|
/// <param name="nodeRole">Role label added as a log enrichment property.</param>
|
|
/// <param name="siteId">Site identifier added as a log enrichment property.</param>
|
|
/// <param name="nodeHostname">Hostname added as a log enrichment property.</param>
|
|
/// <param name="warningWriter">Writer that receives the one-shot Host-020 override-warning when both keys are present.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a Serilog <see cref="LogEventLevel"/> name, falling back to
|
|
/// <see cref="LogEventLevel.Information"/> for null/blank/unrecognised values.
|
|
///
|
|
/// Host-022: when an operator sets <c>ScadaBridge:Logging:MinimumLevel</c> 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 <see cref="Console.Error"/>
|
|
/// using <see cref="WriteParseWarning"/>; 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.
|
|
/// </summary>
|
|
internal static LogEventLevel ParseLevel(string? level)
|
|
=> ParseLevel(level, Console.Error);
|
|
|
|
/// <summary>
|
|
/// Test-visible overload of <see cref="ParseLevel(string?)"/> that routes the
|
|
/// one-shot warning through a caller-supplied writer (<see cref="Console.Error"/>
|
|
/// in production) so unit tests can capture the warning output.
|
|
/// </summary>
|
|
/// <param name="level">Configured level string, possibly null/blank/invalid.</param>
|
|
/// <param name="warningWriter">Writer that receives a single warning line if the value is non-blank but unparseable.</param>
|
|
internal static LogEventLevel ParseLevel(string? level, TextWriter warningWriter)
|
|
{
|
|
if (Enum.TryParse<LogEventLevel>(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;
|
|
}
|
|
}
|