Issue #4: add structured logging and metrics foundation
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
using MxGateway.Server.Diagnostics;
|
||||
|
||||
namespace MxGateway.Tests.Diagnostics;
|
||||
|
||||
public sealed class GatewayLogRedactorTests
|
||||
{
|
||||
[Fact]
|
||||
public void RedactApiKey_PreservesKeyIdAndRemovesSecret()
|
||||
{
|
||||
string? redacted = GatewayLogRedactor.RedactApiKey("Bearer mxgw_operator01_super-secret");
|
||||
|
||||
Assert.Equal("Bearer mxgw_operator01_[redacted]", redacted);
|
||||
Assert.DoesNotContain("super-secret", redacted);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("AuthenticateUser")]
|
||||
[InlineData("WriteSecured")]
|
||||
[InlineData("WriteSecured2")]
|
||||
public void IsCredentialBearingCommand_IdentifiesSensitiveMxAccessCommands(string commandMethod)
|
||||
{
|
||||
Assert.True(GatewayLogRedactor.IsCredentialBearingCommand(commandMethod));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RedactCommandValue_DoesNotLogRawValuesByDefault()
|
||||
{
|
||||
object? redacted = GatewayLogRedactor.RedactCommandValue("Write", "plaintext-tag-value");
|
||||
|
||||
Assert.Equal("[redacted]", redacted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RedactCommandValue_RedactsSecuredWriteEvenWhenValueLoggingIsEnabled()
|
||||
{
|
||||
object? redacted = GatewayLogRedactor.RedactCommandValue(
|
||||
"WriteSecured",
|
||||
"credential-bearing-value",
|
||||
valueLoggingEnabled: true);
|
||||
|
||||
Assert.Equal("[redacted]", redacted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RedactCommandValue_AllowsNonSensitiveValueOnlyWhenValueLoggingIsEnabled()
|
||||
{
|
||||
object? redacted = GatewayLogRedactor.RedactCommandValue(
|
||||
"Write",
|
||||
"diagnostic-value",
|
||||
valueLoggingEnabled: true);
|
||||
|
||||
Assert.Equal("diagnostic-value", redacted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogScope_RedactsClientIdentityBeforeScopeStateIsCreated()
|
||||
{
|
||||
GatewayLogScope scope = new(
|
||||
SessionId: "session-1",
|
||||
WorkerProcessId: 1234,
|
||||
CorrelationId: 99,
|
||||
CommandMethod: "AuthenticateUser",
|
||||
ClientIdentity: "Bearer mxgw_admin_secret");
|
||||
|
||||
IReadOnlyDictionary<string, object?> values = scope.ToDictionary();
|
||||
|
||||
Assert.Equal("session-1", values["SessionId"]);
|
||||
Assert.Equal(1234, values["WorkerProcessId"]);
|
||||
Assert.Equal((ulong)99, values["CorrelationId"]);
|
||||
Assert.Equal("AuthenticateUser", values["CommandMethod"]);
|
||||
Assert.Equal("Bearer mxgw_admin_[redacted]", values["ClientIdentity"]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MxGateway.Server;
|
||||
using MxGateway.Server.Metrics;
|
||||
|
||||
namespace MxGateway.Tests.Gateway;
|
||||
|
||||
@@ -19,4 +21,14 @@ public sealed class GatewayApplicationTests
|
||||
|
||||
Assert.Equal("LiveHealth", endpoint.Metadata.GetMetadata<IEndpointNameMetadata>()?.EndpointName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_RegistersGatewayMetrics()
|
||||
{
|
||||
WebApplication app = GatewayApplication.Build([]);
|
||||
|
||||
GatewayMetrics metrics = app.Services.GetRequiredService<GatewayMetrics>();
|
||||
|
||||
Assert.NotNull(metrics);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using MxGateway.Server.Metrics;
|
||||
|
||||
namespace MxGateway.Tests.Metrics;
|
||||
|
||||
public sealed class GatewayMetricsTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetSnapshot_ReflectsSessionWorkerCommandEventAndFaultUpdates()
|
||||
{
|
||||
using GatewayMetrics metrics = new();
|
||||
|
||||
metrics.SessionOpened();
|
||||
metrics.WorkerStarted(TimeSpan.FromMilliseconds(250));
|
||||
metrics.CommandStarted("Register");
|
||||
metrics.CommandSucceeded("Register", TimeSpan.FromMilliseconds(10));
|
||||
metrics.CommandStarted("WriteSecured");
|
||||
metrics.CommandFailed("WriteSecured", "AuthorizationFailed", TimeSpan.FromMilliseconds(12));
|
||||
metrics.EventReceived("session-1", "OnDataChange");
|
||||
metrics.EventReceived("session-1", "OnDataChange");
|
||||
metrics.SetEventQueueDepth(7);
|
||||
metrics.QueueOverflow("session-events");
|
||||
metrics.Fault("CommandTimeout");
|
||||
metrics.WorkerKilled("CommandTimeout");
|
||||
metrics.WorkerStopped("Killed");
|
||||
metrics.HeartbeatFailed("session-1");
|
||||
metrics.StreamDisconnected("ClientCancelled");
|
||||
metrics.SessionClosed();
|
||||
|
||||
GatewayMetricsSnapshot snapshot = metrics.GetSnapshot();
|
||||
|
||||
Assert.Equal(0, snapshot.OpenSessions);
|
||||
Assert.Equal(0, snapshot.WorkersRunning);
|
||||
Assert.Equal(7, snapshot.EventQueueDepth);
|
||||
Assert.Equal(1, snapshot.SessionsOpened);
|
||||
Assert.Equal(1, snapshot.SessionsClosed);
|
||||
Assert.Equal(2, snapshot.CommandsStarted);
|
||||
Assert.Equal(1, snapshot.CommandsSucceeded);
|
||||
Assert.Equal(1, snapshot.CommandsFailed);
|
||||
Assert.Equal(2, snapshot.EventsReceived);
|
||||
Assert.Equal(1, snapshot.QueueOverflows);
|
||||
Assert.Equal(1, snapshot.Faults);
|
||||
Assert.Equal(1, snapshot.WorkerKills);
|
||||
Assert.Equal(1, snapshot.WorkerExits);
|
||||
Assert.Equal(1, snapshot.HeartbeatFailures);
|
||||
Assert.Equal(1, snapshot.StreamDisconnects);
|
||||
Assert.Equal(1, snapshot.CommandFailuresByMethod["WriteSecured"]);
|
||||
Assert.Equal(2, snapshot.EventsByFamily["OnDataChange"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetEventQueueDepth_RejectsNegativeDepth()
|
||||
{
|
||||
using GatewayMetrics metrics = new();
|
||||
|
||||
ArgumentOutOfRangeException exception = Assert.Throws<ArgumentOutOfRangeException>(
|
||||
() => metrics.SetEventQueueDepth(-1));
|
||||
|
||||
Assert.Equal("depth", exception.ParamName);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user