- 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
287 lines
8.0 KiB
C#
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
|
|
);
|
|
}
|
|
}
|
|
|