From 9e09780cc07c3fbe70b052f961e14e4ab54b87dc Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 24 Aug 2026 18:49:34 +0200 Subject: [PATCH] feat(core): add system metrics text parsers --- src/Vantage.Core/Metrics/Parsing/CpuUsage.cs | 16 ++++++ src/Vantage.Core/Metrics/Parsing/DfParser.cs | 30 ++++++++++ .../Metrics/Parsing/LoadAvgParser.cs | 20 +++++++ .../Metrics/Parsing/MemInfoParser.cs | 56 +++++++++++++++++++ .../Metrics/Parsing/ProcStatParser.cs | 33 +++++++++++ .../Metrics/Parsing/UptimeParser.cs | 16 ++++++ 6 files changed, 171 insertions(+) create mode 100644 src/Vantage.Core/Metrics/Parsing/CpuUsage.cs create mode 100644 src/Vantage.Core/Metrics/Parsing/DfParser.cs create mode 100644 src/Vantage.Core/Metrics/Parsing/LoadAvgParser.cs create mode 100644 src/Vantage.Core/Metrics/Parsing/MemInfoParser.cs create mode 100644 src/Vantage.Core/Metrics/Parsing/ProcStatParser.cs create mode 100644 src/Vantage.Core/Metrics/Parsing/UptimeParser.cs diff --git a/src/Vantage.Core/Metrics/Parsing/CpuUsage.cs b/src/Vantage.Core/Metrics/Parsing/CpuUsage.cs new file mode 100644 index 0000000..236d47d --- /dev/null +++ b/src/Vantage.Core/Metrics/Parsing/CpuUsage.cs @@ -0,0 +1,16 @@ +namespace Vantage.Core.Metrics.Parsing; + +public static class CpuUsage +{ + public static double Compute(CpuTimes previous, CpuTimes current) + { + var totalDelta = current.TotalTime - previous.TotalTime; + var idleDelta = current.IdleTime - previous.IdleTime; + + if (totalDelta <= 0) + return 0; + + var busyRatio = (double)(totalDelta - idleDelta) / totalDelta; + return Math.Clamp(Math.Round(busyRatio * 100.0, 1), 0, 100); + } +} diff --git a/src/Vantage.Core/Metrics/Parsing/DfParser.cs b/src/Vantage.Core/Metrics/Parsing/DfParser.cs new file mode 100644 index 0000000..479e9d5 --- /dev/null +++ b/src/Vantage.Core/Metrics/Parsing/DfParser.cs @@ -0,0 +1,30 @@ +using System.Globalization; + +namespace Vantage.Core.Metrics.Parsing; + +public readonly record struct DiskUsage(long UsedBytes, long TotalBytes); + +public static class DfParser +{ + public static DiskUsage Parse(string dfOutput) + { + var lines = dfOutput + .Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(l => l.Trim()) + .Where(l => l.Length > 0) + .ToArray(); + + if (lines.Length < 2) + throw new FormatException("df output has no data row"); + + var dataLine = lines[^1]; + var fields = dataLine.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (fields.Length < 5) + throw new FormatException($"Unexpected df row: '{dataLine}'"); + + var totalBytes = long.Parse(fields[^5], CultureInfo.InvariantCulture); + var usedBytes = long.Parse(fields[^4], CultureInfo.InvariantCulture); + + return new DiskUsage(usedBytes, totalBytes); + } +} diff --git a/src/Vantage.Core/Metrics/Parsing/LoadAvgParser.cs b/src/Vantage.Core/Metrics/Parsing/LoadAvgParser.cs new file mode 100644 index 0000000..9f74a97 --- /dev/null +++ b/src/Vantage.Core/Metrics/Parsing/LoadAvgParser.cs @@ -0,0 +1,20 @@ +using System.Globalization; + +namespace Vantage.Core.Metrics.Parsing; + +public readonly record struct LoadAverage(double Load1, double Load5, double Load15); + +public static class LoadAvgParser +{ + public static LoadAverage Parse(string procLoadavg) + { + var fields = procLoadavg.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (fields.Length < 3) + throw new FormatException($"Unexpected /proc/loadavg content: '{procLoadavg}'"); + + return new LoadAverage( + double.Parse(fields[0], CultureInfo.InvariantCulture), + double.Parse(fields[1], CultureInfo.InvariantCulture), + double.Parse(fields[2], CultureInfo.InvariantCulture)); + } +} diff --git a/src/Vantage.Core/Metrics/Parsing/MemInfoParser.cs b/src/Vantage.Core/Metrics/Parsing/MemInfoParser.cs new file mode 100644 index 0000000..dac5e38 --- /dev/null +++ b/src/Vantage.Core/Metrics/Parsing/MemInfoParser.cs @@ -0,0 +1,56 @@ +using System.Globalization; + +namespace Vantage.Core.Metrics.Parsing; + +public readonly record struct MemoryInfo(long UsedBytes, long TotalBytes); + +public static class MemInfoParser +{ + public static MemoryInfo Parse(string procMeminfo) + { + long? totalKb = null; + long? availableKb = null; + long? freeKb = null; + + foreach (var rawLine in procMeminfo.Split('\n')) + { + var line = rawLine.Trim(); + if (line.Length == 0) + continue; + + var colon = line.IndexOf(':'); + if (colon <= 0) + continue; + + var key = line[..colon].Trim(); + var value = ParseKilobytes(line[(colon + 1)..]); + + switch (key) + { + case "MemTotal": + totalKb = value; + break; + case "MemAvailable": + availableKb = value; + break; + case "MemFree": + freeKb = value; + break; + } + } + + if (totalKb is null) + throw new FormatException("MemTotal not found in /proc/meminfo"); + + var availabilityKb = availableKb ?? freeKb ?? 0; + var usedKb = Math.Max(0, totalKb.Value - availabilityKb); + + return new MemoryInfo(usedKb * 1024, totalKb.Value * 1024); + } + + private static long ParseKilobytes(string valueWithUnit) + { + var fields = valueWithUnit.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return long.Parse(fields[0], CultureInfo.InvariantCulture); + } +} diff --git a/src/Vantage.Core/Metrics/Parsing/ProcStatParser.cs b/src/Vantage.Core/Metrics/Parsing/ProcStatParser.cs new file mode 100644 index 0000000..c77e64e --- /dev/null +++ b/src/Vantage.Core/Metrics/Parsing/ProcStatParser.cs @@ -0,0 +1,33 @@ +using System.Globalization; + +namespace Vantage.Core.Metrics.Parsing; + +public readonly record struct CpuTimes(long IdleTime, long TotalTime); + +public static class ProcStatParser +{ + public static CpuTimes Parse(string procStat) + { + foreach (var rawLine in procStat.Split('\n')) + { + var line = rawLine.Trim(); + if (!line.StartsWith("cpu ", StringComparison.Ordinal) && line != "cpu") + continue; + + var fields = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + long total = 0; + long idle = 0; + for (var i = 1; i < fields.Length; i++) + { + var value = long.Parse(fields[i], CultureInfo.InvariantCulture); + total += value; + if (i == 4 || i == 5) + idle += value; + } + + return new CpuTimes(idle, total); + } + + throw new FormatException("Aggregate 'cpu' line not found in /proc/stat"); + } +} diff --git a/src/Vantage.Core/Metrics/Parsing/UptimeParser.cs b/src/Vantage.Core/Metrics/Parsing/UptimeParser.cs new file mode 100644 index 0000000..1b683a9 --- /dev/null +++ b/src/Vantage.Core/Metrics/Parsing/UptimeParser.cs @@ -0,0 +1,16 @@ +using System.Globalization; + +namespace Vantage.Core.Metrics.Parsing; + +public static class UptimeParser +{ + public static TimeSpan Parse(string procUptime) + { + var fields = procUptime.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (fields.Length < 1) + throw new FormatException($"Unexpected /proc/uptime content: '{procUptime}'"); + + var seconds = double.Parse(fields[0], CultureInfo.InvariantCulture); + return TimeSpan.FromSeconds(seconds); + } +}