feat(core): add Prometheus metrics provider and node mapper

This commit is contained in:
dualfroz committed 2026-08-25 12:10:49 +02:00
1 parent 42bc2d1695
commit 3a913c4375
2 files changed
+124

No files matched your search

@@ -0,0 +1,32 @@
using System.Collections.Concurrent;
using Vantage.Core.Metrics;
using Vantage.Core.Models;
namespace Vantage.Core.Prometheus;
public sealed class PrometheusMetricsProvider : IMetricsProvider
{
private readonly HttpClient httpClient;
private readonly Func<DateTimeOffset> clock;
private readonly ConcurrentDictionary<string, CpuCounters> cpuState = new();
public PrometheusMetricsProvider(HttpClient httpClient, Func<DateTimeOffset>? clock = null)
{
this.httpClient = httpClient;
this.clock = clock ?? (() => DateTimeOffset.UtcNow);
}
public async Task<HostMetrics> GetMetricsAsync(HostConfig host, CancellationToken cancellationToken)
{
var endpoint = host.ResolveMetricsEndpoint();
var exposition = await httpClient.GetStringAsync(endpoint, cancellationToken).ConfigureAwait(false);
var samples = PrometheusTextParser.Parse(exposition);
var current = PrometheusNodeMapper.ExtractCpuCounters(samples);
var hadPrevious = cpuState.TryGetValue(host.Id, out var previous);
cpuState[host.Id] = current;
var cpuPercent = hadPrevious ? PrometheusNodeMapper.CpuPercent(previous, current) : 0;
return PrometheusNodeMapper.Map(samples, host.DiskMount, cpuPercent, clock());
}
}
@@ -0,0 +1,92 @@
using Vantage.Core.Models;
namespace Vantage.Core.Prometheus;
public readonly record struct CpuCounters(double IdleSeconds, double TotalSeconds);
public static class PrometheusNodeMapper
{
public static CpuCounters ExtractCpuCounters(IReadOnlyList<PrometheusSample> samples)
{
double idle = 0;
double total = 0;
foreach (var sample in samples)
{
if (sample.Name != "node_cpu_seconds_total")
continue;
total += sample.Value;
if (sample.HasLabel("mode", "idle"))
idle += sample.Value;
}
return new CpuCounters(idle, total);
}
public static double CpuPercent(CpuCounters previous, CpuCounters current)
{
var totalDelta = current.TotalSeconds - previous.TotalSeconds;
var idleDelta = current.IdleSeconds - previous.IdleSeconds;
if (totalDelta <= 0)
return 0;
var busy = (totalDelta - idleDelta) / totalDelta * 100.0;
return Math.Clamp(Math.Round(busy, 1), 0, 100);
}
public static HostMetrics Map(
IReadOnlyList<PrometheusSample> samples,
string diskMount,
double cpuPercent,
DateTimeOffset now)
{
var memTotal = (long)First(samples, "node_memory_MemTotal_bytes");
var memAvailable = (long)First(samples, "node_memory_MemAvailable_bytes");
var memUsed = Math.Max(0, memTotal - memAvailable);
var diskSize = (long)FirstForMount(samples, "node_filesystem_size_bytes", diskMount);
var diskAvail = (long)FirstForMount(samples, "node_filesystem_avail_bytes", diskMount);
var diskUsed = Math.Max(0, diskSize - diskAvail);
var uptimeSeconds = First(samples, "node_time_seconds") - First(samples, "node_boot_time_seconds");
var uptime = uptimeSeconds > 0 ? TimeSpan.FromSeconds(uptimeSeconds) : TimeSpan.Zero;
return new HostMetrics
{
CpuUsagePercent = cpuPercent,
MemoryUsedBytes = memUsed,
MemoryTotalBytes = memTotal,
DiskUsedBytes = diskUsed,
DiskTotalBytes = diskSize,
Load1 = First(samples, "node_load1"),
Load5 = First(samples, "node_load5"),
Load15 = First(samples, "node_load15"),
Uptime = uptime,
CollectedAt = now
};
}
private static double First(IReadOnlyList<PrometheusSample> samples, string name)
{
foreach (var sample in samples)
if (sample.Name == name)
return sample.Value;
return 0;
}
private static double FirstForMount(IReadOnlyList<PrometheusSample> samples, string name, string mount)
{
double? fallback = null;
foreach (var sample in samples)
{
if (sample.Name != name)
continue;
if (sample.HasLabel("mountpoint", mount))
return sample.Value;
fallback ??= sample.Value;
}
return fallback ?? 0;
}
}