513fa1aa16
Comment/XML-doc changes only: - add summaries to ~10k test methods, fixtures, fakes and helpers (+ <param> on theories, <returns> on async Task methods, <inheritdoc/> on interface implementations the checker can resolve) - strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N, #NN, Component-0NN) from comments, keeping the explanatory text; reword false positives in comments only - real docs on fakes implementing BCL/library interfaces (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime) - reword the last 5 src/ false positives (CWE-1236, failures-1, year-0001, M365) Only non-comment edits: four one-line nested classes split across lines so members can carry docs. Build 0 errors / 0 warnings.
161 lines
6.2 KiB
C#
161 lines
6.2 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Serilog;
|
|
using Serilog.Events;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
|
|
|
/// <summary>
|
|
/// Deferred-work register row 71 — a standby node logged the correct-by-design
|
|
/// <c>active-node</c> health-check verdict at ERROR on every probe.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <c>DefaultHealthCheckService</c> picks the log level from the verdict (Debug/Warning/Error
|
|
/// for Healthy/Degraded/Unhealthy). Both active-node checks answer Unhealthy on the standby
|
|
/// because 503 is the contract <c>/health/active</c> publishes, so a healthy standby logged
|
|
/// ERR every 10 s on a site node and every 5 s on a central node behind Traefik's probe.
|
|
/// </para>
|
|
/// <para>
|
|
/// The constraint on the fix is that a GENUINE failure must still log at Error, which is why
|
|
/// the filter is keyed on the check NAME rather than on the logger category.
|
|
/// </para>
|
|
/// </remarks>
|
|
public class StandbyHealthCheckLogNoiseTests
|
|
{
|
|
private const string HealthCheckCategory =
|
|
"Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService";
|
|
|
|
private const string Template =
|
|
"Health check {HealthCheckName} with status {HealthStatus} completed after "
|
|
+ "{ElapsedMilliseconds}ms with message '{HealthCheckDescription}'";
|
|
|
|
private static ILogger BuildLogger(InMemorySink sink)
|
|
{
|
|
var config = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["ScadaBridge:Logging:MinimumLevel"] = "Debug",
|
|
})
|
|
.Build();
|
|
|
|
return LoggerConfigurationFactory
|
|
.Build(config, "Site", "site-a", "site-a-b")
|
|
.WriteTo.Sink(sink)
|
|
.CreateLogger();
|
|
}
|
|
|
|
/// <summary>Emits one event shaped exactly like the framework's per-check line.</summary>
|
|
private static void EmitHealthCheckLine(
|
|
ILogger logger, string category, LogEventLevel level, string checkName, string status) =>
|
|
logger
|
|
.ForContext("SourceContext", category)
|
|
.Write(level, Template, checkName, status, 1.0, "some message");
|
|
|
|
/// <summary>Verifies that the standby active-node verdict is not logged.</summary>
|
|
[Fact]
|
|
public void StandbyActiveNodeVerdict_IsNotLogged()
|
|
{
|
|
// The regression: "Health check active-node with status Unhealthy … 'not the
|
|
// site pair primary (standby)'" at Error, once per probe, forever.
|
|
var sink = new InMemorySink();
|
|
var logger = BuildLogger(sink);
|
|
|
|
EmitHealthCheckLine(
|
|
logger, HealthCheckCategory, LogEventLevel.Error, "active-node", "Unhealthy");
|
|
|
|
Assert.Empty(sink.LogEvents);
|
|
}
|
|
|
|
/// <summary>Verifies that a genuine failure of a health check still logs at Error.</summary>
|
|
/// <param name="checkName">The name of the failing health check.</param>
|
|
[Theory]
|
|
[InlineData("localdb")]
|
|
[InlineData("akka-cluster")]
|
|
[InlineData("database")]
|
|
[InlineData("required-singletons")]
|
|
public void GenuineCheckFailure_StillLogsAtError(string checkName)
|
|
{
|
|
// The constraint. The filter is name-scoped, so every OTHER check on the same logger
|
|
// category keeps its Error line — a blanket category override would have silenced
|
|
// these too, which is exactly the trap this test exists to catch.
|
|
var sink = new InMemorySink();
|
|
var logger = BuildLogger(sink);
|
|
|
|
EmitHealthCheckLine(
|
|
logger, HealthCheckCategory, LogEventLevel.Error, checkName, "Unhealthy");
|
|
|
|
var evt = Assert.Single(sink.LogEvents);
|
|
Assert.Equal(LogEventLevel.Error, evt.Level);
|
|
}
|
|
|
|
/// <summary>Verifies that a healthy active-node verdict is unaffected.</summary>
|
|
[Fact]
|
|
public void ActiveNodeHealthyVerdict_IsUnaffected()
|
|
{
|
|
// The active node reports Healthy, which the framework logs at Debug. Nothing about
|
|
// the filter should touch it — it only ever matches Error.
|
|
var sink = new InMemorySink();
|
|
var logger = BuildLogger(sink);
|
|
|
|
EmitHealthCheckLine(
|
|
logger, HealthCheckCategory, LogEventLevel.Debug, "active-node", "Healthy");
|
|
|
|
Assert.Single(sink.LogEvents);
|
|
}
|
|
|
|
/// <summary>Verifies that a degraded active-node verdict is unaffected.</summary>
|
|
[Fact]
|
|
public void ActiveNodeDegradedVerdict_IsUnaffected()
|
|
{
|
|
// Degraded logs at Warning. If a future check shape ever reports Degraded for
|
|
// active-node, that should still be visible — the filter is Error-only.
|
|
var sink = new InMemorySink();
|
|
var logger = BuildLogger(sink);
|
|
|
|
EmitHealthCheckLine(
|
|
logger, HealthCheckCategory, LogEventLevel.Warning, "active-node", "Degraded");
|
|
|
|
Assert.Single(sink.LogEvents);
|
|
}
|
|
|
|
/// <summary>Verifies that an error mentioning the active node from another category is not suppressed.</summary>
|
|
[Fact]
|
|
public void AnErrorMentioningActiveNode_FromAnotherCategory_IsNotSuppressed()
|
|
{
|
|
// The predicate requires BOTH the health-check category and the check name, so
|
|
// application code logging about the active node keeps its Error.
|
|
var sink = new InMemorySink();
|
|
var logger = BuildLogger(sink);
|
|
|
|
EmitHealthCheckLine(
|
|
logger,
|
|
"ZB.MOM.WW.ScadaBridge.Host.Actors.AkkaHostedService",
|
|
LogEventLevel.Error,
|
|
"active-node",
|
|
"Unhealthy");
|
|
|
|
Assert.Single(sink.LogEvents);
|
|
}
|
|
|
|
/// <summary>Verifies that ordinary errors with no health-check properties are not suppressed.</summary>
|
|
[Fact]
|
|
public void OrdinaryErrorsWithNoHealthCheckProperties_AreNotSuppressed()
|
|
{
|
|
var sink = new InMemorySink();
|
|
var logger = BuildLogger(sink);
|
|
|
|
logger.Error("something genuinely broke");
|
|
|
|
Assert.Single(sink.LogEvents);
|
|
}
|
|
|
|
/// <summary>Verifies that the predicate rejects a null event.</summary>
|
|
[Fact]
|
|
public void Predicate_RejectsANullEvent()
|
|
{
|
|
// Defensive: Serilog never hands the filter a null, but the predicate is internal
|
|
// and reachable, and returning "suppress" for null would be the wrong default.
|
|
Assert.False(LoggerConfigurationFactory.IsExpectedStandbyActiveNodeProbe(null!));
|
|
}
|
|
}
|