eabf270d71
Resolve all 622 issues flagged by the enhanced CommentChecker: add missing <returns> tags (incl. the standard phrasing on non-generic Task methods), add missing <summary> tags, and replace misused/redundant <inheritdoc/> on members that override or implement nothing with real documentation. Documentation-only — no behavior change; solution builds clean.
133 lines
7.5 KiB
C#
133 lines
7.5 KiB
C#
using Serilog;
|
|
using Serilog.Events;
|
|
using ZB.MOM.WW.Telemetry.Serilog;
|
|
|
|
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>
|
|
/// <returns>The fully configured <see cref="LoggerConfiguration"/> ready to create the Serilog logger.</returns>
|
|
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)
|
|
.Enrich.With(new TraceContextEnricher());
|
|
}
|
|
|
|
/// <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>
|
|
/// <param name="level">Configured level string, possibly null/blank/invalid.</param>
|
|
/// <returns>The parsed <see cref="LogEventLevel"/>, or <see cref="LogEventLevel.Information"/> when the value is absent or unrecognised.</returns>
|
|
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>
|
|
/// <returns>The parsed <see cref="LogEventLevel"/>, or <see cref="LogEventLevel.Information"/> when the value is absent or unrecognised.</returns>
|
|
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;
|
|
}
|
|
}
|