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);
}
}