Surface historian plugin and alarm-tracking health in the status dashboard so operators can detect misconfiguration and runtime degradation that previously showed as fully healthy

Wraps the 4 HistoryRead overrides and OnAlarmAcknowledge with PerformanceMetrics.BeginOperation, adds alarm counters to LmxNodeManager, publishes a structured HistorianPluginOutcome from HistorianPluginLoader, and extends HealthCheckService with plugin-load, history-read, and alarm-ack-failure degradation rules.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-12 15:52:03 -04:00
parent 9b42b61eb6
commit c5ed5312a9
10 changed files with 647 additions and 26 deletions

View File

@@ -1,10 +1,12 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Text.Json;
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository;
using ZB.MOM.WW.LmxOpcUa.Host.Historian;
using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
@@ -22,6 +24,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
private GalaxyRepositoryStats? _galaxyStats;
private PerformanceMetrics? _metrics;
private HistorianConfiguration? _historianConfig;
private IMxAccessClient? _mxAccessClient;
private LmxNodeManager? _nodeManager;
private RedundancyConfiguration? _redundancyConfig;
@@ -53,7 +56,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
public void SetComponents(IMxAccessClient? mxAccessClient, PerformanceMetrics? metrics,
GalaxyRepositoryStats? galaxyStats, OpcUaServerHost? serverHost,
LmxNodeManager? nodeManager = null,
RedundancyConfiguration? redundancyConfig = null, string? applicationUri = null)
RedundancyConfiguration? redundancyConfig = null, string? applicationUri = null,
HistorianConfiguration? historianConfig = null)
{
_mxAccessClient = mxAccessClient;
_metrics = metrics;
@@ -62,6 +66,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
_nodeManager = nodeManager;
_redundancyConfig = redundancyConfig;
_applicationUri = applicationUri;
_historianConfig = historianConfig;
}
/// <summary>
@@ -71,6 +76,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
public StatusData GetStatusData()
{
var connectionState = _mxAccessClient?.State ?? ConnectionState.Disconnected;
var historianInfo = BuildHistorianStatusInfo();
var alarmInfo = BuildAlarmStatusInfo();
return new StatusData
{
@@ -80,7 +87,7 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
ReconnectCount = _mxAccessClient?.ReconnectCount ?? 0,
ActiveSessions = _serverHost?.ActiveSessionCount ?? 0
},
Health = _healthCheck.CheckHealth(connectionState, _metrics),
Health = _healthCheck.CheckHealth(connectionState, _metrics, historianInfo, alarmInfo),
Subscriptions = new SubscriptionInfo
{
ActiveCount = _mxAccessClient?.ActiveSubscriptionCount ?? 0
@@ -102,6 +109,8 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
TotalEvents = _nodeManager?.TotalMxChangeEvents ?? 0
},
Operations = _metrics?.GetStatistics() ?? new Dictionary<string, MetricsStatistics>(),
Historian = historianInfo,
Alarms = alarmInfo,
Redundancy = BuildRedundancyInfo(),
Footer = new FooterInfo
{
@@ -111,6 +120,33 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
};
}
private HistorianStatusInfo BuildHistorianStatusInfo()
{
var outcome = HistorianPluginLoader.LastOutcome;
return new HistorianStatusInfo
{
Enabled = _historianConfig?.Enabled ?? false,
PluginStatus = outcome.Status.ToString(),
PluginError = outcome.Error,
PluginPath = outcome.PluginPath,
ServerName = _historianConfig?.ServerName ?? "",
Port = _historianConfig?.Port ?? 0
};
}
private AlarmStatusInfo BuildAlarmStatusInfo()
{
return new AlarmStatusInfo
{
TrackingEnabled = _nodeManager?.AlarmTrackingEnabled ?? false,
ConditionCount = _nodeManager?.AlarmConditionCount ?? 0,
ActiveAlarmCount = _nodeManager?.ActiveAlarmCount ?? 0,
TransitionCount = _nodeManager?.AlarmTransitionCount ?? 0,
AckEventCount = _nodeManager?.AlarmAckEventCount ?? 0,
AckWriteFailures = _nodeManager?.AlarmAckWriteFailures ?? 0
};
}
private RedundancyInfo? BuildRedundancyInfo()
{
if (_redundancyConfig == null || !_redundancyConfig.Enabled)
@@ -204,6 +240,26 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
sb.AppendLine($"<p>Last Rebuild: {data.Galaxy.LastRebuildTime:O}</p>");
sb.AppendLine("</div>");
// Historian panel
var histColor = data.Historian.PluginStatus == "Loaded" ? "green"
: !data.Historian.Enabled ? "gray" : "red";
sb.AppendLine($"<div class='panel {histColor}'><h2>Historian</h2>");
sb.AppendLine(
$"<p>Enabled: <b>{data.Historian.Enabled}</b> | Plugin: <b>{data.Historian.PluginStatus}</b> | Server: {WebUtility.HtmlEncode(data.Historian.ServerName)}:{data.Historian.Port}</p>");
if (!string.IsNullOrEmpty(data.Historian.PluginError))
sb.AppendLine($"<p>Error: {WebUtility.HtmlEncode(data.Historian.PluginError)}</p>");
sb.AppendLine("</div>");
// Alarms panel
var alarmPanelColor = !data.Alarms.TrackingEnabled ? "gray"
: data.Alarms.AckWriteFailures > 0 ? "yellow" : "green";
sb.AppendLine($"<div class='panel {alarmPanelColor}'><h2>Alarms</h2>");
sb.AppendLine(
$"<p>Tracking: <b>{data.Alarms.TrackingEnabled}</b> | Conditions: {data.Alarms.ConditionCount} | Active: <b>{data.Alarms.ActiveAlarmCount}</b></p>");
sb.AppendLine(
$"<p>Transitions: {data.Alarms.TransitionCount:N0} | Ack Events: {data.Alarms.AckEventCount:N0} | Ack Write Failures: {data.Alarms.AckWriteFailures}</p>");
sb.AppendLine("</div>");
// Operations table
sb.AppendLine("<div class='panel gray'><h2>Operations</h2>");
sb.AppendLine(
@@ -254,7 +310,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
var connectionState = _mxAccessClient?.State ?? ConnectionState.Disconnected;
var mxConnected = connectionState == ConnectionState.Connected;
var dbConnected = _galaxyStats?.DbConnected ?? false;
var health = _healthCheck.CheckHealth(connectionState, _metrics);
var historianInfo = BuildHistorianStatusInfo();
var alarmInfo = BuildAlarmStatusInfo();
var health = _healthCheck.CheckHealth(connectionState, _metrics, historianInfo, alarmInfo);
var uptime = DateTime.UtcNow - _startTime;
var data = new HealthEndpointData
@@ -265,7 +323,9 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
{
MxAccess = connectionState.ToString(),
Database = dbConnected ? "Connected" : "Disconnected",
OpcUaServer = _serverHost?.IsRunning ?? false ? "Running" : "Stopped"
OpcUaServer = _serverHost?.IsRunning ?? false ? "Running" : "Stopped",
Historian = historianInfo.PluginStatus,
Alarms = alarmInfo.TrackingEnabled ? "Enabled" : "Disabled"
},
Uptime = FormatUptime(uptime),
Timestamp = DateTime.UtcNow
@@ -354,6 +414,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
sb.AppendLine(
$"<div class='redundancy'>Role: <b>{data.RedundancyRole}</b> | Mode: <b>{data.RedundancyMode}</b></div>");
var historianColor = data.Components.Historian == "Loaded" ? "#00cc66"
: data.Components.Historian == "Disabled" ? "#666" : "#cc3333";
var alarmColor = data.Components.Alarms == "Enabled" ? "#00cc66" : "#666";
// Component health cards
sb.AppendLine("<div class='components'>");
sb.AppendLine(
@@ -362,6 +426,10 @@ namespace ZB.MOM.WW.LmxOpcUa.Host.Status
$"<div class='component' style='border-color: {dbColor};'><div class='name'>Galaxy Database</div><div class='value' style='color: {dbColor};'>{data.Components.Database}</div></div>");
sb.AppendLine(
$"<div class='component' style='border-color: {uaColor};'><div class='name'>OPC UA Server</div><div class='value' style='color: {uaColor};'>{data.Components.OpcUaServer}</div></div>");
sb.AppendLine(
$"<div class='component' style='border-color: {historianColor};'><div class='name'>Historian</div><div class='value' style='color: {historianColor};'>{data.Components.Historian}</div></div>");
sb.AppendLine(
$"<div class='component' style='border-color: {alarmColor};'><div class='name'>Alarm Tracking</div><div class='value' style='color: {alarmColor};'>{data.Components.Alarms}</div></div>");
sb.AppendLine("</div>");
// Footer