Files
MoShiro/backend/agent/src/Monitor.Agent/Host/HostSampler.cs
T
2026-10-04 21:46:39 +08:00

182 lines
6.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Globalization;
using Monitor.Protocol;
namespace Monitor.Agent;
/// <summary>
/// 主机资源占用的只读快照:CPU 占用、内存、根分区、网卡累计流量与瞬时网速、系统运行时长。
/// 只有汇总的数字:<b>不采集 CPU 型号、主机名、网卡名和任何 IP 地址</b>,所以面板和展示页想泄漏也泄漏不出去。
/// Linux 读 <c>/proc</c>;读不到的项是 null(例如 Windows 上的 CPU)。不执行任何命令,不写文件。
/// </summary>
public sealed class HostSampler
{
private static readonly string[] s_virtualPrefixes = ["lo", "veth", "docker", "br-", "virbr", "cni", "flannel", "cali", "tun", "tap"];
private readonly TimeProvider _time;
private readonly Func<string, string?> _read;
private readonly Func<(long Total, long Used)?> _disk;
private (long Total, long Idle)? _previousCpu;
private (long At, long Rx, long Tx)? _previousNet;
public HostSampler(TimeProvider time, Func<string, string?>? read = null, Func<(long Total, long Used)?>? disk = null)
{
_time = time;
_read = read ?? ReadProc;
_disk = disk ?? ReadRootDisk;
}
public HostSnapshot Sample()
{
long now = _time.GetTimestamp();
var snapshot = new HostSnapshot { AtUtcMs = _time.GetUtcNow().ToUnixTimeMilliseconds() };
if (_read("/proc/stat") is { } stat && ParseCpu(stat) is { } cpu)
{
if (_previousCpu is { } before && cpu.Total > before.Total)
{
double busy = 1.0 - ((double)(cpu.Idle - before.Idle) / (cpu.Total - before.Total));
snapshot.CpuPercent = Math.Round(Math.Clamp(busy * 100, 0, 100), 1);
}
_previousCpu = cpu;
}
if (_read("/proc/meminfo") is { } meminfo && ParseMemory(meminfo) is { } memory)
{
snapshot.MemTotal = memory.Total;
snapshot.MemUsed = memory.Used;
}
if (_disk() is { } disk)
{
snapshot.DiskTotal = disk.Total;
snapshot.DiskUsed = disk.Used;
}
if (_read("/proc/net/dev") is { } dev && ParseNetwork(dev) is { } net)
{
snapshot.NetRxTotal = net.Rx;
snapshot.NetTxTotal = net.Tx;
if (_previousNet is { } before && net.Rx >= before.Rx && net.Tx >= before.Tx)
{
double seconds = Math.Max(0.001, _time.GetElapsedTime(before.At, now).TotalSeconds);
snapshot.NetRxBps = (long)((net.Rx - before.Rx) / seconds);
snapshot.NetTxBps = (long)((net.Tx - before.Tx) / seconds);
}
_previousNet = (now, net.Rx, net.Tx);
}
snapshot.UptimeSeconds = _read("/proc/uptime") is { } uptime && ParseUptime(uptime) is { } up ? up : Environment.TickCount64 / 1000;
return snapshot;
}
/// <summary>第一行 <c>cpu user nice system idle iowait irq softirq steal …</c>:总时间与空闲时间(idle + iowait)。</summary>
public static (long Total, long Idle)? ParseCpu(string stat)
{
string? line = stat.Split('\n').FirstOrDefault(l => l.StartsWith("cpu ", StringComparison.Ordinal));
if (line is null)
{
return null;
}
long[] fields = [.. line.Split(' ', StringSplitOptions.RemoveEmptyEntries).Skip(1).Take(8).Select(f => long.TryParse(f, CultureInfo.InvariantCulture, out long v) ? v : 0)];
if (fields.Length < 5)
{
return null;
}
return (fields.Sum(), fields[3] + fields[4]);
}
/// <summary>MemTotal 与 MemAvailable(kB)→ 字节;已用 = 总量 − 可用。</summary>
public static (long Total, long Used)? ParseMemory(string meminfo)
{
long? total = null;
long? available = null;
foreach (string line in meminfo.Split('\n'))
{
if (line.StartsWith("MemTotal:", StringComparison.Ordinal))
{
total = KiloBytes(line);
}
else if (line.StartsWith("MemAvailable:", StringComparison.Ordinal))
{
available = KiloBytes(line);
}
}
return total is { } t && available is { } a ? (t, Math.Max(0, t - a)) : null;
}
/// <summary>所有物理网卡(排除回环、容器、虚拟网桥)的累计收发字节。</summary>
public static (long Rx, long Tx)? ParseNetwork(string dev)
{
long rx = 0;
long tx = 0;
bool any = false;
foreach (string line in dev.Split('\n'))
{
int colon = line.IndexOf(':', StringComparison.Ordinal);
if (colon < 0)
{
continue;
}
string name = line[..colon].Trim();
if (s_virtualPrefixes.Any(p => name.StartsWith(p, StringComparison.Ordinal)))
{
continue;
}
string[] fields = line[(colon + 1)..].Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (fields.Length >= 9 && long.TryParse(fields[0], CultureInfo.InvariantCulture, out long r) && long.TryParse(fields[8], CultureInfo.InvariantCulture, out long t))
{
rx += r;
tx += t;
any = true;
}
}
return any ? (rx, tx) : null;
}
public static long? ParseUptime(string uptime)
{
string first = uptime.Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? "";
return double.TryParse(first, NumberStyles.Float, CultureInfo.InvariantCulture, out double seconds) ? (long)seconds : null;
}
private static long? KiloBytes(string line)
{
string[] parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return parts.Length >= 2 && long.TryParse(parts[1], CultureInfo.InvariantCulture, out long kb) ? kb * 1024 : null;
}
private static string? ReadProc(string path)
{
try
{
return OperatingSystem.IsLinux() && File.Exists(path) ? File.ReadAllText(path) : null;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
return null;
}
}
private static (long Total, long Used)? ReadRootDisk()
{
try
{
string root = OperatingSystem.IsWindows() ? Path.GetPathRoot(Environment.SystemDirectory) ?? "C:\\" : "/";
var drive = new DriveInfo(root);
return drive.IsReady ? (drive.TotalSize, drive.TotalSize - drive.AvailableFreeSpace) : null;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException)
{
return null;
}
}
}