Files
jmon/JMonAgent/MetricCollector.cs
James Getrost 127b3ad5bf Initial commit: JMon cross-platform monitoring system
- gRPC client-server metrics collection
- PeriodicTimer-based 5-second sampling
- Delta encoding with configurable thresholds
- Queue/retry mechanism for resilience
- ProcessId tracking for duplicate detection
- Server-side state reconstruction
- Native AOT compilation support
- Docker/Podman containerization
- PostgreSQL + TimescaleDB persistence layer
2026-01-05 23:23:55 -06:00

287 lines
8.0 KiB
C#

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace JMonAgent;
public interface IMetricCollector
{
SystemMetrics Collect();
}
public class LinuxMetricCollector : IMetricCollector
{
private long _prevJiffy;
private long _prevIdle;
private string? _cachedIpAddress;
private string? _cachedHwId;
public SystemMetrics Collect()
{
var memInfo = ParseMemInfo();
var swapInfo = ParseSwapInfo();
var diskInfo = ParseDiskInfo();
// Cache IP and HWID on first call
_cachedIpAddress ??= GetIpAddress();
_cachedHwId ??= GetHwId();
return new SystemMetrics(
Environment.MachineName,
RuntimeInformation.OSDescription,
GetCpuUsage(),
memInfo.UsedBytes,
memInfo.TotalBytes,
swapInfo.UsedBytes,
swapInfo.TotalBytes,
diskInfo.UsedBytes,
diskInfo.TotalBytes,
diskInfo.MountPoint,
DateTime.UtcNow,
_cachedIpAddress ?? "",
_cachedHwId ?? "",
Environment.ProcessId
);
}
private string GetIpAddress()
{
try
{
var psi = new ProcessStartInfo("hostname", "-I")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null) return "";
var output = process.StandardOutput.ReadToEnd().Trim();
var addresses = output.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return addresses.FirstOrDefault() ?? "";
}
catch
{
return "";
}
}
private string GetHwId()
{
try
{
// Try to read machine-id from Linux
if (File.Exists("/etc/machine-id"))
{
return File.ReadAllText("/etc/machine-id").Trim();
}
// Fallback: use MAC address from first interface
var interfaces = Directory.GetFiles("/sys/class/net/");
if (interfaces.Length > 0)
{
var firstInterface = Path.GetFileName(interfaces[0]);
var macPath = $"/sys/class/net/{firstInterface}/address";
if (File.Exists(macPath))
{
return File.ReadAllText(macPath).Trim();
}
}
return "";
}
catch
{
return "";
}
}
private (long UsedBytes, long TotalBytes) ParseMemInfo()
{
try
{
var lines = File.ReadAllLines("/proc/meminfo");
var dict = new Dictionary<string, long>();
foreach (var line in lines)
{
var parts = line.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2 && long.TryParse(Regex.Replace(parts[1], @"\D+", ""), out var value))
{
dict[parts[0]] = value * 1024; // Convert from KB to bytes
}
}
long total = dict.TryGetValue("MemTotal", out var t) ? t : 0;
long available = dict.TryGetValue("MemAvailable", out var a) ? a : 0;
long used = total - available;
return (used, total);
}
catch
{
return (0, 0);
}
}
private (long UsedBytes, long TotalBytes) ParseSwapInfo()
{
try
{
var lines = File.ReadAllLines("/proc/meminfo");
var dict = new Dictionary<string, long>();
foreach (var line in lines)
{
var parts = line.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2 && long.TryParse(Regex.Replace(parts[1], @"\D+", ""), out var value))
{
dict[parts[0]] = value * 1024; // Convert from KB to bytes
}
}
long total = dict.TryGetValue("SwapTotal", out var st) ? st : 0;
long free = dict.TryGetValue("SwapFree", out var sf) ? sf : 0;
long used = total - free;
return (used, total);
}
catch
{
return (0, 0);
}
}
private (long UsedBytes, long TotalBytes, string MountPoint) ParseDiskInfo()
{
try
{
// Use 'df' command to get root filesystem info
var psi = new ProcessStartInfo("df", "-B1 /")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null) return (0, 0, "/");
var output = process.StandardOutput.ReadToEnd();
var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
// Skip header, parse second line
if (lines.Length >= 2)
{
var parts = lines[1].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 4
&& long.TryParse(parts[1], out var total)
&& long.TryParse(parts[2], out var used))
{
return (used, total, parts[parts.Length - 1]);
}
}
return (0, 0, "/");
}
catch
{
return (0, 0, "/");
}
}
private double GetCpuUsage()
{
try
{
var firstLine = File.ReadLines("/proc/stat").First();
var parts = firstLine.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 5) return 0.0;
// /proc/stat format: cpu user nice system idle iowait irq softirq steal guest guest_nice
long user = long.Parse(parts[1]);
long nice = long.Parse(parts[2]);
long system = long.Parse(parts[3]);
long idle = long.Parse(parts[4]);
long totalJiffy = user + nice + system + idle;
long activeJiffy = user + nice + system;
// First call - initialize baseline
if (_prevJiffy == 0)
{
_prevJiffy = totalJiffy;
_prevIdle = idle;
return 0.0;
}
// Calculate deltas
long diffTotal = totalJiffy - _prevJiffy;
long diffIdle = idle - _prevIdle;
_prevJiffy = totalJiffy;
_prevIdle = idle;
if (diffTotal == 0) return 0.0;
double usage = 100.0 * (1.0 - ((double)diffIdle / diffTotal));
return Math.Max(0, Math.Min(100, usage));
}
catch
{
return 0.0;
}
}
}
public class WindowsMetricCollector : IMetricCollector
{
public SystemMetrics Collect()
{
// TODO: Use PerformanceCounters or WMI for Windows
return new SystemMetrics(
Environment.MachineName,
RuntimeInformation.OSDescription,
0.0,
0,
0,
0,
0,
0,
0,
"C:\\",
DateTime.UtcNow,
"",
"",
Environment.ProcessId
);
}
}
public class MacMetricCollector : IMetricCollector
{
public SystemMetrics Collect()
{
// TODO: Use sysctl for macOS metrics
return new SystemMetrics(
Environment.MachineName,
RuntimeInformation.OSDescription,
0.0,
0,
0,
0,
0,
0,
0,
"/",
DateTime.UtcNow,
"",
"",
Environment.ProcessId
);
}
}