Improved node statistics. Added overview for single nodes and replaced mockup values with api fetched values for nodes list
This commit is contained in:
@@ -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<Node> NodeRepository;
|
||||||
|
|
||||||
|
public StatisticsController(NodeService nodeService, DatabaseRepository<Node> nodeRepository)
|
||||||
|
{
|
||||||
|
NodeService = nodeService;
|
||||||
|
NodeRepository = nodeRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{nodeId:int}/statistics")]
|
||||||
|
public async Task<StatisticsResponse> 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<DockerStatisticsResponse> 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<Node> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,16 +49,10 @@ public class NodeService
|
|||||||
|
|
||||||
#region Statistics
|
#region Statistics
|
||||||
|
|
||||||
public async Task<StatisticsApplicationResponse> GetApplicationStatistics(Node node)
|
public async Task<StatisticsResponse> GetStatistics(Node node)
|
||||||
{
|
{
|
||||||
using var apiClient = CreateApiClient(node);
|
using var apiClient = CreateApiClient(node);
|
||||||
return await apiClient.GetJson<StatisticsApplicationResponse>("api/statistics/application");
|
return await apiClient.GetJson<StatisticsResponse>("api/statistics");
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<StatisticsHostResponse> GetHostStatistics(Node node)
|
|
||||||
{
|
|
||||||
using var apiClient = CreateApiClient(node);
|
|
||||||
return await apiClient.GetJson<StatisticsHostResponse>("api/statistics/host");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<StatisticsDockerResponse> GetDockerStatistics(Node node)
|
public async Task<StatisticsDockerResponse> GetDockerStatistics(Node node)
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using Mono.Unix.Native;
|
||||||
using MoonCore.Attributes;
|
using MoonCore.Attributes;
|
||||||
|
using MoonCore.Helpers;
|
||||||
|
using MoonlightServers.Daemon.Models;
|
||||||
|
|
||||||
namespace MoonlightServers.Daemon.Helpers;
|
namespace MoonlightServers.Daemon.Helpers;
|
||||||
|
|
||||||
[Singleton]
|
[Singleton]
|
||||||
public class HostSystemHelper
|
public class HostSystemHelper
|
||||||
{
|
{
|
||||||
|
private readonly ILogger<HostSystemHelper> Logger;
|
||||||
|
|
||||||
|
public HostSystemHelper(ILogger<HostSystemHelper> logger)
|
||||||
|
{
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
public string GetOsName()
|
public string GetOsName()
|
||||||
{
|
{
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
@@ -45,5 +55,209 @@ public class HostSystemHelper
|
|||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region CPU Usage
|
||||||
|
|
||||||
|
public async Task<CpuUsageDetails> GetCpuUsage()
|
||||||
|
{
|
||||||
|
var result = new CpuUsageDetails();
|
||||||
|
var perCoreUsages = new List<double>();
|
||||||
|
|
||||||
|
// 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<long[]>();
|
||||||
|
var sumList = new List<long>();
|
||||||
|
|
||||||
|
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<MemoryUsageDetails> 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<DiskUsageDetails[]> GetDiskUsages()
|
||||||
|
{
|
||||||
|
var details = new List<DiskUsageDetails>();
|
||||||
|
|
||||||
|
// First we need to check which mounts actually exist
|
||||||
|
var diskDevices = new Dictionary<string, string>();
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<StatisticsApplicationResponse> Get()
|
|
||||||
{
|
|
||||||
return new StatisticsApplicationResponse()
|
|
||||||
{
|
|
||||||
Uptime = ProcessHelper.GetUptime(),
|
|
||||||
MemoryUsage = ProcessHelper.GetMemoryUsage(),
|
|
||||||
CpuUsage = ProcessHelper.CpuUsage()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<StatisticsResponse> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<StatisticsHostResponse> Get()
|
|
||||||
{
|
|
||||||
return new()
|
|
||||||
{
|
|
||||||
OperatingSystem = HostSystemHelper.GetOsName()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
8
MoonlightServers.Daemon/Models/CpuUsageDetails.cs
Normal file
8
MoonlightServers.Daemon/Models/CpuUsageDetails.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
11
MoonlightServers.Daemon/Models/DiskUsageDetails.cs
Normal file
11
MoonlightServers.Daemon/Models/DiskUsageDetails.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
11
MoonlightServers.Daemon/Models/MemoryUsageDetails.cs
Normal file
11
MoonlightServers.Daemon/Models/MemoryUsageDetails.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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; }
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Statistics;
|
|
||||||
|
|
||||||
public class StatisticsHostResponse
|
|
||||||
{
|
|
||||||
public string OperatingSystem { get; set; }
|
|
||||||
}
|
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using MoonCore.Attributes;
|
using MoonCore.Attributes;
|
||||||
using MoonCore.Helpers;
|
using MoonCore.Helpers;
|
||||||
|
using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Statistics;
|
||||||
using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys;
|
using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys;
|
||||||
|
|
||||||
namespace MoonlightServers.Frontend.Services;
|
namespace MoonlightServers.Frontend.Services;
|
||||||
@@ -18,4 +19,18 @@ public class NodeService
|
|||||||
{
|
{
|
||||||
return await HttpApiClient.GetJson<NodeSystemStatusResponse>($"api/admin/servers/nodes/{nodeId}/system/status");
|
return await HttpApiClient.GetJson<NodeSystemStatusResponse>($"api/admin/servers/nodes/{nodeId}/system/status");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<StatisticsResponse> GetStatistics(int nodeId)
|
||||||
|
{
|
||||||
|
return await HttpApiClient.GetJson<StatisticsResponse>(
|
||||||
|
$"api/admin/servers/nodes/{nodeId}/statistics"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DockerStatisticsResponse> GetDockerStatistics(int nodeId)
|
||||||
|
{
|
||||||
|
return await HttpApiClient.GetJson<DockerStatisticsResponse>(
|
||||||
|
$"api/admin/servers/nodes/{nodeId}/statistics/docker"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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<OverviewNodeUpdate> Logger
|
||||||
|
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
<LazyLoader Load="Load">
|
||||||
|
|
||||||
|
<div class="mb-3 mt-5 text-xl font-semibold">
|
||||||
|
Overview
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
||||||
|
<div class="col-span-1 card card-body">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p class="text-xl font-semibold text-slate-200">
|
||||||
|
@Math.Round(Statistics.Cpu.Usage, 2)%
|
||||||
|
</p>
|
||||||
|
<i class="icon-cpu text-4xl text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-base text-slate-300">
|
||||||
|
<span class="truncate">
|
||||||
|
CPU: @Statistics.Cpu.Model
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-1 card card-body">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p class="text-xl font-semibold text-slate-200">
|
||||||
|
@Formatter.FormatSize(Statistics.Memory.Total - Statistics.Memory.Available)
|
||||||
|
/
|
||||||
|
@Formatter.FormatSize(Statistics.Memory.Total)
|
||||||
|
</p>
|
||||||
|
<i class="icon-memory-stick text-4xl text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-base text-slate-300">
|
||||||
|
Memory
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-1 card card-body">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div class="text-xl font-semibold text-slate-200">
|
||||||
|
@Formatter.FormatSize(Statistics.Memory.SwapTotal - Statistics.Memory.SwapFree)
|
||||||
|
/
|
||||||
|
@Formatter.FormatSize(Statistics.Memory.SwapTotal)
|
||||||
|
</div>
|
||||||
|
<i class="icon-shapes text-4xl text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-base text-slate-300">
|
||||||
|
Swap
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 mt-5 text-xl font-semibold">
|
||||||
|
CPU
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card card-body grid grid-cols-1 lg:grid-cols-2 gap-y-2 gap-x-5">
|
||||||
|
@{
|
||||||
|
var i = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@foreach (var usage in Statistics.Cpu.UsagePerCore)
|
||||||
|
{
|
||||||
|
var percentRounded = Math.Round(usage, 2);
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center col-span-1">
|
||||||
|
<div class="text-sm text-slate-300 me-1.5 grow-0 flex flex-col">
|
||||||
|
<span>#@(i)</span>
|
||||||
|
</div>
|
||||||
|
<div class="grow">
|
||||||
|
<div class="progress bg-gray-750">
|
||||||
|
<div style="width: @(usage)%"
|
||||||
|
class="progress-bar h-3 @(GetBackgroundColorByPercent(percentRounded))"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 mt-5 text-xl font-semibold">
|
||||||
|
Disks
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
||||||
|
@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);
|
||||||
|
|
||||||
|
<div class="col-span-1 card card-body">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="grow">
|
||||||
|
<div class="progress bg-gray-750">
|
||||||
|
<div style="width: @(usedPercent)%"
|
||||||
|
class="progress-bar h-3 @(GetBackgroundColorByPercent(usedPercent))"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-slate-300 mt-2.5 flex flex-col">
|
||||||
|
<div>
|
||||||
|
Device: <span class="font-semibold">@disk.Device</span> - Mounted at: <span class="font-semibold truncate">@disk.MountPath</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Used: <span class="font-semibold">@Formatter.FormatSize(disk.DiskTotal - disk.DiskFree)</span>
|
||||||
|
Total: <span class="font-semibold">@Formatter.FormatSize(disk.DiskTotal)</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
INodes: <span class="font-semibold">@(iNodesPercent)%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3 mt-5 text-xl font-semibold">
|
||||||
|
Docker
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
||||||
|
<div class="col-span-1 card card-body">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p class="text-xl font-semibold text-slate-200">
|
||||||
|
@Formatter.FormatSize(DockerStatistics.ImagesUsed) (@Formatter.FormatSize(DockerStatistics.ImagesReclaimable) unused)
|
||||||
|
</p>
|
||||||
|
<i class="icon-gallery-horizontal-end text-4xl text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-base text-slate-300">
|
||||||
|
Images
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-1 card card-body">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p class="text-xl font-semibold text-slate-200">
|
||||||
|
@Formatter.FormatSize(DockerStatistics.ContainersUsed) ( @Formatter.FormatSize(DockerStatistics.ContainersReclaimable) unused)
|
||||||
|
</p>
|
||||||
|
<i class="icon-container text-4xl text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-base text-slate-300">
|
||||||
|
Containers
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-1 card card-body">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<p class="text-xl font-semibold text-slate-200">
|
||||||
|
@Formatter.FormatSize(DockerStatistics.BuildCacheUsed) (@Formatter.FormatSize(DockerStatistics.BuildCacheReclaimable) unused)
|
||||||
|
</p>
|
||||||
|
<i class="icon-hard-hat text-4xl text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="text-base text-slate-300">
|
||||||
|
Build Cache
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</LazyLoader>
|
||||||
|
|
||||||
|
@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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,12 +8,14 @@
|
|||||||
@using MoonCore.Blazor.Tailwind.Dt
|
@using MoonCore.Blazor.Tailwind.Dt
|
||||||
@using MoonCore.Blazor.Tailwind.Toasts
|
@using MoonCore.Blazor.Tailwind.Toasts
|
||||||
@using MoonlightServers.Frontend.Services
|
@using MoonlightServers.Frontend.Services
|
||||||
|
@using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Statistics
|
||||||
@using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys
|
@using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Sys
|
||||||
|
|
||||||
@inject HttpApiClient ApiClient
|
@inject HttpApiClient ApiClient
|
||||||
@inject NodeService NodeService
|
@inject NodeService NodeService
|
||||||
@inject AlertService AlertService
|
@inject AlertService AlertService
|
||||||
@inject ToastService ToastService
|
@inject ToastService ToastService
|
||||||
|
@inject ILogger<Index> Logger
|
||||||
|
|
||||||
@attribute [Authorize(Policy = "permissions:admin.servers.nodes.get")]
|
@attribute [Authorize(Policy = "permissions:admin.servers.nodes.get")]
|
||||||
|
|
||||||
@@ -44,86 +46,96 @@
|
|||||||
<DataTableColumn TItem="NodeDetailResponse" Field="@(x => x.Fqdn)" Name="Fqdn"/>
|
<DataTableColumn TItem="NodeDetailResponse" Field="@(x => x.Fqdn)" Name="Fqdn"/>
|
||||||
<DataTableColumn TItem="NodeDetailResponse" Field="@(x => x.Fqdn)" Name="Status">
|
<DataTableColumn TItem="NodeDetailResponse" Field="@(x => x.Fqdn)" Name="Status">
|
||||||
<ColumnTemplate>
|
<ColumnTemplate>
|
||||||
<LazyLoader Load="_ => LoadNodeStatus(context.Id)">
|
|
||||||
@{
|
@{
|
||||||
bool isFetched;
|
var isFetched = StatusResponses.TryGetValue(context.Id, out var data);
|
||||||
NodeSystemStatusResponse? data;
|
|
||||||
|
|
||||||
lock (Responses)
|
|
||||||
isFetched = Responses.TryGetValue(context.Id, out data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (isFetched)
|
@if (isFetched)
|
||||||
{
|
{
|
||||||
if (data == null)
|
if (data == null)
|
||||||
{
|
{
|
||||||
<span class="text-danger">
|
<div class="text-danger flex items-center">
|
||||||
<i class="icon-server-offg text-base me-1 align-middle"></i>
|
<i class="icon-server-off text-base me-1"></i>
|
||||||
<span class="align-middle">
|
<span>
|
||||||
API Error
|
API Error
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (data.RoundtripSuccess)
|
if (data.RoundtripSuccess)
|
||||||
{
|
{
|
||||||
<span class="text-success">
|
<div class="text-success flex items-center">
|
||||||
<i class="icon-check text-base me-1 align-middle"></i>
|
<i class="icon-check text-base me-1"></i>
|
||||||
<span class="align-middle">Online (@(data.Version))</span>
|
<span>Online (@(data.Version))</span>
|
||||||
</span>
|
</div>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="text-danger">
|
<div class="text-danger flex items-center">
|
||||||
<i class="icon-server-off text-base me-1 align-middle"></i>
|
<i class="icon-server-off text-base me-1"></i>
|
||||||
<span class="align-middle">
|
<span class="me-2">
|
||||||
Error
|
Error
|
||||||
|
</span>
|
||||||
<a @onclick="() => ShowErrorDetails(context.Id)" @onclick:preventDefault
|
<a @onclick="() => ShowErrorDetails(context.Id)" @onclick:preventDefault
|
||||||
href="#" class="ms-1 text-gray-600">Details</a>
|
href="#" class="ms-1 text-gray-600">Details</a>
|
||||||
</span>
|
</div>
|
||||||
</span>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<span class="text-gray-500">
|
<div class="text-gray-500">
|
||||||
<i class="icon-loader text-base me-1 align-middle"></i>
|
<i class="icon-loader text-base me-1 align-middle"></i>
|
||||||
<span class="align-middle">Loading</span>
|
<span class="align-middle">Loading</span>
|
||||||
</span>
|
</div>
|
||||||
}
|
}
|
||||||
</LazyLoader>
|
|
||||||
</ColumnTemplate>
|
</ColumnTemplate>
|
||||||
</DataTableColumn>
|
</DataTableColumn>
|
||||||
<DataTableColumn TItem="NodeDetailResponse"
|
<DataTableColumn TItem="NodeDetailResponse"
|
||||||
|
Name="Utilization"
|
||||||
HeaderCss="p-2 font-semibold text-left hidden xl:table-cell"
|
HeaderCss="p-2 font-semibold text-left hidden xl:table-cell"
|
||||||
ColumnCss="p-2 text-left font-normal hidden xl:table-cell">
|
ColumnCss="p-2 text-left font-normal hidden xl:table-cell">
|
||||||
<ColumnTemplate>
|
<ColumnTemplate>
|
||||||
<div>
|
@{
|
||||||
<i class="icon-cpu text-lg me-1 align-middle text-primary"></i>
|
var isFetched = Statistics.TryGetValue(context.Id, out var data);
|
||||||
<span class="align-middle">33% of 6 Cores</span>
|
}
|
||||||
|
|
||||||
|
@if (isFetched)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
<div class="flex items-center text-danger">
|
||||||
|
<i class="icon-server-off text-base me-1"></i>
|
||||||
|
<span>
|
||||||
|
API Error
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ColumnTemplate>
|
}
|
||||||
</DataTableColumn>
|
else
|
||||||
<DataTableColumn TItem="NodeDetailResponse"
|
{
|
||||||
HeaderCss="p-2 font-semibold text-left hidden xl:table-cell"
|
<div class="flex flex-row">
|
||||||
ColumnCss="p-2 text-left font-normal hidden xl:table-cell">
|
<div class="flex items-center">
|
||||||
<ColumnTemplate>
|
<i class="text-primary text-base me-2 icon-cpu"></i>
|
||||||
<div>
|
<span>@(Math.Round(data.Cpu.Usage))%</span>
|
||||||
<i class="icon-memory-stick text-lg me-1 align-middle text-primary"></i>
|
|
||||||
<span class="align-middle">1.56GB / 64GB</span>
|
|
||||||
</div>
|
</div>
|
||||||
</ColumnTemplate>
|
|
||||||
</DataTableColumn>
|
<div class="flex items-center ms-5">
|
||||||
<DataTableColumn TItem="NodeDetailResponse"
|
<i class="text-primary text-base me-2 icon-memory-stick"></i>
|
||||||
HeaderCss="p-2 font-semibold text-left hidden xl:table-cell"
|
<span>
|
||||||
ColumnCss="p-2 text-left font-normal hidden xl:table-cell">
|
@(Math.Round((data.Memory.Total - data.Memory.Free - data.Memory.Cached) / (double)data.Memory.Total * 100))%
|
||||||
<ColumnTemplate>
|
</span>
|
||||||
<div>
|
|
||||||
<i class="icon-hard-drive text-lg me-1 align-middle text-primary"></i>
|
|
||||||
<span class="align-middle">78.68GB / 1TB</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="flex items-center text-gray-500">
|
||||||
|
<i class="icon-loader text-base me-1"></i>
|
||||||
|
<span>Loading</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</ColumnTemplate>
|
</ColumnTemplate>
|
||||||
</DataTableColumn>
|
</DataTableColumn>
|
||||||
<DataTableColumn TItem="NodeDetailResponse">
|
<DataTableColumn TItem="NodeDetailResponse">
|
||||||
@@ -147,10 +159,60 @@
|
|||||||
{
|
{
|
||||||
private DataTable<NodeDetailResponse> Table;
|
private DataTable<NodeDetailResponse> Table;
|
||||||
|
|
||||||
private Dictionary<int, NodeSystemStatusResponse?> Responses = new();
|
private Dictionary<int, NodeSystemStatusResponse?> StatusResponses = new();
|
||||||
|
private Dictionary<int, StatisticsResponse?> Statistics = new();
|
||||||
|
|
||||||
private async Task<IPagedData<NodeDetailResponse>> LoadData(PaginationOptions options)
|
private async Task<IPagedData<NodeDetailResponse>> LoadData(PaginationOptions options)
|
||||||
=> await ApiClient.GetJson<PagedData<NodeDetailResponse>>($"api/admin/servers/nodes?page={options.Page}&pageSize={options.PerPage}");
|
{
|
||||||
|
Statistics.Clear();
|
||||||
|
StatusResponses.Clear();
|
||||||
|
|
||||||
|
var result = await ApiClient.GetJson<PagedData<NodeDetailResponse>>(
|
||||||
|
$"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)
|
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)
|
private async Task ShowErrorDetails(int id)
|
||||||
{
|
{
|
||||||
NodeSystemStatusResponse? data;
|
var data = StatusResponses.GetValueOrDefault(id);
|
||||||
|
|
||||||
lock (Responses)
|
|
||||||
data = Responses.GetValueOrDefault(id);
|
|
||||||
|
|
||||||
if (data == null)
|
if (data == null)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
@attribute [Authorize(Policy = "permissions:admin.servers.nodes.update")]
|
@attribute [Authorize(Policy = "permissions:admin.servers.nodes.update")]
|
||||||
|
|
||||||
<LazyLoader Load="Load">
|
<LazyLoader Load="Load">
|
||||||
<PageHeader Title="Update Node">
|
<PageHeader Title="@Node.Name">
|
||||||
<a href="/admin/servers/nodes" class="btn btn-secondary">
|
<a href="/admin/servers/nodes" class="btn btn-secondary">
|
||||||
<i class="icon-chevron-left mr-1"></i>
|
<i class="icon-chevron-left mr-1"></i>
|
||||||
Back
|
Back
|
||||||
@@ -29,6 +29,10 @@
|
|||||||
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
|
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
|
<Tab Name="Overview">
|
||||||
|
<OverviewNodeUpdate Node="Node" />
|
||||||
|
</Tab>
|
||||||
|
|
||||||
<Tab Name="Settings">
|
<Tab Name="Settings">
|
||||||
<GeneralNodeUpdate Request="Request"/>
|
<GeneralNodeUpdate Request="Request"/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user