using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Serilog; using Serilog.Core; using Serilog.Events; using ZB.MOM.WW.Telemetry.Serilog; namespace ZB.MOM.WW.Telemetry.Serilog.Tests; /// /// scadaproj#12: the caller-supplied minimum-level governor on AddZbSerilog. The contract /// being pinned is ScadaBridge's (REQ-HOST-8 / Host-011): an app-owned key is the floor and /// OVERRIDES Serilog:MinimumLevel, while per-namespace overrides from config still apply. /// public sealed class MinimumLevelGovernanceTests { private const string AppLevelKey = "App:Logging:MinimumLevel"; private static IHost Build( Dictionary config, Action? configureSerilog) { var builder = Host.CreateApplicationBuilder(new HostApplicationBuilderSettings { DisableDefaults = true, }); builder.Configuration.AddInMemoryCollection(config); if (configureSerilog is null) builder.AddZbSerilog(o => o.ServiceName = "governance-test"); else builder.AddZbSerilog(o => o.ServiceName = "governance-test", configureSerilog); return builder.Build(); } private static ILogger Logger(IHost host) => host.Services.GetRequiredService(); private static LogEventLevel? FromAppKey(IConfiguration c) => Enum.TryParse(c[AppLevelKey], ignoreCase: true, out var level) ? level : null; [Fact] public void Without_a_governor_Serilog_MinimumLevel_from_config_wins() { // Backward-compat pin: the two-argument overload keeps "config overrides the code floor". using var host = Build( new() { ["Serilog:MinimumLevel:Default"] = "Warning", [AppLevelKey] = "Debug" }, configureSerilog: null); var log = Logger(host); Assert.False(log.IsEnabled(LogEventLevel.Information)); Assert.True(log.IsEnabled(LogEventLevel.Warning)); } [Fact] public void Governor_overrides_Serilog_MinimumLevel_downward() { using var host = Build( new() { ["Serilog:MinimumLevel:Default"] = "Warning", [AppLevelKey] = "Debug" }, o => o.MinimumLevelGovernor = FromAppKey); var log = Logger(host); Assert.True(log.IsEnabled(LogEventLevel.Debug)); Assert.False(log.IsEnabled(LogEventLevel.Verbose)); } [Fact] public void Governor_overrides_Serilog_MinimumLevel_upward_including_the_scalar_form() { // "Serilog:MinimumLevel": "Verbose" (scalar, not :Default) is the other shape config takes. using var host = Build( new() { ["Serilog:MinimumLevel"] = "Verbose", [AppLevelKey] = "Error" }, o => o.MinimumLevelGovernor = FromAppKey); var log = Logger(host); Assert.False(log.IsEnabled(LogEventLevel.Warning)); Assert.True(log.IsEnabled(LogEventLevel.Error)); } [Fact] public void Governor_returning_null_defers_to_config() { using var host = Build( new() { ["Serilog:MinimumLevel:Default"] = "Warning" }, o => o.MinimumLevelGovernor = FromAppKey); var log = Logger(host); Assert.False(log.IsEnabled(LogEventLevel.Information)); Assert.True(log.IsEnabled(LogEventLevel.Warning)); } [Fact] public void Governor_receives_the_host_configuration() { IConfiguration? seen = null; using var host = Build( new() { [AppLevelKey] = "Fatal" }, o => o.MinimumLevelGovernor = c => { seen = c; return null; }); Assert.NotNull(seen); Assert.Equal("Fatal", seen![AppLevelKey]); } [Fact] public void Namespace_overrides_from_config_still_apply_under_a_governor() { using var host = Build( new() { [AppLevelKey] = "Debug", ["Serilog:MinimumLevel:Override:Noisy.Namespace"] = "Error", }, o => o.MinimumLevelGovernor = FromAppKey); var log = Logger(host); var noisy = log.ForContext(Constants.SourceContextPropertyName, "Noisy.Namespace.Thing"); Assert.True(log.IsEnabled(LogEventLevel.Debug)); Assert.False(noisy.IsEnabled(LogEventLevel.Warning)); Assert.True(noisy.IsEnabled(LogEventLevel.Error)); } [Fact] public void ConfigureLogger_runs_last_and_can_add_a_filter() { using var host = Build( new() { [AppLevelKey] = "Information" }, o => { o.MinimumLevelGovernor = FromAppKey; o.ConfigureLogger = (lc, sp) => { Assert.NotNull(sp); lc.Filter.ByExcluding(e => e.MessageTemplate.Text == "drop me") .WriteTo.Sink(CapturingSink.Instance); }; }); CapturingSink.Instance.Events.Clear(); var log = Logger(host); log.Information("drop me"); log.Information("keep me"); var texts = CapturingSink.Instance.Events.Select(e => e.MessageTemplate.Text).ToList(); Assert.Equal(["keep me"], texts); } [Fact] public void Null_hook_configurator_is_rejected() { var builder = Host.CreateApplicationBuilder(); Assert.Throws(() => builder.AddZbSerilog(o => o.ServiceName = "x", configureSerilog: null!)); } private sealed class CapturingSink : ILogEventSink { public static readonly CapturingSink Instance = new(); public List Events { get; } = []; public void Emit(LogEvent logEvent) => Events.Add(logEvent); } }