feat(core): add SSH metrics provider and bundle parser
This commit is contained in:
1 parent
9e09780cc0
commit
7c44f54376
5 files changed
+165
No files matched your search
@@ -0,0 +1,8 @@
|
||||
using Vantage.Core.Models;
|
||||
|
||||
namespace Vantage.Core.Metrics;
|
||||
|
||||
public interface IMetricsProvider
|
||||
{
|
||||
Task<HostMetrics> GetMetricsAsync(HostConfig host, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using Vantage.Core.Metrics.Parsing;
|
||||
using Vantage.Core.Models;
|
||||
|
||||
namespace Vantage.Core.Metrics;
|
||||
|
||||
public static class MetricsBundleParser
|
||||
{
|
||||
public static HostMetrics Parse(string bundle)
|
||||
{
|
||||
var sections = SplitSections(bundle);
|
||||
|
||||
var load = LoadAvgParser.Parse(Require(sections, MetricsCommand.LoadAvg));
|
||||
var uptime = UptimeParser.Parse(Require(sections, MetricsCommand.Uptime));
|
||||
var memory = MemInfoParser.Parse(Require(sections, MetricsCommand.MemInfo));
|
||||
var disk = DfParser.Parse(Require(sections, MetricsCommand.Df));
|
||||
|
||||
var statBefore = ProcStatParser.Parse(Require(sections, MetricsCommand.Stat1));
|
||||
var statAfter = ProcStatParser.Parse(Require(sections, MetricsCommand.Stat2));
|
||||
var cpu = CpuUsage.Compute(statBefore, statAfter);
|
||||
|
||||
return new HostMetrics
|
||||
{
|
||||
CpuUsagePercent = cpu,
|
||||
MemoryUsedBytes = memory.UsedBytes,
|
||||
MemoryTotalBytes = memory.TotalBytes,
|
||||
DiskUsedBytes = disk.UsedBytes,
|
||||
DiskTotalBytes = disk.TotalBytes,
|
||||
Load1 = load.Load1,
|
||||
Load5 = load.Load5,
|
||||
Load15 = load.Load15,
|
||||
Uptime = uptime,
|
||||
CollectedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> SplitSections(string bundle)
|
||||
{
|
||||
var sections = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
string? current = null;
|
||||
var buffer = new List<string>();
|
||||
|
||||
void Flush()
|
||||
{
|
||||
if (current is not null)
|
||||
sections[current] = string.Join('\n', buffer);
|
||||
buffer.Clear();
|
||||
}
|
||||
|
||||
foreach (var rawLine in bundle.Replace("\r\n", "\n").Split('\n'))
|
||||
{
|
||||
if (rawLine.StartsWith(MetricsCommand.MarkerPrefix, StringComparison.Ordinal))
|
||||
{
|
||||
Flush();
|
||||
current = rawLine[MetricsCommand.MarkerPrefix.Length..].Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.Add(rawLine);
|
||||
}
|
||||
}
|
||||
|
||||
Flush();
|
||||
return sections;
|
||||
}
|
||||
|
||||
private static string Require(IReadOnlyDictionary<string, string> sections, string name) =>
|
||||
sections.TryGetValue(name, out var value)
|
||||
? value
|
||||
: throw new FormatException($"Metrics bundle is missing section '{name}'");
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace Vantage.Core.Metrics;
|
||||
|
||||
public static class MetricsCommand
|
||||
{
|
||||
public const string MarkerPrefix = "@@vantage:";
|
||||
public const string LoadAvg = "loadavg";
|
||||
public const string Uptime = "uptime";
|
||||
public const string MemInfo = "meminfo";
|
||||
public const string Df = "df";
|
||||
public const string Stat1 = "stat1";
|
||||
public const string Stat2 = "stat2";
|
||||
|
||||
public static string Build(string diskMount, int cpuSampleMs = 400)
|
||||
{
|
||||
var mount = Ssh.ShellArgs.Quote(diskMount);
|
||||
var sleepSeconds = (cpuSampleMs / 1000.0).ToString("0.###", System.Globalization.CultureInfo.InvariantCulture);
|
||||
return string.Join(" ; ", new[]
|
||||
{
|
||||
Emit(LoadAvg) + "cat /proc/loadavg",
|
||||
Emit(Uptime) + "cat /proc/uptime",
|
||||
Emit(MemInfo) + "cat /proc/meminfo",
|
||||
Emit(Df) + $"df -B1 -P {mount}",
|
||||
Emit(Stat1) + "cat /proc/stat",
|
||||
$"sleep {sleepSeconds}",
|
||||
Emit(Stat2) + "cat /proc/stat"
|
||||
});
|
||||
}
|
||||
|
||||
private static string Emit(string section) => $"echo '{MarkerPrefix}{section}' ; ";
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace Vantage.Core.Metrics;
|
||||
|
||||
public sealed class MetricsHistory
|
||||
{
|
||||
private readonly double[] buffer;
|
||||
private int count;
|
||||
private int head;
|
||||
|
||||
public MetricsHistory(int capacity)
|
||||
{
|
||||
if (capacity < 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(capacity), "History capacity must be positive");
|
||||
buffer = new double[capacity];
|
||||
}
|
||||
|
||||
public int Capacity => buffer.Length;
|
||||
public int Count => count;
|
||||
public double Latest => count == 0 ? 0 : buffer[(head - 1 + buffer.Length) % buffer.Length];
|
||||
|
||||
public void Add(double value)
|
||||
{
|
||||
buffer[head] = value;
|
||||
head = (head + 1) % buffer.Length;
|
||||
if (count < buffer.Length)
|
||||
count++;
|
||||
}
|
||||
|
||||
public IReadOnlyList<double> Snapshot()
|
||||
{
|
||||
var ordered = new double[count];
|
||||
var start = (head - count + buffer.Length) % buffer.Length;
|
||||
for (var i = 0; i < count; i++)
|
||||
ordered[i] = buffer[(start + i) % buffer.Length];
|
||||
return ordered;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Vantage.Core.Models;
|
||||
using Vantage.Core.Ssh;
|
||||
|
||||
namespace Vantage.Core.Metrics;
|
||||
|
||||
public sealed class SshMetricsProvider : IMetricsProvider
|
||||
{
|
||||
private readonly ISshCommandRunner runner;
|
||||
|
||||
public SshMetricsProvider(ISshCommandRunner runner)
|
||||
{
|
||||
this.runner = runner;
|
||||
}
|
||||
|
||||
public async Task<HostMetrics> GetMetricsAsync(HostConfig host, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = MetricsCommand.Build(host.DiskMount);
|
||||
var bundle = await runner.RunAsync(host, command, cancellationToken).ConfigureAwait(false);
|
||||
return MetricsBundleParser.Parse(bundle);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user