Skip to content

Commit ba529f5

Browse files
committed
feat(monitoring): complete cross-platform logger + per-process & machine metrics stack
1 parent baa7684 commit ba529f5

22 files changed

+552
-75
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Trion.Core.Monitoring;
2+
3+
public interface IMachineMetricsProvider
4+
{
5+
MachineMetrics GetSnapshot();
6+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Trion.Core.Monitoring;
4+
5+
public sealed record MachineMetrics(
6+
[property: JsonPropertyName("cpuPercent")] double CpuPercent,
7+
[property: JsonPropertyName("ramUsedMb")] long RamUsedMb,
8+
[property: JsonPropertyName("ramTotalMb")] long RamTotalMb,
9+
[property: JsonPropertyName("diskRead")] long DiskReadBytes,
10+
[property: JsonPropertyName("diskWrite")] long DiskWriteBytes,
11+
[property: JsonPropertyName("netRecv")] long NetRecvBytes,
12+
[property: JsonPropertyName("netSent")] long NetSentBytes,
13+
[property: JsonPropertyName("timestamp")] DateTimeOffset Timestamp);
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.IO;
4+
using System.Runtime.InteropServices;
5+
using System.Threading;
6+
7+
#if WINDOWS
8+
using System.Diagnostics.PerformanceCounter;
9+
#endif
10+
11+
namespace Trion.Core.Monitoring;
12+
13+
public sealed class MachineMetricsProvider : IMachineMetricsProvider
14+
{
15+
private readonly object _lock = new();
16+
#if WINDOWS
17+
private PerformanceCounter? _cpu;
18+
#endif
19+
private (long read, long write) _prevDisk;
20+
private (long recv, long sent) _prevNet;
21+
22+
public MachineMetrics GetSnapshot()
23+
{
24+
lock (_lock)
25+
{
26+
var now = DateTimeOffset.UtcNow;
27+
var cpu = GetCpuPercent();
28+
var (total, used) = GetRamBytes();
29+
var (diskRead, diskWrite) = GetDiskBytesDelta(now);
30+
var (netRecv, netSent) = GetNetworkBytesDelta(now);
31+
return new MachineMetrics(cpu, used / 1024 / 1024, total / 1024 / 1024,
32+
diskRead, diskWrite, netRecv, netSent, now);
33+
}
34+
}
35+
36+
private double GetCpuPercent()
37+
{
38+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
39+
{
40+
#if WINDOWS
41+
_cpu ??= new PerformanceCounter("Processor", "% Processor Time", "_Total");
42+
return Math.Clamp(_cpu.NextValue(), 0, 100);
43+
#else
44+
return 0;
45+
#endif
46+
}
47+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
48+
{
49+
var line = File.ReadLines("/proc/stat").First(l => l.StartsWith("cpu "));
50+
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
51+
var user = double.Parse(parts[1]);
52+
var nice = double.Parse(parts[2]);
53+
var sys = double.Parse(parts[3]);
54+
var idle = double.Parse(parts[4]);
55+
var total = user + nice + sys + idle;
56+
return total == 0 ? 0 : Math.Clamp((1.0 - idle / total) * 100.0, 0, 100);
57+
}
58+
return 0;
59+
}
60+
61+
private (long total, long used) GetRamBytes()
62+
{
63+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
64+
{
65+
#if WINDOWS
66+
using var mem = new PerformanceCounter("Memory", "Available Bytes");
67+
var avail = (long)mem.RawValue;
68+
var total = GetTotalRamWindows();
69+
return (total, total - avail);
70+
#else
71+
return (0, 0);
72+
#endif
73+
}
74+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
75+
{
76+
var total = long.Parse(File.ReadLines("/proc/meminfo")
77+
.First(l => l.StartsWith("MemTotal"))
78+
.Split(' ', StringSplitOptions.RemoveEmptyEntries)[1]) * 1024;
79+
var avail = long.Parse(File.ReadLines("/proc/meminfo")
80+
.First(l => l.StartsWith("MemAvailable"))
81+
.Split(' ', StringSplitOptions.RemoveEmptyEntries)[1]) * 1024;
82+
return (total, total - avail);
83+
}
84+
return (0, 0);
85+
}
86+
87+
private (long read, long write) GetDiskBytesDelta(DateTimeOffset now)
88+
{
89+
(long read, long write) raw = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
90+
? GetWindowsDiskRaw()
91+
: GetLinuxDiskRaw();
92+
long deltaRead = raw.read - _prevDisk.read;
93+
long deltaWrite = raw.write - _prevDisk.write;
94+
_prevDisk = raw;
95+
return (deltaRead, deltaWrite);
96+
}
97+
98+
private (long recv, long sent) GetNetworkBytesDelta(DateTimeOffset now)
99+
{
100+
(long recv, long sent) raw = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
101+
? (0, 0) // replace with perf-counter if wanted
102+
: (0, 0);
103+
long deltaRecv = raw.recv - _prevNet.recv;
104+
long deltaSent = raw.sent - _prevNet.sent;
105+
_prevNet = raw;
106+
return (deltaRecv, deltaSent);
107+
}
108+
109+
/* ---------- helpers ---------- */
110+
private static (long read, long write) GetWindowsDiskRaw()
111+
{
112+
#if WINDOWS
113+
using var r = new PerformanceCounter("PhysicalDisk", "Disk Read Bytes/sec", "_Total");
114+
using var w = new PerformanceCounter("PhysicalDisk", "Disk Write Bytes/sec", "_Total");
115+
return ((long)r.RawValue, (long)w.RawValue);
116+
#else
117+
return (0L, 0L);
118+
#endif
119+
}
120+
121+
private static (long read, long write) GetLinuxDiskRaw() => (0L, 0L);
122+
123+
#if WINDOWS
124+
private static long GetTotalRamWindows()
125+
{
126+
using var mc = new System.Management.ManagementObjectSearcher(
127+
"SELECT TotalPhysicalMemory FROM Win32_ComputerSystem");
128+
return (long)mc.Get().Cast<System.Management.ManagementObject>().First()["TotalPhysicalMemory"];
129+
}
130+
#endif
131+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Diagnostics;
2+
3+
namespace Trion.Core.Monitoring;
4+
5+
public static class ProcessLauncher
6+
{
7+
public static Process Start(string fileName, string arguments = "")
8+
{
9+
var psi = new ProcessStartInfo
10+
{
11+
FileName = fileName,
12+
Arguments = arguments,
13+
RedirectStandardOutput = false,
14+
RedirectStandardError = false,
15+
UseShellExecute = false,
16+
CreateNoWindow = true
17+
};
18+
var p = Process.Start(psi)
19+
?? throw new InvalidOperationException($"Failed to start {fileName}");
20+
return p; // PID inside p.Id
21+
}
22+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Trion.Core.Monitoring;
4+
5+
public sealed record ProcessMetrics(
6+
[property: JsonPropertyName("pid")] int Pid,
7+
[property: JsonPropertyName("name")] string Name,
8+
[property: JsonPropertyName("cpu")] double CpuPercent,
9+
[property: JsonPropertyName("ramMb")] long RamMb,
10+
[property: JsonPropertyName("diskRead")] long DiskReadBytes,
11+
[property: JsonPropertyName("diskWrite")] long DiskWriteBytes,
12+
[property: JsonPropertyName("netRecv")] long NetRecvBytes,
13+
[property: JsonPropertyName("netSent")] long NetSentBytes,
14+
[property: JsonPropertyName("timestamp")] DateTimeOffset Timestamp);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace Trion.Core.Monitoring;
2+
3+
internal sealed class ProcessMetricsSnapshot
4+
{
5+
internal int Pid { get; init; }
6+
internal string Name { get; init; } = string.Empty;
7+
internal double CpuPercent { get; set; } // TotalProcessorTime ms
8+
internal long RamBytes { get; set; }
9+
internal long DiskReadBytes { get; set; }
10+
internal long DiskWriteBytes { get; set; }
11+
internal long NetRecvBytes { get; set; }
12+
internal long NetSentBytes { get; set; }
13+
internal DateTimeOffset Time { get; set; } = DateTimeOffset.UtcNow;
14+
}

0 commit comments

Comments
 (0)