1ab5ca026b
Adds an additive AddZbSerilog(builder, configure, configureSerilog) overload taking ZbSerilogOptions: - MinimumLevelGovernor: Func<IConfiguration, LogEventLevel?> applied with MinimumLevel.Is AFTER ReadFrom.Configuration, so an app-owned key (e.g. ScadaBridge:Logging:MinimumLevel) outranks Serilog:MinimumLevel; null defers to config; Serilog:MinimumLevel:Override:* entries still apply. - ConfigureLogger: runs last, for app filters/enrichers/sinks (ScadaBridge's standby active-node exclusion). The two-argument overload is unchanged. 8 new tests (4 fail with the hook neutralized). Telemetry + Telemetry.Serilog 0.1.0 -> 0.2.0, not published. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
163 lines
5.7 KiB
C#
163 lines
5.7 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// scadaproj#12: the caller-supplied minimum-level governor on <c>AddZbSerilog</c>. The contract
|
|
/// being pinned is ScadaBridge's (REQ-HOST-8 / Host-011): an app-owned key is the floor and
|
|
/// OVERRIDES <c>Serilog:MinimumLevel</c>, while per-namespace overrides from config still apply.
|
|
/// </summary>
|
|
public sealed class MinimumLevelGovernanceTests
|
|
{
|
|
private const string AppLevelKey = "App:Logging:MinimumLevel";
|
|
|
|
private static IHost Build(
|
|
Dictionary<string, string?> config,
|
|
Action<ZbSerilogOptions>? 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<ILogger>();
|
|
|
|
private static LogEventLevel? FromAppKey(IConfiguration c) =>
|
|
Enum.TryParse<LogEventLevel>(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<ArgumentNullException>(() =>
|
|
builder.AddZbSerilog(o => o.ServiceName = "x", configureSerilog: null!));
|
|
}
|
|
|
|
private sealed class CapturingSink : ILogEventSink
|
|
{
|
|
public static readonly CapturingSink Instance = new();
|
|
public List<LogEvent> Events { get; } = [];
|
|
public void Emit(LogEvent logEvent) => Events.Add(logEvent);
|
|
}
|
|
}
|