Add rich HTTP health endpoints for cluster monitoring
Enhance /api/health with component-level health, ServiceLevel, and redundancy state for load balancer probes. Add /health HTML page for operators to monitor node health in clustered System Platform deployments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,139 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
sut.IsHealthy().ShouldBe(false);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHealthData_WhenConnected_ReturnsHealthyStatus()
|
||||
{
|
||||
var sut = CreateService();
|
||||
var data = sut.GetHealthData();
|
||||
|
||||
data.Status.ShouldBe("Healthy");
|
||||
data.Components.MxAccess.ShouldBe("Connected");
|
||||
data.Components.Database.ShouldBe("Connected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHealthData_WhenDisconnected_ReturnsUnhealthyStatus()
|
||||
{
|
||||
var mxClient = new FakeMxAccessClient { State = ConnectionState.Disconnected };
|
||||
var galaxyStats = new GalaxyRepositoryStats { DbConnected = false };
|
||||
var sut = new StatusReportService(new HealthCheckService(), 10);
|
||||
sut.SetComponents(mxClient, null, galaxyStats, null);
|
||||
|
||||
var data = sut.GetHealthData();
|
||||
|
||||
data.Status.ShouldBe("Unhealthy");
|
||||
data.ServiceLevel.ShouldBe((byte)0);
|
||||
data.Components.MxAccess.ShouldBe("Disconnected");
|
||||
data.Components.Database.ShouldBe("Disconnected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHealthData_NoRedundancy_ServiceLevel255WhenHealthy()
|
||||
{
|
||||
var sut = CreateService();
|
||||
var data = sut.GetHealthData();
|
||||
|
||||
data.RedundancyEnabled.ShouldBe(false);
|
||||
data.ServiceLevel.ShouldBe((byte)255);
|
||||
data.RedundancyRole.ShouldBeNull();
|
||||
data.RedundancyMode.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHealthData_WithRedundancy_IncludesRoleAndServiceLevel()
|
||||
{
|
||||
var sut = CreateServiceWithRedundancy("Primary");
|
||||
var data = sut.GetHealthData();
|
||||
|
||||
data.RedundancyEnabled.ShouldBe(true);
|
||||
data.RedundancyRole.ShouldBe("Primary");
|
||||
data.RedundancyMode.ShouldBe("Warm");
|
||||
data.ServiceLevel.ShouldBe((byte)200);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHealthData_SecondaryRole_LowerServiceLevel()
|
||||
{
|
||||
var sut = CreateServiceWithRedundancy("Secondary");
|
||||
var data = sut.GetHealthData();
|
||||
|
||||
data.ServiceLevel.ShouldBe((byte)150);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHealthData_ContainsUptime()
|
||||
{
|
||||
var sut = CreateService();
|
||||
var data = sut.GetHealthData();
|
||||
|
||||
data.Uptime.ShouldNotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHealthData_ContainsTimestamp()
|
||||
{
|
||||
var sut = CreateService();
|
||||
var data = sut.GetHealthData();
|
||||
|
||||
data.Timestamp.ShouldBeGreaterThan(DateTime.UtcNow.AddMinutes(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateHealthJson_ContainsExpectedFields()
|
||||
{
|
||||
var sut = CreateService();
|
||||
var json = sut.GenerateHealthJson();
|
||||
|
||||
json.ShouldContain("Status");
|
||||
json.ShouldContain("ServiceLevel");
|
||||
json.ShouldContain("Components");
|
||||
json.ShouldContain("MxAccess");
|
||||
json.ShouldContain("Database");
|
||||
json.ShouldContain("OpcUaServer");
|
||||
json.ShouldContain("Uptime");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateHealthHtml_ContainsStatusBadge()
|
||||
{
|
||||
var sut = CreateService();
|
||||
var html = sut.GenerateHealthHtml();
|
||||
|
||||
html.ShouldContain("HEALTHY");
|
||||
html.ShouldContain("SERVICE LEVEL");
|
||||
html.ShouldContain("255");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateHealthHtml_ContainsComponentCards()
|
||||
{
|
||||
var sut = CreateService();
|
||||
var html = sut.GenerateHealthHtml();
|
||||
|
||||
html.ShouldContain("MXAccess");
|
||||
html.ShouldContain("Galaxy Database");
|
||||
html.ShouldContain("OPC UA Server");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateHealthHtml_WithRedundancy_ShowsRoleAndMode()
|
||||
{
|
||||
var sut = CreateServiceWithRedundancy("Primary");
|
||||
var html = sut.GenerateHealthHtml();
|
||||
|
||||
html.ShouldContain("Primary");
|
||||
html.ShouldContain("Warm");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateHealthHtml_ContainsAutoRefresh()
|
||||
{
|
||||
var sut = CreateService();
|
||||
var html = sut.GenerateHealthHtml();
|
||||
html.ShouldContain("meta http-equiv='refresh' content='10'");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a status report service preloaded with representative runtime, Galaxy, and metrics data.
|
||||
/// </summary>
|
||||
@@ -157,5 +290,21 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
sut.SetComponents(mxClient, metrics, galaxyStats, null);
|
||||
return sut;
|
||||
}
|
||||
|
||||
private static StatusReportService CreateServiceWithRedundancy(string role)
|
||||
{
|
||||
var mxClient = new FakeMxAccessClient();
|
||||
var galaxyStats = new GalaxyRepositoryStats { GalaxyName = "TestGalaxy", DbConnected = true };
|
||||
var redundancyConfig = new Host.Configuration.RedundancyConfiguration
|
||||
{
|
||||
Enabled = true,
|
||||
Mode = "Warm",
|
||||
Role = role,
|
||||
ServiceLevelBase = 200
|
||||
};
|
||||
var sut = new StatusReportService(new HealthCheckService(), 10);
|
||||
sut.SetComponents(mxClient, null, galaxyStats, null, null, redundancyConfig, "urn:test:instance1");
|
||||
return sut;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +107,35 @@ namespace ZB.MOM.WW.LmxOpcUa.Tests.Status
|
||||
response.Headers.CacheControl?.NoStore.ShouldBe(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the /health route returns an HTML health page.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task HealthPage_ReturnsHtml200()
|
||||
{
|
||||
var response = await _client.GetAsync("/health");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
response.Content.Headers.ContentType?.MediaType.ShouldBe("text/html");
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
body.ShouldContain("SERVICE LEVEL");
|
||||
body.ShouldContain("MXAccess");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that /api/health returns rich JSON with component health details.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task ApiHealth_ReturnsRichJson()
|
||||
{
|
||||
var response = await _client.GetAsync("/api/health");
|
||||
response.StatusCode.ShouldBe(HttpStatusCode.OK);
|
||||
response.Content.Headers.ContentType?.MediaType.ShouldBe("application/json");
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
body.ShouldContain("ServiceLevel");
|
||||
body.ShouldContain("Components");
|
||||
body.ShouldContain("Uptime");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Confirms that the server can be started and stopped cleanly.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user