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
This commit is contained in:
286
JMonAgent/MetricCollector.cs
Normal file
286
JMonAgent/MetricCollector.cs
Normal file
@@ -0,0 +1,286 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user