Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/SerilogSinkConfigTests.cs
T
Joseph Doherty 2d2e9f35c5 feat(host): build the process logger through the shared AddZbSerilog bootstrap (#94)
Switch ScadaBridge from its own LoggerConfigurationFactory to AddZbSerilog
(ZB.MOM.WW.Telemetry(.Serilog) 0.1.0 -> 0.2.0) using the two 0.2.0 hooks:

- MinimumLevelGovernor: ScadaBridge:Logging:MinimumLevel, applied after
  ReadFrom.Configuration so it outranks Serilog:MinimumLevel (never null, so the
  floor never defers to the Serilog key; Override:* namespace entries still apply;
  the one-shot conflict / unparseable-value warnings are unchanged).
- ConfigureLogger: the standby active-node exclusion filter (register row 71),
  the configured node identity (the shared bootstrap stamps NodeHostname from
  the machine name and skips empty values; an overwriting enricher keeps the
  configured ScadaBridge:Node values verbatim), Enrich.FromLogContext and the
  ExecutionLogCaptureSink.

ScadaBridgeLogging runs AddZbSerilog on a small pre-host builder and Program
installs the result as the static Log.Logger; both web hosts keep UseSerilog()
over it. One logger and one file-sink handle per process, usable before the
host is built (startup banner, warnings, pre-host Fatal), by SerilogAkkaLogger,
and after the host is disposed (shutdown line, unexpected-termination Fatal) —
AddZbSerilog's own DI registration would leave Log.Logger alone and have the
host dispose the logger.

LoggerConfigurationFactory is deleted (nothing else used it). Tests: existing
logging/standby/sink tests now run through the new pipeline, plus
ScadaBridgeLoggingParityTests (level precedence, Override:* still applies,
one-shot warning, configured identity incl. NodeHostname != MachineName and empty
SiteId, exact property set of a plain line, trace_id/span_id). Mutation-checked:
governor deferring, filter removed, identity enricher removed, FromLogContext
removed, capture sink removed each fail tests.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
2026-11-12 14:43:19 -05:00

95 lines
3.9 KiB
C#

using System.Reflection;
using System.Text.Json;
using Microsoft.Extensions.Configuration;
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
/// <summary>
/// Regression test: the Host requires its Serilog sinks
/// (console and file at minimum) to be <em>configuration-driven</em>. The sinks
/// must be defined in a <c>Serilog</c> section in <c>appsettings.json</c> and
/// applied via <c>ReadFrom.Configuration</c> — they must not be hard-coded in
/// <c>Program.cs</c>, so an operator can change the file path, rolling interval or
/// output template without recompiling.
/// </summary>
public class SerilogSinkConfigTests
{
private static string FindHostProjectDirectory()
{
var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var dir = new DirectoryInfo(assemblyDir);
while (dir != null)
{
var hostPath = Path.Combine(dir.FullName, "src", "ZB.MOM.WW.ScadaBridge.Host");
if (Directory.Exists(hostPath))
return hostPath;
dir = dir.Parent;
}
throw new DirectoryNotFoundException("Could not locate src/ZB.MOM.WW.ScadaBridge.Host");
}
/// <summary>Verifies that the shipped appsettings contain a Serilog section with console and file sinks.</summary>
[Fact]
public void ShippedAppSettings_HasSerilogSection_WithConsoleAndFileSinks()
{
var path = Path.Combine(FindHostProjectDirectory(), "appsettings.json");
using var doc = JsonDocument.Parse(File.ReadAllText(path));
Assert.True(
doc.RootElement.TryGetProperty("Serilog", out var serilog),
"appsettings.json must contain a `Serilog` section so ReadFrom.Configuration " +
"drives the sinks (REQ-HOST-8 / Host-014).");
Assert.True(
serilog.TryGetProperty("WriteTo", out var writeTo),
"the `Serilog` section must contain a `WriteTo` array defining the sinks.");
var sinkNames = writeTo.EnumerateArray()
.Select(e => e.TryGetProperty("Name", out var n) ? n.GetString() : null)
.ToList();
Assert.Contains("Console", sinkNames);
Assert.Contains("File", sinkNames);
}
/// <summary>Verifies that the host logger (shared AddZbSerilog bootstrap) applies the configured file sink.</summary>
[Fact]
public void ScadaBridgeLogging_AppliesConfiguredFileSink()
{
// A `Serilog` section in configuration must actually reach the built logger
// via ReadFrom.Configuration — proving the sink set is configuration-driven.
var logDir = Path.Combine(Path.GetTempPath(), "scadabridge-host014-" + Guid.NewGuid().ToString("N"));
var logPath = Path.Combine(logDir, "test-.log");
try
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Serilog:Using:0"] = "Serilog.Sinks.File",
["Serilog:WriteTo:0:Name"] = "File",
["Serilog:WriteTo:0:Args:path"] = logPath,
["Serilog:WriteTo:0:Args:rollingInterval"] = "Day",
})
.Build();
var logger = ScadaBridgeLogging.CreateLogger(
configuration, "Central", "central", "node1", new StringWriter(), null);
logger.Information("host-014 configured-sink probe");
logger.Dispose();
Assert.True(Directory.Exists(logDir), "the configured file sink must have created its directory.");
var written = Directory.GetFiles(logDir, "test-*.log");
Assert.NotEmpty(written);
Assert.Contains(
"host-014 configured-sink probe",
File.ReadAllText(written[0]));
}
finally
{
if (Directory.Exists(logDir))
Directory.Delete(logDir, recursive: true);
}
}
}