Implement LmxOpcUa server — all 6 phases complete
Full OPC UA server on .NET Framework 4.8 (x86) exposing AVEVA System Platform Galaxy tags via MXAccess. Mirrors Galaxy object hierarchy as OPC UA address space, translating contained-name browse paths to tag-name runtime references. Components implemented: - Configuration: AppConfiguration with 4 sections, validator - Domain: ConnectionState, Quality, Vtq, MxDataTypeMapper, error codes - MxAccess: StaComThread, MxAccessClient (partial classes), MxProxyAdapter using strongly-typed ArchestrA.MxAccess COM interop - Galaxy Repository: SQL queries (hierarchy, attributes, change detection), ChangeDetectionService with auto-rebuild on deploy - OPC UA Server: LmxNodeManager (CustomNodeManager2), LmxOpcUaServer, OpcUaServerHost with programmatic config, SecurityPolicy None - Status Dashboard: HTTP server with HTML/JSON/health endpoints - Integration: Full 14-step startup, graceful shutdown, component wiring 175 tests (174 unit + 1 integration), all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
146
src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs
Normal file
146
src/ZB.MOM.WW.LmxOpcUa.Host/Status/StatusReportService.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
|
||||
using ZB.MOM.WW.LmxOpcUa.Host.OpcUa;
|
||||
|
||||
namespace ZB.MOM.WW.LmxOpcUa.Host.Status
|
||||
{
|
||||
/// <summary>
|
||||
/// Aggregates status from all components and generates HTML/JSON reports. (DASH-001 through DASH-009)
|
||||
/// </summary>
|
||||
public class StatusReportService
|
||||
{
|
||||
private readonly HealthCheckService _healthCheck;
|
||||
private readonly int _refreshIntervalSeconds;
|
||||
|
||||
private IMxAccessClient? _mxAccessClient;
|
||||
private PerformanceMetrics? _metrics;
|
||||
private GalaxyRepositoryStats? _galaxyStats;
|
||||
private OpcUaServerHost? _serverHost;
|
||||
|
||||
public StatusReportService(HealthCheckService healthCheck, int refreshIntervalSeconds)
|
||||
{
|
||||
_healthCheck = healthCheck;
|
||||
_refreshIntervalSeconds = refreshIntervalSeconds;
|
||||
}
|
||||
|
||||
public void SetComponents(IMxAccessClient? mxAccessClient, PerformanceMetrics? metrics,
|
||||
GalaxyRepositoryStats? galaxyStats, OpcUaServerHost? serverHost)
|
||||
{
|
||||
_mxAccessClient = mxAccessClient;
|
||||
_metrics = metrics;
|
||||
_galaxyStats = galaxyStats;
|
||||
_serverHost = serverHost;
|
||||
}
|
||||
|
||||
public StatusData GetStatusData()
|
||||
{
|
||||
var connectionState = _mxAccessClient?.State ?? ConnectionState.Disconnected;
|
||||
|
||||
return new StatusData
|
||||
{
|
||||
Connection = new ConnectionInfo
|
||||
{
|
||||
State = connectionState.ToString(),
|
||||
ReconnectCount = _mxAccessClient?.ReconnectCount ?? 0,
|
||||
ActiveSessions = _serverHost?.ActiveSessionCount ?? 0
|
||||
},
|
||||
Health = _healthCheck.CheckHealth(connectionState, _metrics),
|
||||
Subscriptions = new SubscriptionInfo
|
||||
{
|
||||
ActiveCount = _mxAccessClient?.ActiveSubscriptionCount ?? 0
|
||||
},
|
||||
Galaxy = new GalaxyInfo
|
||||
{
|
||||
GalaxyName = _galaxyStats?.GalaxyName ?? "",
|
||||
DbConnected = _galaxyStats?.DbConnected ?? false,
|
||||
LastDeployTime = _galaxyStats?.LastDeployTime,
|
||||
ObjectCount = _galaxyStats?.ObjectCount ?? 0,
|
||||
AttributeCount = _galaxyStats?.AttributeCount ?? 0,
|
||||
LastRebuildTime = _galaxyStats?.LastRebuildTime
|
||||
},
|
||||
Operations = _metrics?.GetStatistics() ?? new(),
|
||||
Footer = new FooterInfo
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
Version = typeof(StatusReportService).Assembly.GetName().Version?.ToString() ?? "1.0.0"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public string GenerateHtml()
|
||||
{
|
||||
var data = GetStatusData();
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("<!DOCTYPE html><html><head>");
|
||||
sb.AppendLine($"<meta http-equiv='refresh' content='{_refreshIntervalSeconds}'>");
|
||||
sb.AppendLine("<title>LmxOpcUa Status</title>");
|
||||
sb.AppendLine("<style>");
|
||||
sb.AppendLine("body { font-family: monospace; background: #1a1a2e; color: #eee; padding: 20px; }");
|
||||
sb.AppendLine(".panel { border: 2px solid #444; border-radius: 8px; padding: 15px; margin: 10px 0; }");
|
||||
sb.AppendLine(".green { border-color: #00cc66; } .red { border-color: #cc3333; } .yellow { border-color: #cccc33; } .gray { border-color: #666; }");
|
||||
sb.AppendLine("table { width: 100%; border-collapse: collapse; } th, td { text-align: left; padding: 4px 8px; border-bottom: 1px solid #333; }");
|
||||
sb.AppendLine("h2 { margin: 0 0 10px 0; } h1 { color: #66ccff; }");
|
||||
sb.AppendLine("</style></head><body>");
|
||||
sb.AppendLine("<h1>LmxOpcUa Status Dashboard</h1>");
|
||||
|
||||
// Connection panel
|
||||
var connColor = data.Connection.State == "Connected" ? "green" : data.Connection.State == "Connecting" ? "yellow" : "red";
|
||||
sb.AppendLine($"<div class='panel {connColor}'><h2>Connection</h2>");
|
||||
sb.AppendLine($"<p>State: <b>{data.Connection.State}</b> | Reconnects: {data.Connection.ReconnectCount} | Sessions: {data.Connection.ActiveSessions}</p>");
|
||||
sb.AppendLine("</div>");
|
||||
|
||||
// Health panel
|
||||
sb.AppendLine($"<div class='panel {data.Health.Color}'><h2>Health</h2>");
|
||||
sb.AppendLine($"<p>Status: <b>{data.Health.Status}</b> — {data.Health.Message}</p>");
|
||||
sb.AppendLine("</div>");
|
||||
|
||||
// Subscriptions panel
|
||||
sb.AppendLine("<div class='panel gray'><h2>Subscriptions</h2>");
|
||||
sb.AppendLine($"<p>Active: {data.Subscriptions.ActiveCount}</p>");
|
||||
sb.AppendLine("</div>");
|
||||
|
||||
// Galaxy Info panel
|
||||
sb.AppendLine("<div class='panel gray'><h2>Galaxy Info</h2>");
|
||||
sb.AppendLine($"<p>Galaxy: <b>{data.Galaxy.GalaxyName}</b> | DB: {(data.Galaxy.DbConnected ? "Connected" : "Disconnected")}</p>");
|
||||
sb.AppendLine($"<p>Last Deploy: {data.Galaxy.LastDeployTime:O} | Objects: {data.Galaxy.ObjectCount} | Attributes: {data.Galaxy.AttributeCount}</p>");
|
||||
sb.AppendLine($"<p>Last Rebuild: {data.Galaxy.LastRebuildTime:O}</p>");
|
||||
sb.AppendLine("</div>");
|
||||
|
||||
// Operations table
|
||||
sb.AppendLine("<div class='panel gray'><h2>Operations</h2>");
|
||||
sb.AppendLine("<table><tr><th>Operation</th><th>Count</th><th>Success Rate</th><th>Avg (ms)</th><th>Min (ms)</th><th>Max (ms)</th><th>P95 (ms)</th></tr>");
|
||||
foreach (var kvp in data.Operations)
|
||||
{
|
||||
var s = kvp.Value;
|
||||
sb.AppendLine($"<tr><td>{kvp.Key}</td><td>{s.TotalCount}</td><td>{s.SuccessRate:P1}</td>" +
|
||||
$"<td>{s.AverageMilliseconds:F1}</td><td>{s.MinMilliseconds:F1}</td><td>{s.MaxMilliseconds:F1}</td><td>{s.Percentile95Milliseconds:F1}</td></tr>");
|
||||
}
|
||||
sb.AppendLine("</table></div>");
|
||||
|
||||
// Footer
|
||||
sb.AppendLine("<div class='panel gray'><h2>Footer</h2>");
|
||||
sb.AppendLine($"<p>Generated: {data.Footer.Timestamp:O} | Version: {data.Footer.Version}</p>");
|
||||
sb.AppendLine("</div>");
|
||||
|
||||
sb.AppendLine("</body></html>");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string GenerateJson()
|
||||
{
|
||||
var data = GetStatusData();
|
||||
return JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true });
|
||||
}
|
||||
|
||||
public bool IsHealthy()
|
||||
{
|
||||
var state = _mxAccessClient?.State ?? ConnectionState.Disconnected;
|
||||
return _healthCheck.IsHealthy(state, _metrics);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user