182 lines
6.7 KiB
C#
182 lines
6.7 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|