using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Diagnostics.HealthChecks; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Serilog; using ZB.MOM.WW.LmxProxy.Host.Domain; using HealthCheckService = ZB.MOM.WW.LmxProxy.Host.Health.HealthCheckService; using ZB.MOM.WW.LmxProxy.Host.Metrics; using ZB.MOM.WW.LmxProxy.Host.Subscriptions; namespace ZB.MOM.WW.LmxProxy.Host.Status { /// /// Aggregates health, metrics, and subscription data into status reports. /// public class StatusReportService { private static readonly ILogger Logger = Log.ForContext(); private readonly IScadaClient _scadaClient; private readonly SubscriptionManager _subscriptionManager; private readonly PerformanceMetrics _performanceMetrics; private readonly HealthCheckService _healthCheckService; public StatusReportService( IScadaClient scadaClient, SubscriptionManager subscriptionManager, PerformanceMetrics performanceMetrics, HealthCheckService healthCheckService) { _scadaClient = scadaClient; _subscriptionManager = subscriptionManager; _performanceMetrics = performanceMetrics; _healthCheckService = healthCheckService; } public async Task GenerateHtmlReportAsync() { try { var statusData = await CollectStatusDataAsync(); return GenerateHtmlFromStatusData(statusData); } catch (Exception ex) { Logger.Error(ex, "Failed to generate HTML report"); return GenerateErrorHtml(ex); } } public async Task GenerateJsonReportAsync() { var statusData = await CollectStatusDataAsync(); var settings = new JsonSerializerSettings { Formatting = Formatting.Indented, ContractResolver = new CamelCasePropertyNamesContractResolver() }; return JsonConvert.SerializeObject(statusData, settings); } public async Task IsHealthyAsync() { var result = await _healthCheckService.CheckHealthAsync(new HealthCheckContext()); return result.Status == HealthStatus.Healthy; } private async Task CollectStatusDataAsync() { var statusData = new StatusData { Timestamp = DateTime.UtcNow, ServiceName = "ZB.MOM.WW.LmxProxy.Host", Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0.0" }; // Connection info statusData.Connection = new ConnectionStatus { IsConnected = _scadaClient.IsConnected, State = _scadaClient.ConnectionState.ToString() }; // Subscription stats var subStats = _subscriptionManager.GetStats(); statusData.Subscriptions = new SubscriptionStatus { TotalClients = subStats.TotalClients, TotalTags = subStats.TotalTags, ActiveSubscriptions = subStats.ActiveSubscriptions, TotalDelivered = subStats.TotalDelivered, TotalDropped = subStats.TotalDropped }; // Performance stats var allStats = _performanceMetrics.GetStatistics(); long totalOps = 0; double totalSuccessRate = 0; int opCount = 0; foreach (var kvp in allStats) { totalOps += kvp.Value.TotalCount; totalSuccessRate += kvp.Value.SuccessRate; opCount++; statusData.Performance.Operations[kvp.Key] = new OperationStatus { TotalCount = kvp.Value.TotalCount, SuccessRate = kvp.Value.SuccessRate, AverageMilliseconds = kvp.Value.AverageMilliseconds, MinMilliseconds = kvp.Value.MinMilliseconds, MaxMilliseconds = kvp.Value.MaxMilliseconds, Percentile95Milliseconds = kvp.Value.Percentile95Milliseconds }; } statusData.Performance.TotalOperations = totalOps; statusData.Performance.AverageSuccessRate = opCount > 0 ? totalSuccessRate / opCount : 1.0; // Health check var healthResult = await _healthCheckService.CheckHealthAsync(new HealthCheckContext()); statusData.Health = new HealthInfo { Status = healthResult.Status.ToString(), Description = healthResult.Description ?? "" }; if (healthResult.Data != null) { foreach (var kvp in healthResult.Data) { statusData.Health.Data[kvp.Key] = kvp.Value?.ToString() ?? ""; } } return statusData; } private static string GenerateHtmlFromStatusData(StatusData statusData) { var sb = new StringBuilder(); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(" "); sb.AppendLine(" "); sb.AppendLine(" LmxProxy Status"); sb.AppendLine(" "); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine("

LmxProxy Status Dashboard

"); // Connection card var connClass = statusData.Connection.IsConnected ? "card-green" : "card-red"; sb.AppendLine($"
"); sb.AppendLine($"
"); sb.AppendLine("

Connection

"); sb.AppendLine($"

Connected: {statusData.Connection.IsConnected}

"); sb.AppendLine($"

State: {statusData.Connection.State}

"); if (!string.IsNullOrEmpty(statusData.Connection.NodeName)) sb.AppendLine($"

Node: {statusData.Connection.NodeName}

"); if (!string.IsNullOrEmpty(statusData.Connection.GalaxyName)) sb.AppendLine($"

Galaxy: {statusData.Connection.GalaxyName}

"); sb.AppendLine("
"); // Health card var healthClass = GetHealthCardClass(statusData.Health.Status); var healthCss = GetHealthStatusCss(statusData.Health.Status); sb.AppendLine($"
"); sb.AppendLine("

Health

"); sb.AppendLine($"

{statusData.Health.Status}

"); sb.AppendLine($"

{statusData.Health.Description}

"); sb.AppendLine("
"); // Subscriptions card var subCardCss = statusData.Subscriptions.TotalDropped > 0 ? "card-yellow" : "card-green"; sb.AppendLine($"
"); sb.AppendLine("

Subscriptions

"); sb.AppendLine($"

Clients: {statusData.Subscriptions.TotalClients}

"); sb.AppendLine($"

Tags: {statusData.Subscriptions.TotalTags}

"); sb.AppendLine($"

Active: {statusData.Subscriptions.ActiveSubscriptions}

"); sb.AppendLine($"

Delivered: {statusData.Subscriptions.TotalDelivered:N0}

"); if (statusData.Subscriptions.TotalDropped > 0) { sb.AppendLine($"

Dropped: {statusData.Subscriptions.TotalDropped:N0}

"); } sb.AppendLine("
"); sb.AppendLine("
"); // RPC Operations table (always shown) sb.AppendLine("
"); sb.AppendLine("

RPC Operations

"); sb.AppendLine(" "); sb.AppendLine(" "); // All known RPC operations — show each even if 0 calls var rpcNames = new[] { "Read", "ReadBatch", "Write", "WriteBatch", "Subscribe" }; foreach (var rpcName in rpcNames) { var key = rpcName.Substring(0, 1).ToLowerInvariant() + rpcName.Substring(1); if (statusData.Performance.Operations.TryGetValue(key, out var op)) { sb.AppendLine($" " + $"" + $"" + $"" + $"" + $"" + $"" + $"" + $""); } else { sb.AppendLine($" "); } } sb.AppendLine("
OperationCountSuccess RateAvg (ms)Min (ms)Max (ms)P95 (ms)
{rpcName}{op.TotalCount}{op.SuccessRate:P1}{op.AverageMilliseconds:F1}{op.MinMilliseconds:F1}{op.MaxMilliseconds:F1}{op.Percentile95Milliseconds:F1}
{rpcName}0
"); sb.AppendLine("
"); sb.AppendLine($"
Last updated: {statusData.Timestamp:yyyy-MM-dd HH:mm:ss} UTC | Service: {statusData.ServiceName} v{statusData.Version}
"); sb.AppendLine(""); sb.AppendLine(""); return sb.ToString(); } private static string GetHealthCardClass(string status) { switch (status) { case "Healthy": return "card-green"; case "Degraded": return "card-yellow"; default: return "card-red"; } } private static string GetHealthStatusCss(string status) { switch (status) { case "Healthy": return "status-healthy"; case "Degraded": return "status-degraded"; default: return "status-unhealthy"; } } private static string GenerateErrorHtml(Exception ex) { var sb = new StringBuilder(); sb.AppendLine(""); sb.AppendLine("LmxProxy Status - Error"); sb.AppendLine(""); sb.AppendLine("

Error generating status report

"); sb.AppendLine($"

{ex.Message}

"); sb.AppendLine(""); return sb.ToString(); } } }