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:
30
JMonAgent/JMonAgent.csproj
Normal file
30
JMonAgent/JMonAgent.csproj
Normal file
@@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>dotnet-JMonAgent-7f8df008-660b-45a2-b649-64956a63f63c</UserSecretsId>
|
||||
<PublishAot>true</PublishAot>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<OptimizationPreference>Size</OptimizationPreference>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Google.Protobuf" Version="3.27.0" />
|
||||
<PackageReference Include="Grpc.Net.Client" Version="2.65.0" />
|
||||
<PackageReference Include="Grpc.Tools" Version="2.65.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="../Protos/metrics.proto" GrpcServices="Client" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(RuntimeIdentifier)' != '' AND $(RuntimeIdentifier.StartsWith('win'))">
|
||||
<PackageReference Include="System.Management" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
32
JMonAgent/MetricsQueue.cs
Normal file
32
JMonAgent/MetricsQueue.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace JMonAgent;
|
||||
|
||||
public class MetricsQueue
|
||||
{
|
||||
private readonly ConcurrentQueue<JMon.Protos.DeltaMetrics> _queue = new();
|
||||
private readonly ILogger<MetricsQueue> _logger;
|
||||
|
||||
public MetricsQueue(ILogger<MetricsQueue> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Enqueue(JMon.Protos.DeltaMetrics metrics)
|
||||
{
|
||||
_queue.Enqueue(metrics);
|
||||
_logger.LogWarning("Metrics queued (total: {count})", _queue.Count);
|
||||
}
|
||||
|
||||
public List<JMon.Protos.DeltaMetrics> DequeueAll()
|
||||
{
|
||||
var items = new List<JMon.Protos.DeltaMetrics>();
|
||||
while (_queue.TryDequeue(out var item))
|
||||
{
|
||||
items.Add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public int Count => _queue.Count;
|
||||
}
|
||||
8
JMonAgent/Program.cs
Normal file
8
JMonAgent/Program.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using JMonAgent;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
builder.Services.AddSingleton<MetricsQueue>();
|
||||
builder.Services.AddHostedService<Worker>();
|
||||
|
||||
var host = builder.Build();
|
||||
host.Run();
|
||||
12
JMonAgent/Properties/launchSettings.json
Normal file
12
JMonAgent/Properties/launchSettings.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"JMonAgent": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
JMonAgent/SystemMetrics.cs
Normal file
44
JMonAgent/SystemMetrics.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace JMonAgent;
|
||||
|
||||
public record SystemMetrics(
|
||||
string MachineName,
|
||||
string OSDescription,
|
||||
double CpuUsagePercentage,
|
||||
long MemoryUsedBytes,
|
||||
long MemoryTotalBytes,
|
||||
long SwapUsedBytes,
|
||||
long SwapTotalBytes,
|
||||
long DiskUsedBytes,
|
||||
long DiskTotalBytes,
|
||||
string DiskMountPoint,
|
||||
DateTime Timestamp,
|
||||
string IpAddress,
|
||||
string HwId,
|
||||
int ProcessId
|
||||
);
|
||||
|
||||
public record DeltaMetrics(
|
||||
string MachineName,
|
||||
string? OSDescription,
|
||||
double? CpuUsagePercentage,
|
||||
long? MemoryUsedBytes,
|
||||
long? MemoryTotalBytes,
|
||||
long? SwapUsedBytes,
|
||||
long? SwapTotalBytes,
|
||||
long? DiskUsedBytes,
|
||||
long? DiskTotalBytes,
|
||||
string? DiskMountPoint,
|
||||
DateTime Timestamp,
|
||||
string? IpAddress,
|
||||
string? HwId,
|
||||
int ProcessId
|
||||
);
|
||||
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
[JsonSerializable(typeof(SystemMetrics))]
|
||||
[JsonSerializable(typeof(DeltaMetrics))]
|
||||
internal partial class MetricsJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
308
JMonAgent/Worker.cs
Normal file
308
JMonAgent/Worker.cs
Normal file
@@ -0,0 +1,308 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Grpc.Net.Client;
|
||||
using JMon.Protos;
|
||||
|
||||
namespace JMonAgent;
|
||||
|
||||
public class Worker : BackgroundService
|
||||
{
|
||||
private readonly ILogger<Worker> _logger;
|
||||
private readonly IMetricCollector _collector;
|
||||
private readonly MetricsQueue _queue;
|
||||
private readonly bool _debugMode;
|
||||
private GrpcChannel? _channel;
|
||||
private MetricsService.MetricsServiceClient? _client;
|
||||
private SystemMetrics? _previousMetrics;
|
||||
private readonly SemaphoreSlim _executionLock = new(1, 1);
|
||||
|
||||
private const double CpuThreshold = 0.5; // Only send if change > 0.5%
|
||||
private const long MemoryThreshold = 10 * 1024 * 1024; // 10 MB
|
||||
private const long SwapThreshold = 10 * 1024 * 1024; // 10 MB
|
||||
private const long DiskThreshold = 100 * 1024 * 1024; // 100 MB
|
||||
|
||||
public Worker(ILogger<Worker> logger, MetricsQueue queue, IConfiguration config)
|
||||
{
|
||||
_logger = logger;
|
||||
_queue = queue;
|
||||
_debugMode = config.GetValue<bool>("DEBUG", false);
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
_collector = new LinuxMetricCollector();
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
_collector = new WindowsMetricCollector();
|
||||
else
|
||||
_collector = new MacMetricCollector();
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Agent started on {platform}", RuntimeInformation.OSDescription);
|
||||
|
||||
if (_debugMode)
|
||||
{
|
||||
_logger.LogInformation("DEBUG MODE ENABLED - Metrics will be printed, not sent");
|
||||
await RunDebugMode(stoppingToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await RunNormalMode(stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunDebugMode(CancellationToken stoppingToken)
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
|
||||
|
||||
try
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken))
|
||||
{
|
||||
// Skip if previous execution is still running (non-blocking lock)
|
||||
if (!_executionLock.Wait(0))
|
||||
{
|
||||
_logger.LogDebug("Skipping tick: previous execution still running");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var metrics = _collector.Collect();
|
||||
var delta = CalculateDelta(metrics);
|
||||
|
||||
Console.WriteLine("\n=== METRICS DEBUG OUTPUT ===");
|
||||
Console.WriteLine($"Machine: {delta.MachineName}");
|
||||
if (delta.OSDescription != null)
|
||||
Console.WriteLine($"OS: {delta.OSDescription}");
|
||||
if (delta.IpAddress != null)
|
||||
Console.WriteLine($"IP: {delta.IpAddress}");
|
||||
if (delta.HwId != null)
|
||||
Console.WriteLine($"HWID: {delta.HwId}");
|
||||
if (delta.CpuUsagePercentage.HasValue)
|
||||
Console.WriteLine($"CPU: {delta.CpuUsagePercentage:F2}%");
|
||||
if (delta.MemoryUsedBytes.HasValue)
|
||||
Console.WriteLine($"Memory: {delta.MemoryUsedBytes.Value / (1024 * 1024)} MB / {delta.MemoryTotalBytes.GetValueOrDefault() / (1024 * 1024)} MB");
|
||||
if (delta.SwapUsedBytes.HasValue)
|
||||
Console.WriteLine($"Swap: {delta.SwapUsedBytes.Value / (1024 * 1024)} MB / {delta.SwapTotalBytes.GetValueOrDefault() / (1024 * 1024)} MB");
|
||||
if (delta.DiskUsedBytes.HasValue)
|
||||
Console.WriteLine($"Disk: {delta.DiskUsedBytes.Value / (1024 * 1024)} MB / {delta.DiskTotalBytes.GetValueOrDefault() / (1024 * 1024)} MB ({delta.DiskMountPoint})");
|
||||
Console.WriteLine($"Timestamp: {delta.Timestamp:O}");
|
||||
Console.WriteLine("===========================\n");
|
||||
|
||||
_previousMetrics = metrics;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Error in debug mode: {msg}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_executionLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Debug mode cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunNormalMode(CancellationToken stoppingToken)
|
||||
{
|
||||
// Create gRPC channel (ignore self-signed certs for development)
|
||||
var handler = new HttpClientHandler();
|
||||
handler.ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true;
|
||||
|
||||
_channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
|
||||
{
|
||||
HttpHandler = handler
|
||||
});
|
||||
|
||||
_client = new MetricsService.MetricsServiceClient(_channel);
|
||||
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
|
||||
|
||||
try
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken))
|
||||
{
|
||||
// Skip if previous execution is still running (non-blocking lock)
|
||||
if (!_executionLock.Wait(0))
|
||||
{
|
||||
_logger.LogWarning("Skipping tick: previous execution still running (>5s)");
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Try to flush queued metrics first
|
||||
if (_queue.Count > 0)
|
||||
{
|
||||
var queuedMetrics = _queue.DequeueAll();
|
||||
_logger.LogInformation("Flushing {count} queued metrics", queuedMetrics.Count);
|
||||
|
||||
foreach (var queued in queuedMetrics)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _client.ReportMetricsAsync(queued, cancellationToken: stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Failed to flush queued metric: {msg}", ex.Message);
|
||||
_queue.Enqueue(queued); // Re-queue on failure
|
||||
// Continue trying other queued metrics instead of breaking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var metrics = _collector.Collect();
|
||||
_logger.LogDebug("Collected metrics: CPU={cpu}%, Memory={mem}MB/{total}MB",
|
||||
metrics.CpuUsagePercentage.ToString("F2"),
|
||||
(metrics.MemoryUsedBytes / (1024 * 1024)),
|
||||
(metrics.MemoryTotalBytes / (1024 * 1024)));
|
||||
|
||||
var delta = CalculateDelta(metrics);
|
||||
|
||||
// Convert to gRPC message using object initializer
|
||||
var grpcDelta = new JMon.Protos.DeltaMetrics
|
||||
{
|
||||
MachineName = delta.MachineName,
|
||||
Hwid = delta.HwId,
|
||||
TimestampUnixMs = new DateTimeOffset(delta.Timestamp).ToUnixTimeMilliseconds(),
|
||||
ProcessId = delta.ProcessId
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(delta.OSDescription))
|
||||
grpcDelta.OsDescription = delta.OSDescription;
|
||||
if (delta.CpuUsagePercentage.HasValue)
|
||||
grpcDelta.CpuUsagePercentage = delta.CpuUsagePercentage.Value;
|
||||
if (delta.MemoryUsedBytes.HasValue)
|
||||
grpcDelta.MemoryUsedBytes = delta.MemoryUsedBytes.Value;
|
||||
if (delta.MemoryTotalBytes.HasValue)
|
||||
grpcDelta.MemoryTotalBytes = delta.MemoryTotalBytes.Value;
|
||||
if (delta.SwapUsedBytes.HasValue)
|
||||
grpcDelta.SwapUsedBytes = delta.SwapUsedBytes.Value;
|
||||
if (delta.SwapTotalBytes.HasValue)
|
||||
grpcDelta.SwapTotalBytes = delta.SwapTotalBytes.Value;
|
||||
if (delta.DiskUsedBytes.HasValue)
|
||||
grpcDelta.DiskUsedBytes = delta.DiskUsedBytes.Value;
|
||||
if (delta.DiskTotalBytes.HasValue)
|
||||
grpcDelta.DiskTotalBytes = delta.DiskTotalBytes.Value;
|
||||
if (!string.IsNullOrEmpty(delta.DiskMountPoint))
|
||||
grpcDelta.DiskMountPoint = delta.DiskMountPoint;
|
||||
if (!string.IsNullOrEmpty(delta.IpAddress))
|
||||
grpcDelta.IpAddress = delta.IpAddress;
|
||||
|
||||
var response = await _client.ReportMetricsAsync(grpcDelta, cancellationToken: stoppingToken);
|
||||
|
||||
_logger.LogInformation("Metrics reported: {response}", response.Command);
|
||||
|
||||
// Handle server commands
|
||||
if (response.Command == MetricsResponse.Types.CommandType.Sync)
|
||||
{
|
||||
_logger.LogInformation("Server requested full resync: {msg}", response.Message);
|
||||
_previousMetrics = null; // Reset to send full update next time
|
||||
}
|
||||
else if (response.Command == MetricsResponse.Types.CommandType.Sigterm)
|
||||
{
|
||||
_logger.LogInformation("Server terminating process: {msg}", response.Message);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
_previousMetrics = metrics;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError("Error reporting metrics: {msg}", ex.Message);
|
||||
|
||||
// On failure, queue the FULL metrics for retry (not delta)
|
||||
// This ensures the server can reconstruct properly even if it was restarted
|
||||
if (ex is not OperationCanceledException)
|
||||
{
|
||||
var metrics = _collector.Collect();
|
||||
|
||||
var grpcDelta = new JMon.Protos.DeltaMetrics
|
||||
{
|
||||
MachineName = metrics.MachineName,
|
||||
Hwid = metrics.HwId,
|
||||
TimestampUnixMs = new DateTimeOffset(metrics.Timestamp).ToUnixTimeMilliseconds(),
|
||||
Retried = true,
|
||||
ProcessId = metrics.ProcessId,
|
||||
// Send FULL metrics (all fields) on retry
|
||||
OsDescription = metrics.OSDescription,
|
||||
CpuUsagePercentage = metrics.CpuUsagePercentage,
|
||||
MemoryUsedBytes = metrics.MemoryUsedBytes,
|
||||
MemoryTotalBytes = metrics.MemoryTotalBytes,
|
||||
SwapUsedBytes = metrics.SwapUsedBytes,
|
||||
SwapTotalBytes = metrics.SwapTotalBytes,
|
||||
DiskUsedBytes = metrics.DiskUsedBytes,
|
||||
DiskTotalBytes = metrics.DiskTotalBytes,
|
||||
DiskMountPoint = metrics.DiskMountPoint,
|
||||
IpAddress = metrics.IpAddress
|
||||
};
|
||||
|
||||
_queue.Enqueue(grpcDelta);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_executionLock.Release();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Normal mode cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
private DeltaMetrics CalculateDelta(SystemMetrics current)
|
||||
{
|
||||
// First time - send everything
|
||||
if (_previousMetrics == null)
|
||||
{
|
||||
return new DeltaMetrics(
|
||||
current.MachineName,
|
||||
current.OSDescription,
|
||||
current.CpuUsagePercentage,
|
||||
current.MemoryUsedBytes,
|
||||
current.MemoryTotalBytes,
|
||||
current.SwapUsedBytes,
|
||||
current.SwapTotalBytes,
|
||||
current.DiskUsedBytes,
|
||||
current.DiskTotalBytes,
|
||||
current.DiskMountPoint,
|
||||
current.Timestamp,
|
||||
current.IpAddress,
|
||||
current.HwId
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate deltas and only include changed values
|
||||
return new DeltaMetrics(
|
||||
current.MachineName,
|
||||
current.OSDescription != _previousMetrics.OSDescription ? current.OSDescription : null,
|
||||
Math.Abs(current.CpuUsagePercentage - _previousMetrics.CpuUsagePercentage) > CpuThreshold ? current.CpuUsagePercentage : null,
|
||||
Math.Abs(current.MemoryUsedBytes - _previousMetrics.MemoryUsedBytes) > MemoryThreshold ? current.MemoryUsedBytes : null,
|
||||
current.MemoryTotalBytes != _previousMetrics.MemoryTotalBytes ? current.MemoryTotalBytes : null,
|
||||
Math.Abs(current.SwapUsedBytes - _previousMetrics.SwapUsedBytes) > SwapThreshold ? current.SwapUsedBytes : null,
|
||||
current.SwapTotalBytes != _previousMetrics.SwapTotalBytes ? current.SwapTotalBytes : null,
|
||||
Math.Abs(current.DiskUsedBytes - _previousMetrics.DiskUsedBytes) > DiskThreshold ? current.DiskUsedBytes : null,
|
||||
current.DiskTotalBytes != _previousMetrics.DiskTotalBytes ? current.DiskTotalBytes : null,
|
||||
current.DiskMountPoint != _previousMetrics.DiskMountPoint ? current.DiskMountPoint : null,
|
||||
current.Timestamp,
|
||||
current.IpAddress != _previousMetrics.IpAddress ? current.IpAddress : null,
|
||||
current.HwId // Always send HWID as it's the unique identifier
|
||||
);
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_channel != null)
|
||||
{
|
||||
await _channel.ShutdownAsync();
|
||||
_channel.Dispose();
|
||||
}
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
8
JMonAgent/appsettings.Development.json
Normal file
8
JMonAgent/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
JMonAgent/appsettings.json
Normal file
10
JMonAgent/appsettings.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Debug",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"Microsoft.AspNetCore.SignalR": "Debug",
|
||||
"Microsoft.AspNetCore.Http.Connections": "Debug"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user