434 lines
19 KiB
C#
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();
|
|
}
|
|
}
|