From f2771acb496f052292c47a396da2a0ddc972f0b7 Mon Sep 17 00:00:00 2001 From: ChiaraBm Date: Tue, 27 May 2025 00:17:42 +0200 Subject: [PATCH] Improved node statistics. Added overview for single nodes and replaced mockup values with api fetched values for nodes list --- .../Admin/Nodes/StatisticsController.cs | 90 +++++++ .../Services/NodeService.cs | 10 +- .../Helpers/HostSystemHelper.cs | 218 +++++++++++++++- .../Helpers/OwnProcessHelper.cs | 36 --- .../StatisticsApplicationController.cs | 32 --- .../Statistics/StatisticsController.cs | 54 ++++ .../Statistics/StatisticsHostController.cs | 30 --- .../Models/CpuUsageDetails.cs | 8 + .../Models/DiskUsageDetails.cs | 11 + .../Models/MemoryUsageDetails.cs | 11 + .../StatisticsApplicationResponse.cs | 8 - .../Statistics/StatisticsHostResponse.cs | 6 - .../Statistics/StatisticsResponse.cs | 36 +++ .../Services/NodeService.cs | 15 ++ .../OverviewNodeUpdate.razor | 218 ++++++++++++++++ .../UI/Views/Admin/Nodes/Index.razor | 232 ++++++++++-------- .../UI/Views/Admin/Nodes/Update.razor | 10 +- .../Statistics/DockerStatisticsResponse.cs | 15 ++ .../Nodes/Statistics/StatisticsResponse.cs | 36 +++ 19 files changed, 853 insertions(+), 223 deletions(-) create mode 100644 MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/StatisticsController.cs delete mode 100644 MoonlightServers.Daemon/Helpers/OwnProcessHelper.cs delete mode 100644 MoonlightServers.Daemon/Http/Controllers/Statistics/StatisticsApplicationController.cs create mode 100644 MoonlightServers.Daemon/Http/Controllers/Statistics/StatisticsController.cs delete mode 100644 MoonlightServers.Daemon/Http/Controllers/Statistics/StatisticsHostController.cs create mode 100644 MoonlightServers.Daemon/Models/CpuUsageDetails.cs create mode 100644 MoonlightServers.Daemon/Models/DiskUsageDetails.cs create mode 100644 MoonlightServers.Daemon/Models/MemoryUsageDetails.cs delete mode 100644 MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Statistics/StatisticsApplicationResponse.cs delete mode 100644 MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Statistics/StatisticsHostResponse.cs create mode 100644 MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Statistics/StatisticsResponse.cs create mode 100644 MoonlightServers.Frontend/UI/Components/Nodes/UpdateNodePartials/OverviewNodeUpdate.razor create mode 100644 MoonlightServers.Shared/Http/Responses/Admin/Nodes/Statistics/DockerStatisticsResponse.cs create mode 100644 MoonlightServers.Shared/Http/Responses/Admin/Nodes/Statistics/StatisticsResponse.cs diff --git a/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/StatisticsController.cs b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/StatisticsController.cs new file mode 100644 index 0000000..f439c8d --- /dev/null +++ b/MoonlightServers.ApiServer/Http/Controllers/Admin/Nodes/StatisticsController.cs @@ -0,0 +1,90 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using MoonCore.Exceptions; +using MoonCore.Extended.Abstractions; +using MoonlightServers.ApiServer.Database.Entities; +using MoonlightServers.ApiServer.Services; +using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Statistics; + +namespace MoonlightServers.ApiServer.Http.Controllers.Admin.Nodes; + +[ApiController] +[Route("api/admin/servers/nodes")] +[Authorize(Policy = "permissions:admin.servers.nodes.statistics")] +public class StatisticsController : Controller +{ + private readonly NodeService NodeService; + private readonly DatabaseRepository NodeRepository; + + public StatisticsController(NodeService nodeService, DatabaseRepository nodeRepository) + { + NodeService = nodeService; + NodeRepository = nodeRepository; + } + + [HttpGet("{nodeId:int}/statistics")] + public async Task Get([FromRoute] int nodeId) + { + var node = await GetNode(nodeId); + var statistics = await NodeService.GetStatistics(node); + + return new() + { + Cpu = new() + { + Model = statistics.Cpu.Model, + Usage = statistics.Cpu.Usage, + UsagePerCore = statistics.Cpu.UsagePerCore + }, + Memory = new() + { + Available = statistics.Memory.Available, + Total = statistics.Memory.Total, + Cached = statistics.Memory.Cached, + Free = statistics.Memory.Free, + SwapFree = statistics.Memory.SwapFree, + SwapTotal = statistics.Memory.SwapTotal + }, + Disks = statistics.Disks.Select(x => new StatisticsResponse.DiskData() + { + Device = x.Device, + DiskFree = x.DiskFree, + DiskTotal = x.DiskTotal, + InodesFree = x.InodesFree, + InodesTotal = x.InodesTotal, + MountPath = x.MountPath + }).ToArray() + }; + } + + [HttpGet("{nodeId:int}/statistics/docker")] + public async Task GetDocker([FromRoute] int nodeId) + { + var node = await GetNode(nodeId); + var statistics = await NodeService.GetDockerStatistics(node); + + return new() + { + BuildCacheReclaimable = statistics.BuildCacheReclaimable, + BuildCacheUsed = statistics.BuildCacheUsed, + ContainersReclaimable = statistics.ContainersReclaimable, + ContainersUsed = statistics.ContainersUsed, + ImagesReclaimable = statistics.ImagesReclaimable, + ImagesUsed = statistics.ImagesUsed, + Version = statistics.Version + }; + } + + private async Task GetNode(int nodeId) + { + var result = await NodeRepository + .Get() + .FirstOrDefaultAsync(x => x.Id == nodeId); + + if (result == null) + throw new HttpApiException("A node with this id could not be found", 404); + + return result; + } +} \ No newline at end of file diff --git a/MoonlightServers.ApiServer/Services/NodeService.cs b/MoonlightServers.ApiServer/Services/NodeService.cs index 176f938..6570bb4 100644 --- a/MoonlightServers.ApiServer/Services/NodeService.cs +++ b/MoonlightServers.ApiServer/Services/NodeService.cs @@ -49,16 +49,10 @@ public class NodeService #region Statistics - public async Task GetApplicationStatistics(Node node) + public async Task GetStatistics(Node node) { using var apiClient = CreateApiClient(node); - return await apiClient.GetJson("api/statistics/application"); - } - - public async Task GetHostStatistics(Node node) - { - using var apiClient = CreateApiClient(node); - return await apiClient.GetJson("api/statistics/host"); + return await apiClient.GetJson("api/statistics"); } public async Task GetDockerStatistics(Node node) diff --git a/MoonlightServers.Daemon/Helpers/HostSystemHelper.cs b/MoonlightServers.Daemon/Helpers/HostSystemHelper.cs index 9196731..985843b 100644 --- a/MoonlightServers.Daemon/Helpers/HostSystemHelper.cs +++ b/MoonlightServers.Daemon/Helpers/HostSystemHelper.cs @@ -1,11 +1,21 @@ using System.Runtime.InteropServices; +using Mono.Unix.Native; using MoonCore.Attributes; +using MoonCore.Helpers; +using MoonlightServers.Daemon.Models; namespace MoonlightServers.Daemon.Helpers; [Singleton] public class HostSystemHelper { + private readonly ILogger Logger; + + public HostSystemHelper(ILogger logger) + { + Logger = logger; + } + public string GetOsName() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -44,6 +54,210 @@ public class HostSystemHelper // Unknown platform return "Unknown"; } - - + + #region CPU Usage + + public async Task GetCpuUsage() + { + var result = new CpuUsageDetails(); + var perCoreUsages = new List(); + + // Initial read + var (cpuLastStats, cpuLastSums) = await ReadAllCpuStats(); + + await Task.Delay(1000); + + // Second read + var (cpuNowStats, cpuNowSums) = await ReadAllCpuStats(); + + for (var i = 0; i < cpuNowStats.Length; i++) + { + var cpuDelta = cpuNowSums[i] - cpuLastSums[i]; + var cpuIdle = cpuNowStats[i][3] - cpuLastStats[i][3]; + var cpuUsed = cpuDelta - cpuIdle; + + var usage = 100.0 * cpuUsed / cpuDelta; + + if (i == 0) + result.OverallUsage = usage; + else + perCoreUsages.Add(usage); + } + + result.PerCoreUsage = perCoreUsages.ToArray(); + + // Get model name + var cpuInfoLines = await File.ReadAllLinesAsync("/proc/cpuinfo"); + var modelLine = cpuInfoLines.FirstOrDefault(x => x.StartsWith("model name")); + result.Model = modelLine?.Split(":")[1].Trim() ?? "N/A"; + + return result; + } + + private async Task<(long[][] cpuStatsList, long[] cpuSums)> ReadAllCpuStats() + { + var lines = await File.ReadAllLinesAsync("/proc/stat"); + + lines = lines.Where(line => line.StartsWith("cpu")) + .TakeWhile(line => line.StartsWith("cpu")) // Ensures only CPU lines are read + .ToArray(); + + var statsList = new List(); + var sumList = new List(); + + foreach (var line in lines) + { + var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries) + .Skip(1) // Skip the "cpu" label + .ToArray(); + + var cpuTimes = parts + .Select(long.Parse) + .ToArray(); + + var sum = cpuTimes.Sum(); + + statsList.Add(cpuTimes); + sumList.Add(sum); + } + + return (statsList.ToArray(), sumList.ToArray()); + } + + #endregion + + #region Memory + + public async Task ClearCachedMemory() + { + await File.WriteAllTextAsync("/proc/sys/vm/drop_caches", "3"); + } + + public async Task GetMemoryUsage() + { + var details = new MemoryUsageDetails(); + + var lines = await File.ReadAllLinesAsync("/proc/meminfo"); + + foreach (var line in lines) + { + // We want to ignore all non kilobyte values + if (!line.Contains("kB")) + continue; + + // Split the line up so we can extract the id and the value + // to map it to the model field + var parts = line.Split(":"); + + var id = parts[0]; + var value = parts[1] + .Replace("kB", "") + .Trim(); + + if (!long.TryParse(value, out var longValue)) + continue; + + var bytes = ByteConverter.FromKiloBytes(longValue).Bytes; + + switch (id) + { + case "MemTotal": + details.Total = bytes; + break; + + case "MemFree": + details.Free = bytes; + break; + + case "MemAvailable": + details.Available = bytes; + break; + + case "Cached": + details.Cached = bytes; + break; + + case "SwapTotal": + details.SwapTotal = bytes; + break; + + case "SwapFree": + details.SwapFree = bytes; + break; + } + } + + return details; + } + + #endregion + + #region Disks + + public async Task GetDiskUsages() + { + var details = new List(); + + // First we need to check which mounts actually exist + var diskDevices = new Dictionary(); + string[] ignoredMounts = ["/boot/efi", "/boot"]; + + var mountLines = await File.ReadAllLinesAsync("/proc/mounts"); + + foreach (var mountLine in mountLines) + { + var parts = mountLine.Split(" "); + + var device = parts[0]; + var mountedAt = parts[1]; + + // We only want to handle mounted physical devices + if (!device.StartsWith("/dev/")) + continue; + + // Ignore certain mounts which we dont want to show + if (ignoredMounts.Contains(mountedAt)) + continue; + + diskDevices.Add(device, mountedAt); + } + + foreach (var diskMount in diskDevices) + { + var device = diskMount.Key; + var mount = diskMount.Value; + + var statusCode = Syscall.statvfs(mount, out var statvfs); + + if (statusCode != 0) + { + var error = Stdlib.GetLastError(); + + Logger.LogError( + "An error occured while checking disk stats for mount {mount}: {error}", + mount, + error + ); + + continue; + } + + // Source: https://man7.org/linux/man-pages/man3/statvfs.3.html + var detail = new DiskUsageDetails() + { + Device = device, + MountPath = mount, + DiskTotal = statvfs.f_blocks * statvfs.f_frsize, + DiskFree = statvfs.f_bfree * statvfs.f_frsize, + InodesTotal = statvfs.f_files, + InodesFree = statvfs.f_ffree + }; + + details.Add(detail); + } + + return details.ToArray(); + } + + #endregion } \ No newline at end of file diff --git a/MoonlightServers.Daemon/Helpers/OwnProcessHelper.cs b/MoonlightServers.Daemon/Helpers/OwnProcessHelper.cs deleted file mode 100644 index bed4ab8..0000000 --- a/MoonlightServers.Daemon/Helpers/OwnProcessHelper.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Diagnostics; -using MoonCore.Attributes; - -namespace MoonlightServers.Daemon.Helpers; - -[Singleton] -public class OwnProcessHelper -{ - public long GetMemoryUsage() - { - var process = Process.GetCurrentProcess(); - var bytes = process.PrivateMemorySize64; - - return bytes; - } - - public TimeSpan GetUptime() - { - var process = Process.GetCurrentProcess(); - var uptime = DateTime.Now - process.StartTime; - - return uptime; - } - - public int CpuUsage() - { - var process = Process.GetCurrentProcess(); - var cpuTime = process.TotalProcessorTime; - var wallClockTime = DateTime.UtcNow - process.StartTime.ToUniversalTime(); - - var cpuUsage = (int)(100.0 * cpuTime.TotalMilliseconds / wallClockTime.TotalMilliseconds / - Environment.ProcessorCount); - - return cpuUsage; - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Statistics/StatisticsApplicationController.cs b/MoonlightServers.Daemon/Http/Controllers/Statistics/StatisticsApplicationController.cs deleted file mode 100644 index 89c7991..0000000 --- a/MoonlightServers.Daemon/Http/Controllers/Statistics/StatisticsApplicationController.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using MoonlightServers.Daemon.Helpers; -using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics; - -namespace MoonlightServers.Daemon.Http.Controllers.Statistics; - -// This controller hosts endpoints for the statistics for the daemon application itself - -[Authorize] -[ApiController] -[Route("api/statistics/application")] -public class StatisticsApplicationController : Controller -{ - private readonly OwnProcessHelper ProcessHelper; - - public StatisticsApplicationController(OwnProcessHelper processHelper) - { - ProcessHelper = processHelper; - } - - [HttpGet] - public async Task Get() - { - return new StatisticsApplicationResponse() - { - Uptime = ProcessHelper.GetUptime(), - MemoryUsage = ProcessHelper.GetMemoryUsage(), - CpuUsage = ProcessHelper.CpuUsage() - }; - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Statistics/StatisticsController.cs b/MoonlightServers.Daemon/Http/Controllers/Statistics/StatisticsController.cs new file mode 100644 index 0000000..f84a20e --- /dev/null +++ b/MoonlightServers.Daemon/Http/Controllers/Statistics/StatisticsController.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MoonlightServers.Daemon.Helpers; +using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics; + +namespace MoonlightServers.Daemon.Http.Controllers.Statistics; + +[Authorize] +[ApiController] +[Route("api/statistics")] +public class StatisticsController : Controller +{ + private readonly HostSystemHelper HostSystemHelper; + + public StatisticsController(HostSystemHelper hostSystemHelper) + { + HostSystemHelper = hostSystemHelper; + } + + [HttpGet] + public async Task Get() + { + var response = new StatisticsResponse(); + + var cpuUsage = await HostSystemHelper.GetCpuUsage(); + + response.Cpu.Model = cpuUsage.Model; + response.Cpu.Usage = cpuUsage.OverallUsage; + response.Cpu.UsagePerCore = cpuUsage.PerCoreUsage; + + var memoryUsage = await HostSystemHelper.GetMemoryUsage(); + + response.Memory.Available = memoryUsage.Available; + response.Memory.Cached = memoryUsage.Cached; + response.Memory.Free = memoryUsage.Free; + response.Memory.Total = memoryUsage.Total; + response.Memory.SwapTotal = memoryUsage.SwapTotal; + response.Memory.SwapFree = memoryUsage.SwapFree; + + var diskDetails = await HostSystemHelper.GetDiskUsages(); + + response.Disks = diskDetails.Select(x => new StatisticsResponse.DiskData() + { + Device = x.Device, + MountPath = x.MountPath, + DiskFree = x.DiskFree, + DiskTotal = x.DiskTotal, + InodesFree = x.InodesFree, + InodesTotal = x.InodesTotal + }).ToArray(); + + return response; + } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Http/Controllers/Statistics/StatisticsHostController.cs b/MoonlightServers.Daemon/Http/Controllers/Statistics/StatisticsHostController.cs deleted file mode 100644 index 10337d2..0000000 --- a/MoonlightServers.Daemon/Http/Controllers/Statistics/StatisticsHostController.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using MoonlightServers.Daemon.Helpers; -using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics; - -namespace MoonlightServers.Daemon.Http.Controllers.Statistics; - -// This controller hosts endpoints for the statistics for host system the daemon runs on - -[Authorize] -[ApiController] -[Route("api/statistics/host")] -public class StatisticsHostController : Controller -{ - private readonly HostSystemHelper HostSystemHelper; - - public StatisticsHostController(HostSystemHelper hostSystemHelper) - { - HostSystemHelper = hostSystemHelper; - } - - [HttpGet] - public async Task Get() - { - return new() - { - OperatingSystem = HostSystemHelper.GetOsName() - }; - } -} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Models/CpuUsageDetails.cs b/MoonlightServers.Daemon/Models/CpuUsageDetails.cs new file mode 100644 index 0000000..3d8a5c6 --- /dev/null +++ b/MoonlightServers.Daemon/Models/CpuUsageDetails.cs @@ -0,0 +1,8 @@ +namespace MoonlightServers.Daemon.Models; + +public class CpuUsageDetails +{ + public string Model { get; set; } + public double OverallUsage { get; set; } + public double[] PerCoreUsage { get; set; } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Models/DiskUsageDetails.cs b/MoonlightServers.Daemon/Models/DiskUsageDetails.cs new file mode 100644 index 0000000..60ff9b8 --- /dev/null +++ b/MoonlightServers.Daemon/Models/DiskUsageDetails.cs @@ -0,0 +1,11 @@ +namespace MoonlightServers.Daemon.Models; + +public class DiskUsageDetails +{ + public string Device { get; set; } + public string MountPath { get; set; } + public ulong DiskTotal { get; set; } + public ulong DiskFree { get; set; } + public ulong InodesTotal { get; set; } + public ulong InodesFree { get; set; } +} \ No newline at end of file diff --git a/MoonlightServers.Daemon/Models/MemoryUsageDetails.cs b/MoonlightServers.Daemon/Models/MemoryUsageDetails.cs new file mode 100644 index 0000000..39a2d1d --- /dev/null +++ b/MoonlightServers.Daemon/Models/MemoryUsageDetails.cs @@ -0,0 +1,11 @@ +namespace MoonlightServers.Daemon.Models; + +public class MemoryUsageDetails +{ + public long Total { get; set; } + public long Available { get; set; } + public long Free { get; set; } + public long Cached { get; set; } + public long SwapTotal { get; set; } + public long SwapFree { get; set; } +} \ No newline at end of file diff --git a/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Statistics/StatisticsApplicationResponse.cs b/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Statistics/StatisticsApplicationResponse.cs deleted file mode 100644 index 74624dd..0000000 --- a/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Statistics/StatisticsApplicationResponse.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics; - -public class StatisticsApplicationResponse -{ - public int CpuUsage { get; set; } - public long MemoryUsage { get; set; } - public TimeSpan Uptime { get; set; } -} \ No newline at end of file diff --git a/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Statistics/StatisticsHostResponse.cs b/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Statistics/StatisticsHostResponse.cs deleted file mode 100644 index f97e6dd..0000000 --- a/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Statistics/StatisticsHostResponse.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics; - -public class StatisticsHostResponse -{ - public string OperatingSystem { get; set; } -} \ No newline at end of file diff --git a/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Statistics/StatisticsResponse.cs b/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Statistics/StatisticsResponse.cs new file mode 100644 index 0000000..f8b3dc9 --- /dev/null +++ b/MoonlightServers.DaemonShared/DaemonSide/Http/Responses/Statistics/StatisticsResponse.cs @@ -0,0 +1,36 @@ +namespace MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics; + +public class StatisticsResponse +{ + public CpuData Cpu { get; set; } = new(); + public MemoryData Memory { get; set; } = new(); + + public DiskData[] Disks { get; set; } = []; + + public record DiskData + { + public string Device { get; set; } + public string MountPath { get; set; } + public ulong DiskTotal { get; set; } + public ulong DiskFree { get; set; } + public ulong InodesTotal { get; set; } + public ulong InodesFree { get; set; } + } + + public record MemoryData + { + public long Total { get; set; } + public long Available { get; set; } + public long Free { get; set; } + public long Cached { get; set; } + public long SwapTotal { get; set; } + public long SwapFree { get; set; } + } + + public record CpuData + { + public string Model { get; set; } + public double Usage { get; set; } + public double[] UsagePerCore { get; set; } + } +} \ No newline at end of file diff --git a/MoonlightServers.Frontend/Services/NodeService.cs b/MoonlightServers.Frontend/Services/NodeService.cs index 619284c..3bdd364 100644 --- a/MoonlightServers.Frontend/Services/NodeService.cs +++ b/MoonlightServers.Frontend/Services/NodeService.cs @@ -1,5 +1,6 @@ using MoonCore.Attributes; using MoonCore.Helpers; +using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Statistics; using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys; namespace MoonlightServers.Frontend.Services; @@ -18,4 +19,18 @@ public class NodeService { return await HttpApiClient.GetJson($"api/admin/servers/nodes/{nodeId}/system/status"); } + + public async Task GetStatistics(int nodeId) + { + return await HttpApiClient.GetJson( + $"api/admin/servers/nodes/{nodeId}/statistics" + ); + } + + public async Task GetDockerStatistics(int nodeId) + { + return await HttpApiClient.GetJson( + $"api/admin/servers/nodes/{nodeId}/statistics/docker" + ); + } } \ No newline at end of file diff --git a/MoonlightServers.Frontend/UI/Components/Nodes/UpdateNodePartials/OverviewNodeUpdate.razor b/MoonlightServers.Frontend/UI/Components/Nodes/UpdateNodePartials/OverviewNodeUpdate.razor new file mode 100644 index 0000000..8cf9b3a --- /dev/null +++ b/MoonlightServers.Frontend/UI/Components/Nodes/UpdateNodePartials/OverviewNodeUpdate.razor @@ -0,0 +1,218 @@ +@using MoonCore.Blazor.Tailwind.Components +@using MoonCore.Blazor.Tailwind.Toasts +@using MoonCore.Helpers +@using MoonlightServers.Frontend.Services +@using MoonlightServers.Shared.Http.Responses.Admin.Nodes +@using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Statistics + +@inject NodeService NodeService +@inject ToastService ToastService +@inject ILogger Logger + +@implements IDisposable + + + +
+ Overview +
+ +
+
+
+

+ @Math.Round(Statistics.Cpu.Usage, 2)% +

+ +
+
+ + CPU: @Statistics.Cpu.Model + +
+
+ +
+
+

+ @Formatter.FormatSize(Statistics.Memory.Total - Statistics.Memory.Available) + / + @Formatter.FormatSize(Statistics.Memory.Total) +

+ +
+
+ Memory +
+
+ +
+
+
+ @Formatter.FormatSize(Statistics.Memory.SwapTotal - Statistics.Memory.SwapFree) + / + @Formatter.FormatSize(Statistics.Memory.SwapTotal) +
+ +
+
+ Swap +
+
+
+ +
+ CPU +
+ +
+ @{ + var i = 0; + } + + @foreach (var usage in Statistics.Cpu.UsagePerCore) + { + var percentRounded = Math.Round(usage, 2); + +
+
+ #@(i) +
+
+
+
+
+
+
+ + i++; + } +
+ +
+ Disks +
+ +
+ @foreach (var disk in Statistics.Disks) + { + var usedPercent = Math.Round((disk.DiskTotal - disk.DiskFree) / (double)disk.DiskTotal * 100, 2); + var iNodesPercent = Math.Round((disk.InodesTotal - disk.InodesFree) / (double)disk.InodesTotal * 100, 2); + +
+
+
+
+
+
+
+
+
+
+ Device: @disk.Device - Mounted at: @disk.MountPath +
+
+ Used: @Formatter.FormatSize(disk.DiskTotal - disk.DiskFree) + Total: @Formatter.FormatSize(disk.DiskTotal) +
+
+ INodes: @(iNodesPercent)% +
+
+
+ } +
+ +
+ Docker +
+ +
+
+
+

+ @Formatter.FormatSize(DockerStatistics.ImagesUsed) (@Formatter.FormatSize(DockerStatistics.ImagesReclaimable) unused) +

+ +
+
+ Images +
+
+ +
+
+

+ @Formatter.FormatSize(DockerStatistics.ContainersUsed) ( @Formatter.FormatSize(DockerStatistics.ContainersReclaimable) unused) +

+ +
+
+ Containers +
+
+ +
+
+

+ @Formatter.FormatSize(DockerStatistics.BuildCacheUsed) (@Formatter.FormatSize(DockerStatistics.BuildCacheReclaimable) unused) +

+ +
+
+ Build Cache +
+
+
+
+ +@code +{ + [Parameter] public NodeDetailResponse Node { get; set; } + + private StatisticsResponse Statistics; + private DockerStatisticsResponse DockerStatistics; + + private Timer? UpdateTimer; + + private async Task Load(LazyLoader _) + { + Statistics = await NodeService.GetStatistics(Node.Id); + DockerStatistics = await NodeService.GetDockerStatistics(Node.Id); + + UpdateTimer = new Timer(HandleUpdate, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3)); + } + + private async void HandleUpdate(object? _) + { + try + { + Statistics = await NodeService.GetStatistics(Node.Id); + DockerStatistics = await NodeService.GetDockerStatistics(Node.Id); + + await InvokeAsync(StateHasChanged); + } + catch (Exception e) + { + Logger.LogWarning("An error occured while fetching status update: {e}", e); + await ToastService.Danger("Unable to fetch status update", e.Message); + } + } + + private string GetBackgroundColorByPercent(double percent) + { + if (percent < 70) + return "bg-success"; + else if (percent < 80) + return "bg-warning"; + else + return "bg-danger"; + } + + public void Dispose() + { + UpdateTimer?.Dispose(); + } +} \ No newline at end of file diff --git a/MoonlightServers.Frontend/UI/Views/Admin/Nodes/Index.razor b/MoonlightServers.Frontend/UI/Views/Admin/Nodes/Index.razor index 0426abd..ebf111e 100644 --- a/MoonlightServers.Frontend/UI/Views/Admin/Nodes/Index.razor +++ b/MoonlightServers.Frontend/UI/Views/Admin/Nodes/Index.razor @@ -8,12 +8,14 @@ @using MoonCore.Blazor.Tailwind.Dt @using MoonCore.Blazor.Tailwind.Toasts @using MoonlightServers.Frontend.Services +@using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Statistics @using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys @inject HttpApiClient ApiClient @inject NodeService NodeService @inject AlertService AlertService @inject ToastService ToastService +@inject ILogger Logger @attribute [Authorize(Policy = "permissions:admin.servers.nodes.get")] @@ -31,8 +33,8 @@ - - + + @@ -44,86 +46,96 @@ - - @{ - bool isFetched; - NodeSystemStatusResponse? data; + @{ + var isFetched = StatusResponses.TryGetValue(context.Id, out var data); + } - lock (Responses) - isFetched = Responses.TryGetValue(context.Id, out data); - } - - @if (isFetched) + @if (isFetched) + { + if (data == null) { - if (data == null) - { - - - - API Error - +
+ + + API Error - } - else - { - if (data.RoundtripSuccess) - { - - - Online (@(data.Version)) - - } - else - { - - - - Error - Details - - - } - } +
} else { - - - Loading - + if (data.RoundtripSuccess) + { +
+ + Online (@(data.Version)) +
+ } + else + { +
+ + + Error + + Details +
+ } } -
+ } + else + { +
+ + Loading +
+ }
-
- - 33% of 6 Cores -
-
-
- - -
- - 1.56GB / 64GB -
-
-
- - -
- - 78.68GB / 1TB -
+ @{ + var isFetched = Statistics.TryGetValue(context.Id, out var data); + } + + @if (isFetched) + { + if (data == null) + { +
+ + + API Error + +
+ } + else + { +
+
+ + @(Math.Round(data.Cpu.Usage))% +
+ +
+ + + @(Math.Round((data.Memory.Total - data.Memory.Free - data.Memory.Cached) / (double)data.Memory.Total * 100))% + +
+
+ } + } + else + { +
+ + Loading +
+ }
@@ -147,10 +159,60 @@ { private DataTable Table; - private Dictionary Responses = new(); + private Dictionary StatusResponses = new(); + private Dictionary Statistics = new(); private async Task> LoadData(PaginationOptions options) - => await ApiClient.GetJson>($"api/admin/servers/nodes?page={options.Page}&pageSize={options.PerPage}"); + { + Statistics.Clear(); + StatusResponses.Clear(); + + var result = await ApiClient.GetJson>( + $"api/admin/servers/nodes?page={options.Page}&pageSize={options.PerPage}" + ); + + Task.Run(async () => + { + foreach (var item in result.Items) + { + try + { + Statistics[item.Id] = await NodeService.GetStatistics(item.Id); + } + catch (Exception e) + { + Logger.LogWarning( + "An error occured while fetching statistics for node {nodeId}: {e}", + item.Id, + e + ); + + Statistics[item.Id] = null; + } + + await InvokeAsync(StateHasChanged); + + try + { + StatusResponses[item.Id] = await NodeService.GetSystemStatus(item.Id); + } + catch (Exception e) + { + Logger.LogWarning( + "An error occured while fetching status for node {nodeId}: {e}", + item.Id, + e + ); + + StatusResponses[item.Id] = null; + } + + await InvokeAsync(StateHasChanged); + } + }); + + return result; + } private async Task Delete(NodeDetailResponse detailResponse) { @@ -167,35 +229,9 @@ ); } - private Task LoadNodeStatus(int node) - { - Task.Run(async () => - { - try - { - var status = await NodeService.GetSystemStatus(node); - - lock (Responses) - Responses[node] = status; - } - catch (Exception e) - { - lock (Responses) - Responses[node] = null; - } - - await InvokeAsync(StateHasChanged); - }); - - return Task.CompletedTask; - } - private async Task ShowErrorDetails(int id) { - NodeSystemStatusResponse? data; - - lock (Responses) - data = Responses.GetValueOrDefault(id); + var data = StatusResponses.GetValueOrDefault(id); if (data == null) return; diff --git a/MoonlightServers.Frontend/UI/Views/Admin/Nodes/Update.razor b/MoonlightServers.Frontend/UI/Views/Admin/Nodes/Update.razor index 44ccb72..ab11ef0 100644 --- a/MoonlightServers.Frontend/UI/Views/Admin/Nodes/Update.razor +++ b/MoonlightServers.Frontend/UI/Views/Admin/Nodes/Update.razor @@ -14,7 +14,7 @@ @attribute [Authorize(Policy = "permissions:admin.servers.nodes.update")] - + Back @@ -29,12 +29,16 @@ + + + + - + - + diff --git a/MoonlightServers.Shared/Http/Responses/Admin/Nodes/Statistics/DockerStatisticsResponse.cs b/MoonlightServers.Shared/Http/Responses/Admin/Nodes/Statistics/DockerStatisticsResponse.cs new file mode 100644 index 0000000..b12e46d --- /dev/null +++ b/MoonlightServers.Shared/Http/Responses/Admin/Nodes/Statistics/DockerStatisticsResponse.cs @@ -0,0 +1,15 @@ +namespace MoonlightServers.Shared.Http.Responses.Admin.Nodes.Statistics; + +public class DockerStatisticsResponse +{ + public string Version { get; set; } + + public long ImagesUsed { get; set; } + public long ImagesReclaimable { get; set; } + + public long ContainersUsed { get; set; } + public long ContainersReclaimable { get; set; } + + public long BuildCacheUsed { get; set; } + public long BuildCacheReclaimable { get; set; } +} \ No newline at end of file diff --git a/MoonlightServers.Shared/Http/Responses/Admin/Nodes/Statistics/StatisticsResponse.cs b/MoonlightServers.Shared/Http/Responses/Admin/Nodes/Statistics/StatisticsResponse.cs new file mode 100644 index 0000000..25f07e5 --- /dev/null +++ b/MoonlightServers.Shared/Http/Responses/Admin/Nodes/Statistics/StatisticsResponse.cs @@ -0,0 +1,36 @@ +namespace MoonlightServers.Shared.Http.Responses.Admin.Nodes.Statistics; + +public class StatisticsResponse +{ + public CpuData Cpu { get; set; } + public MemoryData Memory { get; set; } + + public DiskData[] Disks { get; set; } + + public record DiskData + { + public string Device { get; set; } + public string MountPath { get; set; } + public ulong DiskTotal { get; set; } + public ulong DiskFree { get; set; } + public ulong InodesTotal { get; set; } + public ulong InodesFree { get; set; } + } + + public record MemoryData + { + public long Total { get; set; } + public long Available { get; set; } + public long Free { get; set; } + public long Cached { get; set; } + public long SwapTotal { get; set; } + public long SwapFree { get; set; } + } + + public record CpuData + { + public string Model { get; set; } + public double Usage { get; set; } + public double[] UsagePerCore { get; set; } + } +} \ No newline at end of file