Show health warnings and refresh automatically
This commit is contained in:
parent
210acb7bbc
commit
594b9aaced
5 changed files with 75 additions and 19 deletions
|
|
@ -58,9 +58,10 @@
|
||||||
|
|
||||||
## Этап 5 — мониторинг и терминал
|
## Этап 5 — мониторинг и терминал
|
||||||
|
|
||||||
- [ ] swap, inode, network и выбранные systemd units;
|
- [x] swap, inode, network и состояние SSH/WireGuard;
|
||||||
- [ ] предупреждения по диску, памяти и недоступности;
|
- [x] предупреждения по диску, памяти, inode и недоступности;
|
||||||
- [ ] автоматическое обновление и короткая локальная история;
|
- [x] автоматическое обновление каждые 30 секунд;
|
||||||
|
- [ ] короткая локальная история;
|
||||||
- [ ] графики и экспорт диагностики без секретов;
|
- [ ] графики и экспорт диагностики без секретов;
|
||||||
- [ ] отдельный прямой SSH-терминал;
|
- [ ] отдельный прямой SSH-терминал;
|
||||||
- [ ] отдельная terminal identity и подтверждение пользователя;
|
- [ ] отдельная terminal identity и подтверждение пользователя;
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,7 @@
|
||||||
<StackPanel Grid.Column="3" VerticalAlignment="Center">
|
<StackPanel Grid.Column="3" VerticalAlignment="Center">
|
||||||
<TextBlock Text="{Binding MemoryText}" />
|
<TextBlock Text="{Binding MemoryText}" />
|
||||||
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{Binding DiskText}" />
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{Binding DiskText}" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{Binding HealthText}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="4"
|
Grid.Column="4"
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,15 @@ public sealed partial class MainPage : Page
|
||||||
{
|
{
|
||||||
private readonly ServerStorage _storage = new();
|
private readonly ServerStorage _storage = new();
|
||||||
private readonly SshMonitorService _ssh = new();
|
private readonly SshMonitorService _ssh = new();
|
||||||
|
private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromSeconds(30) };
|
||||||
|
private readonly SemaphoreSlim _refreshLock = new(1, 1);
|
||||||
private bool _loaded;
|
private bool _loaded;
|
||||||
|
|
||||||
public MainPage()
|
public MainPage()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Loaded += MainPage_Loaded;
|
Loaded += MainPage_Loaded;
|
||||||
|
_refreshTimer.Tick += async (_, _) => await RefreshAllAsync();
|
||||||
MainNavigation.SelectedItem = MainNavigation.MenuItems[0];
|
MainNavigation.SelectedItem = MainNavigation.MenuItems[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,6 +35,7 @@ public sealed partial class MainPage : Page
|
||||||
}
|
}
|
||||||
|
|
||||||
_loaded = true;
|
_loaded = true;
|
||||||
|
_refreshTimer.Start();
|
||||||
foreach (var profile in await _storage.LoadAsync())
|
foreach (var profile in await _storage.LoadAsync())
|
||||||
{
|
{
|
||||||
Servers.Add(new ServerViewModel(profile));
|
Servers.Add(new ServerViewModel(profile));
|
||||||
|
|
@ -271,6 +275,12 @@ public sealed partial class MainPage : Page
|
||||||
=> await RefreshAllAsync();
|
=> await RefreshAllAsync();
|
||||||
|
|
||||||
private async Task RefreshAllAsync()
|
private async Task RefreshAllAsync()
|
||||||
|
{
|
||||||
|
if (!await _refreshLock.WaitAsync(0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
{
|
{
|
||||||
if (Servers.Count == 0)
|
if (Servers.Count == 0)
|
||||||
{
|
{
|
||||||
|
|
@ -280,20 +290,25 @@ public sealed partial class MainPage : Page
|
||||||
|
|
||||||
await Task.WhenAll(Servers.Select(RefreshServerAsync));
|
await Task.WhenAll(Servers.Select(RefreshServerAsync));
|
||||||
var online = Servers.Count(server => server.IsOnline);
|
var online = Servers.Count(server => server.IsOnline);
|
||||||
var warnings = Servers.Count - online;
|
var warnings = Servers.Count(server => !server.IsOnline || server.HasWarning);
|
||||||
AvailabilityValueText.Text = $"{online} / {Servers.Count}";
|
AvailabilityValueText.Text = $"{online} / {Servers.Count}";
|
||||||
AvailabilityDetailText.Text = warnings == 0 ? "Все серверы доступны" : $"Недоступно: {warnings}";
|
AvailabilityDetailText.Text = warnings == 0 ? "Все серверы доступны" : $"Недоступно: {warnings}";
|
||||||
AverageLoadValueText.Text = online == 0
|
AverageLoadValueText.Text = online == 0
|
||||||
? "—"
|
? "—"
|
||||||
: $"{Servers.Where(server => server.IsOnline).Average(server => server.CpuPercent):F0}%";
|
: $"{Servers.Where(server => server.IsOnline).Average(server => server.CpuPercent):F0}%";
|
||||||
WarningValueText.Text = warnings.ToString(CultureInfo.InvariantCulture);
|
WarningValueText.Text = warnings.ToString(CultureInfo.InvariantCulture);
|
||||||
WarningDetailText.Text = warnings == 0 ? "Нет предупреждений" : "Проверьте SSH и ключ";
|
WarningDetailText.Text = warnings == 0 ? "Нет предупреждений" : "Проверьте доступность и ресурсы";
|
||||||
HeaderStatusText.Text = $"SSH monitoring · {Servers.Count} сервер(а) · обновлено {DateTime.Now:HH:mm:ss}";
|
HeaderStatusText.Text = $"SSH monitoring · {Servers.Count} сервер(а) · обновлено {DateTime.Now:HH:mm:ss}";
|
||||||
if (Servers.Any(server => server.IsHub))
|
if (Servers.Any(server => server.IsHub))
|
||||||
{
|
{
|
||||||
await RefreshMeshAsync(showSuccess: false);
|
await RefreshMeshAsync(showSuccess: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_refreshLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task RefreshServerAsync(ServerViewModel server)
|
private async Task RefreshServerAsync(ServerViewModel server)
|
||||||
{
|
{
|
||||||
|
|
@ -306,6 +321,11 @@ public sealed partial class MainPage : Page
|
||||||
server.CpuText = $"{metrics.CpuPercent:F0}% load";
|
server.CpuText = $"{metrics.CpuPercent:F0}% load";
|
||||||
server.MemoryText = $"{FormatSize(metrics.MemoryUsedKb)} / {FormatSize(metrics.MemoryTotalKb)} RAM";
|
server.MemoryText = $"{FormatSize(metrics.MemoryUsedKb)} / {FormatSize(metrics.MemoryTotalKb)} RAM";
|
||||||
server.DiskText = $"{FormatSize(metrics.DiskUsedKb)} / {FormatSize(metrics.DiskTotalKb)} disk";
|
server.DiskText = $"{FormatSize(metrics.DiskUsedKb)} / {FormatSize(metrics.DiskTotalKb)} disk";
|
||||||
|
var memoryPercent = Percent(metrics.MemoryUsedKb, metrics.MemoryTotalKb);
|
||||||
|
var diskPercent = Percent(metrics.DiskUsedKb, metrics.DiskTotalKb);
|
||||||
|
var inodePercent = Percent(metrics.InodesUsed, metrics.InodesTotal);
|
||||||
|
server.HasWarning = memoryPercent >= 90 || diskPercent >= 90 || inodePercent >= 90 || metrics.SshState != "active";
|
||||||
|
server.HealthText = $"swap {FormatSize(metrics.SwapUsedKb)}/{FormatSize(metrics.SwapTotalKb)} · inode {inodePercent:F0}% · net ↓{FormatBytes(metrics.NetworkRxBytes)} ↑{FormatBytes(metrics.NetworkTxBytes)} · SSH {metrics.SshState}";
|
||||||
server.LatencyText = $"{metrics.Latency.TotalMilliseconds:F0} ms";
|
server.LatencyText = $"{metrics.Latency.TotalMilliseconds:F0} ms";
|
||||||
server.Status = $"Онлайн · uptime {FormatUptime(metrics.Uptime)}";
|
server.Status = $"Онлайн · uptime {FormatUptime(metrics.Uptime)}";
|
||||||
server.IsOnline = true;
|
server.IsOnline = true;
|
||||||
|
|
@ -319,6 +339,8 @@ public sealed partial class MainPage : Page
|
||||||
server.DiskText = "—";
|
server.DiskText = "—";
|
||||||
server.LatencyText = "offline";
|
server.LatencyText = "offline";
|
||||||
server.IsOnline = false;
|
server.IsOnline = false;
|
||||||
|
server.HasWarning = true;
|
||||||
|
server.HealthText = "Нет данных о службах и сети";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,6 +373,14 @@ public sealed partial class MainPage : Page
|
||||||
? $"{uptime.TotalDays:F0} дн."
|
? $"{uptime.TotalDays:F0} дн."
|
||||||
: $"{uptime.TotalHours:F0} ч.";
|
: $"{uptime.TotalHours:F0} ч.";
|
||||||
|
|
||||||
|
private static double Percent(long used, long total)
|
||||||
|
=> total <= 0 ? 0 : Math.Clamp(used * 100d / total, 0, 100);
|
||||||
|
|
||||||
|
private static string FormatBytes(long bytes)
|
||||||
|
=> bytes >= 1024L * 1024 * 1024
|
||||||
|
? $"{bytes / 1024d / 1024d / 1024d:F1} GB"
|
||||||
|
: $"{bytes / 1024d / 1024d:F1} MB";
|
||||||
|
|
||||||
private static string CompactError(Exception exception)
|
private static string CompactError(Exception exception)
|
||||||
{
|
{
|
||||||
var firstLine = exception.Message.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
|
var firstLine = exception.Message.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ public sealed class ServerViewModel : INotifyPropertyChanged
|
||||||
private string _memoryText = "—";
|
private string _memoryText = "—";
|
||||||
private string _diskText = "—";
|
private string _diskText = "—";
|
||||||
private string _latencyText = "—";
|
private string _latencyText = "—";
|
||||||
|
private string _healthText = "—";
|
||||||
private bool _isOnline;
|
private bool _isOnline;
|
||||||
|
private bool _hasWarning;
|
||||||
|
|
||||||
public ServerViewModel(ServerProfileData profile) => Profile = profile;
|
public ServerViewModel(ServerProfileData profile) => Profile = profile;
|
||||||
|
|
||||||
|
|
@ -33,7 +35,9 @@ public sealed class ServerViewModel : INotifyPropertyChanged
|
||||||
public string MemoryText { get => _memoryText; set => Set(ref _memoryText, value); }
|
public string MemoryText { get => _memoryText; set => Set(ref _memoryText, value); }
|
||||||
public string DiskText { get => _diskText; set => Set(ref _diskText, value); }
|
public string DiskText { get => _diskText; set => Set(ref _diskText, value); }
|
||||||
public string LatencyText { get => _latencyText; set => Set(ref _latencyText, value); }
|
public string LatencyText { get => _latencyText; set => Set(ref _latencyText, value); }
|
||||||
|
public string HealthText { get => _healthText; set => Set(ref _healthText, value); }
|
||||||
public bool IsOnline { get => _isOnline; set => Set(ref _isOnline, value); }
|
public bool IsOnline { get => _isOnline; set => Set(ref _isOnline, value); }
|
||||||
|
public bool HasWarning { get => _hasWarning; set => Set(ref _hasWarning, value); }
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,14 @@ public sealed record ServerMetrics(
|
||||||
long MemoryTotalKb,
|
long MemoryTotalKb,
|
||||||
long DiskUsedKb,
|
long DiskUsedKb,
|
||||||
long DiskTotalKb,
|
long DiskTotalKb,
|
||||||
|
long SwapUsedKb,
|
||||||
|
long SwapTotalKb,
|
||||||
|
long InodesUsed,
|
||||||
|
long InodesTotal,
|
||||||
|
long NetworkRxBytes,
|
||||||
|
long NetworkTxBytes,
|
||||||
|
string SshState,
|
||||||
|
string WireGuardState,
|
||||||
TimeSpan Uptime,
|
TimeSpan Uptime,
|
||||||
TimeSpan Latency);
|
TimeSpan Latency);
|
||||||
|
|
||||||
|
|
@ -61,6 +69,10 @@ public sealed partial class SshMonitorService
|
||||||
var memoryAvailable = ReadLong(values, "MEM_AVAILABLE_KB");
|
var memoryAvailable = ReadLong(values, "MEM_AVAILABLE_KB");
|
||||||
var diskTotal = ReadLong(values, "DISK_TOTAL_KB");
|
var diskTotal = ReadLong(values, "DISK_TOTAL_KB");
|
||||||
var diskAvailable = ReadLong(values, "DISK_AVAILABLE_KB");
|
var diskAvailable = ReadLong(values, "DISK_AVAILABLE_KB");
|
||||||
|
var swapTotal = ReadLong(values, "SWAP_TOTAL_KB");
|
||||||
|
var swapFree = ReadLong(values, "SWAP_FREE_KB");
|
||||||
|
var inodesTotal = ReadLong(values, "DISK_INODES_TOTAL");
|
||||||
|
var inodesFree = ReadLong(values, "DISK_INODES_FREE");
|
||||||
|
|
||||||
return new ServerMetrics(
|
return new ServerMetrics(
|
||||||
values.GetValueOrDefault("HOSTNAME", profile.Name),
|
values.GetValueOrDefault("HOSTNAME", profile.Name),
|
||||||
|
|
@ -69,6 +81,14 @@ public sealed partial class SshMonitorService
|
||||||
memoryTotal,
|
memoryTotal,
|
||||||
Math.Max(0, diskTotal - diskAvailable),
|
Math.Max(0, diskTotal - diskAvailable),
|
||||||
diskTotal,
|
diskTotal,
|
||||||
|
Math.Max(0, swapTotal - swapFree),
|
||||||
|
swapTotal,
|
||||||
|
Math.Max(0, inodesTotal - inodesFree),
|
||||||
|
inodesTotal,
|
||||||
|
ReadLong(values, "NETWORK_RX_BYTES"),
|
||||||
|
ReadLong(values, "NETWORK_TX_BYTES"),
|
||||||
|
values.GetValueOrDefault("SYSTEMD_SSH", "unknown"),
|
||||||
|
values.GetValueOrDefault("SYSTEMD_WIREGUARD", "unknown"),
|
||||||
TimeSpan.FromSeconds(ReadLong(values, "UPTIME_SECONDS")),
|
TimeSpan.FromSeconds(ReadLong(values, "UPTIME_SECONDS")),
|
||||||
stopwatch.Elapsed);
|
stopwatch.Elapsed);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue