Files
scadalink-design/deprecated/lmxproxy/src-reference/ZB.MOM.WW.LmxProxy.Host/Services/StatusReportService.cs
Joseph Doherty 9dccf8e72f deprecate(lmxproxy): move all LmxProxy code, tests, and docs to deprecated/
LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL
adapter files, and related docs to deprecated/. Removed LmxProxy registration
from DataConnectionFactory, project reference from DCL, protocol option from
UI, and cleaned up all requirement docs.
2026-04-08 15:56:23 -04:00

434 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Serilog;
using ZB.MOM.WW.LmxProxy.Host.Domain;
namespace ZB.MOM.WW.LmxProxy.Host.Services
{
/// <summary>
/// Service for collecting and formatting status information from various LmxProxy components
/// </summary>
public class StatusReportService
{
private static readonly ILogger Logger = Log.ForContext<StatusReportService>();
private readonly DetailedHealthCheckService? _detailedHealthCheckService;
private readonly HealthCheckService _healthCheckService;
private readonly PerformanceMetrics _performanceMetrics;
private readonly IScadaClient _scadaClient;
private readonly SubscriptionManager _subscriptionManager;
/// <summary>
/// Initializes a new instance of the StatusReportService class
/// </summary>
public StatusReportService(
IScadaClient scadaClient,
SubscriptionManager subscriptionManager,
PerformanceMetrics performanceMetrics,
HealthCheckService healthCheckService,
DetailedHealthCheckService? detailedHealthCheckService = null)
{
_scadaClient = scadaClient ?? throw new ArgumentNullException(nameof(scadaClient));
_subscriptionManager = subscriptionManager ?? throw new ArgumentNullException(nameof(subscriptionManager));
_performanceMetrics = performanceMetrics ?? throw new ArgumentNullException(nameof(performanceMetrics));
_healthCheckService = healthCheckService ?? throw new ArgumentNullException(nameof(healthCheckService));
_detailedHealthCheckService = detailedHealthCheckService;
}
/// <summary>
/// Generates a comprehensive status report as HTML
/// </summary>
public async Task<string> GenerateHtmlReportAsync()
{
try
{
StatusData statusData = await CollectStatusDataAsync();
return GenerateHtmlFromStatusData(statusData);
}
catch (Exception ex)
{
Logger.Error(ex, "Error generating HTML status report");
return GenerateErrorHtml(ex);
}
}
/// <summary>
/// Generates a comprehensive status report as JSON
/// </summary>
public async Task<string> GenerateJsonReportAsync()
{
try
{
StatusData statusData = await CollectStatusDataAsync();
return JsonSerializer.Serialize(statusData, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
}
catch (Exception ex)
{
Logger.Error(ex, "Error generating JSON status report");
return JsonSerializer.Serialize(new { error = ex.Message }, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
}
}
/// <summary>
/// Checks if the service is healthy
/// </summary>
public async Task<bool> IsHealthyAsync()
{
try
{
HealthCheckResult healthResult = await _healthCheckService.CheckHealthAsync(new HealthCheckContext());
return healthResult.Status == HealthStatus.Healthy;
}
catch (Exception ex)
{
Logger.Error(ex, "Error checking health status");
return false;
}
}
/// <summary>
/// Collects status data from all components
/// </summary>
private async Task<StatusData> CollectStatusDataAsync()
{
var statusData = new StatusData
{
Timestamp = DateTime.UtcNow,
ServiceName = "ZB.MOM.WW.LmxProxy.Host",
Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "Unknown"
};
// Collect connection status
statusData.Connection = new ConnectionStatus
{
IsConnected = _scadaClient.IsConnected,
State = _scadaClient.ConnectionState.ToString(),
NodeName = "N/A", // Could be extracted from configuration if needed
GalaxyName = "N/A" // Could be extracted from configuration if needed
};
// Collect subscription statistics
SubscriptionStats subscriptionStats = _subscriptionManager.GetSubscriptionStats();
statusData.Subscriptions = new SubscriptionStatus
{
TotalClients = subscriptionStats.TotalClients,
TotalTags = subscriptionStats.TotalTags,
ActiveSubscriptions = subscriptionStats.TotalTags // Assuming same for simplicity
};
// Collect performance metrics
Dictionary<string, MetricsStatistics> perfMetrics = _performanceMetrics.GetStatistics();
statusData.Performance = new PerformanceStatus
{
TotalOperations = perfMetrics.Values.Sum(m => m.TotalCount),
AverageSuccessRate = perfMetrics.Count > 0 ? perfMetrics.Values.Average(m => m.SuccessRate) : 1.0,
Operations = perfMetrics.ToDictionary(
kvp => kvp.Key,
kvp => new OperationStatus
{
TotalCount = kvp.Value.TotalCount,
SuccessRate = kvp.Value.SuccessRate,
AverageMilliseconds = kvp.Value.AverageMilliseconds,
MinMilliseconds = kvp.Value.MinMilliseconds,
MaxMilliseconds = kvp.Value.MaxMilliseconds
})
};
// Collect health check results
try
{
HealthCheckResult healthResult = await _healthCheckService.CheckHealthAsync(new HealthCheckContext());
statusData.Health = new HealthInfo
{
Status = healthResult.Status.ToString(),
Description = healthResult.Description ?? "",
Data = healthResult.Data?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToString() ?? "") ??
new Dictionary<string, string>()
};
// Collect detailed health check if available
if (_detailedHealthCheckService != null)
{
HealthCheckResult detailedHealthResult =
await _detailedHealthCheckService.CheckHealthAsync(new HealthCheckContext());
statusData.DetailedHealth = new HealthInfo
{
Status = detailedHealthResult.Status.ToString(),
Description = detailedHealthResult.Description ?? "",
Data = detailedHealthResult.Data?.ToDictionary(kvp => kvp.Key,
kvp => kvp.Value?.ToString() ?? "") ?? new Dictionary<string, string>()
};
}
}
catch (Exception ex)
{
Logger.Error(ex, "Error collecting health check data");
statusData.Health = new HealthInfo
{
Status = "Error",
Description = $"Health check failed: {ex.Message}",
Data = new Dictionary<string, string>()
};
}
return statusData;
}
/// <summary>
/// Generates HTML from status data
/// </summary>
private static string GenerateHtmlFromStatusData(StatusData statusData)
{
var html = new StringBuilder();
html.AppendLine("<!DOCTYPE html>");
html.AppendLine("<html>");
html.AppendLine("<head>");
html.AppendLine(" <title>LmxProxy Status</title>");
html.AppendLine(" <meta charset=\"utf-8\">");
html.AppendLine(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
html.AppendLine(" <meta http-equiv=\"refresh\" content=\"30\">");
html.AppendLine(" <style>");
html.AppendLine(
" body { font-family: Arial, sans-serif; margin: 40px; background-color: #f5f5f5; }");
html.AppendLine(
" .container { max-width: 1200px; margin: 0 auto; background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }");
html.AppendLine(" .header { text-align: center; margin-bottom: 30px; }");
html.AppendLine(
" .status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }");
html.AppendLine(
" .status-card { background: #f9f9f9; padding: 15px; border-radius: 6px; border-left: 4px solid #007acc; }");
html.AppendLine(" .status-card h3 { margin-top: 0; color: #333; }");
html.AppendLine(" .status-value { font-weight: bold; color: #007acc; }");
html.AppendLine(" .status-healthy { color: #28a745; }");
html.AppendLine(" .status-warning { color: #ffc107; }");
html.AppendLine(" .status-error { color: #dc3545; }");
html.AppendLine(" .status-connected { border-left-color: #28a745; }");
html.AppendLine(" .status-disconnected { border-left-color: #dc3545; }");
html.AppendLine(" table { width: 100%; border-collapse: collapse; margin-top: 10px; }");
html.AppendLine(" th, td { text-align: left; padding: 8px; border-bottom: 1px solid #ddd; }");
html.AppendLine(" th { background-color: #f2f2f2; }");
html.AppendLine(
" .timestamp { text-align: center; margin-top: 20px; color: #666; font-size: 0.9em; }");
html.AppendLine(" </style>");
html.AppendLine("</head>");
html.AppendLine("<body>");
html.AppendLine(" <div class=\"container\">");
// Header
html.AppendLine(" <div class=\"header\">");
html.AppendLine(" <h1>LmxProxy Status Dashboard</h1>");
html.AppendLine($" <p>Service: {statusData.ServiceName} | Version: {statusData.Version}</p>");
html.AppendLine(" </div>");
html.AppendLine(" <div class=\"status-grid\">");
// Connection Status Card
string connectionClass = statusData.Connection.IsConnected ? "status-connected" : "status-disconnected";
string connectionStatusText = statusData.Connection.IsConnected ? "Connected" : "Disconnected";
string connectionStatusClass = statusData.Connection.IsConnected ? "status-healthy" : "status-error";
html.AppendLine($" <div class=\"status-card {connectionClass}\">");
html.AppendLine(" <h3>MxAccess Connection</h3>");
html.AppendLine(
$" <p>Status: <span class=\"status-value {connectionStatusClass}\">{connectionStatusText}</span></p>");
html.AppendLine(
$" <p>State: <span class=\"status-value\">{statusData.Connection.State}</span></p>");
html.AppendLine(" </div>");
// Subscription Status Card
html.AppendLine(" <div class=\"status-card\">");
html.AppendLine(" <h3>Subscriptions</h3>");
html.AppendLine(
$" <p>Total Clients: <span class=\"status-value\">{statusData.Subscriptions.TotalClients}</span></p>");
html.AppendLine(
$" <p>Total Tags: <span class=\"status-value\">{statusData.Subscriptions.TotalTags}</span></p>");
html.AppendLine(
$" <p>Active Subscriptions: <span class=\"status-value\">{statusData.Subscriptions.ActiveSubscriptions}</span></p>");
html.AppendLine(" </div>");
// Performance Status Card
html.AppendLine(" <div class=\"status-card\">");
html.AppendLine(" <h3>Performance</h3>");
html.AppendLine(
$" <p>Total Operations: <span class=\"status-value\">{statusData.Performance.TotalOperations:N0}</span></p>");
html.AppendLine(
$" <p>Success Rate: <span class=\"status-value\">{statusData.Performance.AverageSuccessRate:P2}</span></p>");
html.AppendLine(" </div>");
// Health Status Card
string healthStatusClass = statusData.Health.Status.ToLowerInvariant() switch
{
"healthy" => "status-healthy",
"degraded" => "status-warning",
_ => "status-error"
};
html.AppendLine(" <div class=\"status-card\">");
html.AppendLine(" <h3>Health Status</h3>");
html.AppendLine(
$" <p>Status: <span class=\"status-value {healthStatusClass}\">{statusData.Health.Status}</span></p>");
html.AppendLine(
$" <p>Description: <span class=\"status-value\">{statusData.Health.Description}</span></p>");
html.AppendLine(" </div>");
html.AppendLine(" </div>");
// Performance Metrics Table
if (statusData.Performance.Operations.Any())
{
html.AppendLine(" <div class=\"status-card\" style=\"margin-top: 20px;\">");
html.AppendLine(" <h3>Operation Performance Metrics</h3>");
html.AppendLine(" <table>");
html.AppendLine(" <tr>");
html.AppendLine(" <th>Operation</th>");
html.AppendLine(" <th>Count</th>");
html.AppendLine(" <th>Success Rate</th>");
html.AppendLine(" <th>Avg (ms)</th>");
html.AppendLine(" <th>Min (ms)</th>");
html.AppendLine(" <th>Max (ms)</th>");
html.AppendLine(" </tr>");
foreach (KeyValuePair<string, OperationStatus> operation in statusData.Performance.Operations)
{
html.AppendLine(" <tr>");
html.AppendLine($" <td>{operation.Key}</td>");
html.AppendLine($" <td>{operation.Value.TotalCount:N0}</td>");
html.AppendLine($" <td>{operation.Value.SuccessRate:P2}</td>");
html.AppendLine($" <td>{operation.Value.AverageMilliseconds:F2}</td>");
html.AppendLine($" <td>{operation.Value.MinMilliseconds:F2}</td>");
html.AppendLine($" <td>{operation.Value.MaxMilliseconds:F2}</td>");
html.AppendLine(" </tr>");
}
html.AppendLine(" </table>");
html.AppendLine(" </div>");
}
// Timestamp
html.AppendLine(
$" <div class=\"timestamp\">Last updated: {statusData.Timestamp:yyyy-MM-dd HH:mm:ss} UTC</div>");
html.AppendLine(" </div>");
html.AppendLine("</body>");
html.AppendLine("</html>");
return html.ToString();
}
/// <summary>
/// Generates error HTML when status collection fails
/// </summary>
private static string GenerateErrorHtml(Exception ex)
{
return $@"<!DOCTYPE html>
<html>
<head>
<title>LmxProxy Status - Error</title>
<meta charset=""utf-8"">
<style>
body {{ font-family: Arial, sans-serif; margin: 40px; background-color: #f5f5f5; }}
.container {{ max-width: 800px; margin: 0 auto; background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
.error {{ color: #dc3545; background-color: #f8d7da; padding: 15px; border-radius: 6px; border: 1px solid #f5c6cb; }}
</style>
</head>
<body>
<div class=""container"">
<h1>LmxProxy Status Dashboard</h1>
<div class=""error"">
<h3>Error Loading Status</h3>
<p>An error occurred while collecting status information:</p>
<p><strong>{ex.Message}</strong></p>
</div>
<div style=""text-align: center; margin-top: 20px; color: #666; font-size: 0.9em;"">
Last updated: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC
</div>
</div>
</body>
</html>";
}
}
/// <summary>
/// Data structure for holding complete status information
/// </summary>
public class StatusData
{
public DateTime Timestamp { get; set; }
public string ServiceName { get; set; } = "";
public string Version { get; set; } = "";
public ConnectionStatus Connection { get; set; } = new();
public SubscriptionStatus Subscriptions { get; set; } = new();
public PerformanceStatus Performance { get; set; } = new();
public HealthInfo Health { get; set; } = new();
public HealthInfo? DetailedHealth { get; set; }
}
/// <summary>
/// Connection status information
/// </summary>
public class ConnectionStatus
{
public bool IsConnected { get; set; }
public string State { get; set; } = "";
public string NodeName { get; set; } = "";
public string GalaxyName { get; set; } = "";
}
/// <summary>
/// Subscription status information
/// </summary>
public class SubscriptionStatus
{
public int TotalClients { get; set; }
public int TotalTags { get; set; }
public int ActiveSubscriptions { get; set; }
}
/// <summary>
/// Performance status information
/// </summary>
public class PerformanceStatus
{
public long TotalOperations { get; set; }
public double AverageSuccessRate { get; set; }
public Dictionary<string, OperationStatus> Operations { get; set; } = new();
}
/// <summary>
/// Individual operation status
/// </summary>
public class OperationStatus
{
public long TotalCount { get; set; }
public double SuccessRate { get; set; }
public double AverageMilliseconds { get; set; }
public double MinMilliseconds { get; set; }
public double MaxMilliseconds { get; set; }
}
/// <summary>
/// Health check status information
/// </summary>
public class HealthInfo
{
public string Status { get; set; } = "";
public string Description { get; set; } = "";
public Dictionary<string, string> Data { get; set; } = new();
}
}