d6efa9163b
- Implemented ITelegramNotificationService and TelegramNotificationService for sending alerts via Telegram. - Updated MonitoringSettings to include Telegram configuration options. - Enhanced AlertService to send alerts and resolutions through Telegram. - Added API endpoints for checking Telegram status and sending test alerts. - Updated README and TELEGRAM_SETUP.md with setup instructions and features. - Included example configuration in appsettings.telegram.example.json.
519 lines
22 KiB
C#
519 lines
22 KiB
C#
using Microsoft.AspNetCore.Builder;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Newtonsoft.Json;
|
|
using ResourceMonitorService.Configuration;
|
|
using ResourceMonitorService.Models;
|
|
using ResourceMonitorService.Services;
|
|
using System.Diagnostics;
|
|
|
|
namespace ResourceMonitorService
|
|
{
|
|
public class Worker : BackgroundService
|
|
{
|
|
private readonly ILogger<Worker> _logger;
|
|
private readonly IHostApplicationLifetime _lifetime;
|
|
private readonly ISystemInfoService _systemInfoService;
|
|
private readonly IResourceMonitorService _resourceMonitorService;
|
|
private readonly IGameDetectionService _gameDetectionService;
|
|
private readonly IAlertService _alertService;
|
|
private readonly ITelegramNotificationService _telegramService;
|
|
private readonly ApiSettings _apiSettings;
|
|
private readonly MonitoringSettings _monitoringSettings;
|
|
|
|
public Worker(
|
|
ILogger<Worker> logger,
|
|
IHostApplicationLifetime lifetime,
|
|
ISystemInfoService systemInfoService,
|
|
IResourceMonitorService resourceMonitorService,
|
|
IGameDetectionService gameDetectionService,
|
|
IAlertService alertService,
|
|
ITelegramNotificationService telegramService,
|
|
IOptions<ApiSettings> apiSettings,
|
|
IOptions<MonitoringSettings> monitoringSettings)
|
|
{
|
|
_logger = logger;
|
|
_lifetime = lifetime;
|
|
_systemInfoService = systemInfoService;
|
|
_resourceMonitorService = resourceMonitorService;
|
|
_gameDetectionService = gameDetectionService;
|
|
_alertService = alertService;
|
|
_telegramService = telegramService;
|
|
_apiSettings = apiSettings.Value;
|
|
_monitoringSettings = monitoringSettings.Value;
|
|
}
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
_logger.LogInformation("Resource Monitor Service starting...");
|
|
|
|
var builder = WebApplication.CreateBuilder();
|
|
|
|
// Configure CORS
|
|
builder.Services.AddCors(options =>
|
|
{
|
|
options.AddPolicy("AllowedOrigins", policy =>
|
|
policy.WithOrigins(_apiSettings.AllowedOrigins.ToArray())
|
|
.AllowAnyHeader()
|
|
.AllowAnyMethod());
|
|
});
|
|
|
|
builder.Services.AddControllers().AddNewtonsoftJson();
|
|
|
|
var app = builder.Build();
|
|
|
|
// API Key middleware (if enabled)
|
|
if (_apiSettings.RequireApiKey)
|
|
{
|
|
app.Use(async (context, next) =>
|
|
{
|
|
if (!context.Request.Headers.TryGetValue("X-API-KEY", out var extractedApiKey) ||
|
|
extractedApiKey != _apiSettings.ApiKey)
|
|
{
|
|
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
|
await context.Response.WriteAsync("Unauthorized: Invalid API Key");
|
|
return;
|
|
}
|
|
await next();
|
|
});
|
|
}
|
|
|
|
app.UseCors("AllowedOrigins");
|
|
|
|
// Enhanced API endpoints
|
|
ConfigureApiEndpoints(app);
|
|
|
|
// Start background monitoring
|
|
_ = Task.Run(async () => await BackgroundMonitoringLoop(stoppingToken), stoppingToken);
|
|
|
|
// Start the web application
|
|
_ = app.RunAsync(stoppingToken);
|
|
|
|
await Task.Delay(Timeout.Infinite, stoppingToken);
|
|
}
|
|
|
|
private void ConfigureApiEndpoints(WebApplication app)
|
|
{
|
|
var basePath = _apiSettings.BasePath;
|
|
|
|
// System information endpoints
|
|
app.MapGet($"{basePath}/system-info", async () =>
|
|
Results.Ok(await _systemInfoService.GetSystemInfoAsync()));
|
|
|
|
// Resource usage endpoints
|
|
app.MapGet($"{basePath}/resource-usage", async () =>
|
|
Results.Ok(await _resourceMonitorService.GetResourceUsageAsync()));
|
|
|
|
app.MapGet($"{basePath}/cpu-usage", async () =>
|
|
Results.Ok(await _resourceMonitorService.GetCpuUsageAsync()));
|
|
|
|
app.MapGet($"{basePath}/memory-usage", async () =>
|
|
Results.Ok(await _resourceMonitorService.GetMemoryUsageAsync()));
|
|
|
|
app.MapGet($"{basePath}/gpu-usage", async () =>
|
|
Results.Ok(await _resourceMonitorService.GetGpuUsageAsync()));
|
|
|
|
app.MapGet($"{basePath}/disk-usage", async () =>
|
|
Results.Ok(await _resourceMonitorService.GetDiskUsageAsync()));
|
|
|
|
app.MapGet($"{basePath}/network-usage", async () =>
|
|
Results.Ok(await _resourceMonitorService.GetNetworkUsageAsync()));
|
|
|
|
app.MapGet($"{basePath}/top-processes", async (int count = 10) =>
|
|
Results.Ok(await _resourceMonitorService.GetTopProcessesAsync(count)));
|
|
|
|
// Game detection endpoints
|
|
app.MapGet($"{basePath}/current-game", async () =>
|
|
Results.Ok(await _gameDetectionService.GetCurrentlyRunningGameAsync()));
|
|
|
|
app.MapGet($"{basePath}/all-games", async () =>
|
|
Results.Ok(await _gameDetectionService.GetAllDetectedGamesAsync()));
|
|
|
|
app.MapGet($"{basePath}/fullscreen-status", async () =>
|
|
Results.Ok(new { IsFullscreen = await _gameDetectionService.IsGameRunningFullscreenAsync() }));
|
|
|
|
// Alert endpoints
|
|
app.MapGet($"{basePath}/alerts/active", async () =>
|
|
Results.Ok(await _alertService.GetActiveAlertsAsync()));
|
|
|
|
app.MapGet($"{basePath}/alerts/history", async (int count = 100) =>
|
|
Results.Ok(await _alertService.GetAlertHistoryAsync(count)));
|
|
|
|
app.MapPost($"{basePath}/alerts/{{alertId}}/resolve", async (string alertId) =>
|
|
{
|
|
await _alertService.ResolveAlertAsync(alertId);
|
|
return Results.Ok(new { Message = "Alert resolved successfully" });
|
|
});
|
|
|
|
app.MapGet($"{basePath}/alerts/enabled", async () =>
|
|
Results.Ok(new { Enabled = await _alertService.IsAlertingEnabledAsync() }));
|
|
|
|
// Telegram endpoints
|
|
app.MapGet($"{basePath}/telegram/status", async () =>
|
|
Results.Ok(new {
|
|
Enabled = await _telegramService.IsEnabledAsync(),
|
|
Connected = await _telegramService.TestConnectionAsync()
|
|
}));
|
|
|
|
app.MapPost($"{basePath}/telegram/test", async () =>
|
|
{
|
|
try
|
|
{
|
|
var testAlert = new Alert
|
|
{
|
|
Timestamp = DateTime.Now,
|
|
Component = "Test",
|
|
Level = "Warning",
|
|
Message = "This is a test alert from Resource Monitor Service",
|
|
CurrentValue = 100,
|
|
ThresholdValue = 90,
|
|
IsResolved = false
|
|
};
|
|
|
|
await _telegramService.SendAlertAsync(testAlert);
|
|
return Results.Ok(new { Message = "Test alert sent successfully" });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return Results.Problem($"Failed to send test alert: {ex.Message}");
|
|
}
|
|
});
|
|
|
|
// Process management endpoints (enhanced)
|
|
app.MapPost($"{basePath}/process/kill", async (HttpContext context) =>
|
|
{
|
|
try
|
|
{
|
|
var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
|
|
var request = JsonConvert.DeserializeObject<dynamic>(body);
|
|
|
|
int processId = request?.ProcessId ?? 0;
|
|
bool force = request?.Force ?? false;
|
|
|
|
if (processId <= 0)
|
|
{
|
|
return Results.BadRequest("Invalid process ID");
|
|
}
|
|
|
|
var processes = Process.GetProcesses().Where(p => p.Id == processId).ToArray();
|
|
|
|
if (processes.Length == 0)
|
|
{
|
|
return Results.NotFound($"No process found with ID {processId}");
|
|
}
|
|
|
|
var process = processes[0];
|
|
var processName = process.ProcessName;
|
|
|
|
if (force)
|
|
{
|
|
process.Kill(true); // Force kill entire process tree
|
|
}
|
|
else
|
|
{
|
|
process.CloseMainWindow(); // Try graceful close first
|
|
|
|
// Wait a bit for graceful close
|
|
await Task.Delay(3000);
|
|
|
|
if (!process.HasExited)
|
|
{
|
|
process.Kill();
|
|
}
|
|
}
|
|
|
|
_logger.LogWarning("Process {ProcessName} (ID: {ProcessId}) was terminated", processName, processId);
|
|
return Results.Ok(new { Message = $"Process {processName} (ID: {processId}) has been terminated." });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error terminating process");
|
|
return Results.Problem(ex.Message);
|
|
}
|
|
});
|
|
|
|
// Enhanced shutdown/restart endpoints
|
|
app.MapPost($"{basePath}/system/shutdown", async (HttpContext context) =>
|
|
{
|
|
try
|
|
{
|
|
var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
|
|
var request = JsonConvert.DeserializeObject<dynamic>(body);
|
|
|
|
string action = request?.Action?.ToString()?.ToLower() ?? "shutdown";
|
|
int delaySeconds = request?.DelaySeconds ?? 0;
|
|
string message = request?.Message?.ToString() ?? "System shutdown initiated by Resource Monitor";
|
|
|
|
if (action != "shutdown" && action != "restart" && action != "cancel")
|
|
{
|
|
return Results.BadRequest("Invalid action. Use 'shutdown', 'restart', or 'cancel'.");
|
|
}
|
|
|
|
if (action == "cancel")
|
|
{
|
|
var cancelProcess = new ProcessStartInfo
|
|
{
|
|
FileName = "shutdown",
|
|
Arguments = "/a",
|
|
CreateNoWindow = true,
|
|
UseShellExecute = false
|
|
};
|
|
Process.Start(cancelProcess);
|
|
|
|
_logger.LogWarning("System shutdown cancelled");
|
|
return Results.Ok(new { Message = "Shutdown cancelled." });
|
|
}
|
|
|
|
if (delaySeconds < 0)
|
|
{
|
|
return Results.BadRequest("Delay must be a non-negative integer.");
|
|
}
|
|
|
|
string shutdownCommand = action == "shutdown"
|
|
? $"/s /f /t {delaySeconds} /c \"{message}\""
|
|
: $"/r /f /t {delaySeconds} /c \"{message}\"";
|
|
|
|
var processStartInfo = new ProcessStartInfo
|
|
{
|
|
FileName = "shutdown",
|
|
Arguments = shutdownCommand,
|
|
CreateNoWindow = true,
|
|
UseShellExecute = false
|
|
};
|
|
|
|
Process.Start(processStartInfo);
|
|
|
|
_logger.LogWarning("System {Action} initiated with {Delay} seconds delay", action, delaySeconds);
|
|
return Results.Ok(new {
|
|
Message = $"{action.ToUpper()} command executed with a delay of {delaySeconds} seconds.",
|
|
Action = action,
|
|
DelaySeconds = delaySeconds,
|
|
Timestamp = DateTime.Now
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error executing system command");
|
|
return Results.Problem(ex.Message);
|
|
}
|
|
});
|
|
|
|
// VM-specific endpoints for Unraid
|
|
app.MapGet($"{basePath}/vm/info", async () =>
|
|
{
|
|
var systemInfo = await _systemInfoService.GetSystemInfoAsync();
|
|
return Results.Ok(new
|
|
{
|
|
IsVirtualMachine = systemInfo.IsVirtualMachine,
|
|
HypervisorVendor = systemInfo.HypervisorVendor,
|
|
Uptime = systemInfo.Uptime,
|
|
BootTime = systemInfo.BootTime,
|
|
MachineName = systemInfo.MachineName,
|
|
Domain = systemInfo.Domain
|
|
});
|
|
});
|
|
|
|
// Performance history endpoint (simple in-memory storage)
|
|
app.MapGet($"{basePath}/performance/history", async (int minutes = 60) =>
|
|
{
|
|
// This would ideally be stored in a database or time-series database
|
|
// For now, return current snapshot with timestamp
|
|
var usage = await _resourceMonitorService.GetResourceUsageAsync();
|
|
return Results.Ok(new {
|
|
Current = usage,
|
|
Message = "Historical data not implemented yet - showing current values"
|
|
});
|
|
});
|
|
|
|
// Health check endpoint
|
|
app.MapGet($"{basePath}/health", async () =>
|
|
{
|
|
var systemInfo = await _systemInfoService.GetSystemInfoAsync();
|
|
var alerts = await _alertService.GetActiveAlertsAsync();
|
|
|
|
return Results.Ok(new
|
|
{
|
|
Status = "Healthy",
|
|
Timestamp = DateTime.Now,
|
|
Uptime = systemInfo.Uptime,
|
|
ActiveAlerts = alerts.Count,
|
|
MonitoringEnabled = new
|
|
{
|
|
GPU = _monitoringSettings.EnableGpuMonitoring,
|
|
Disk = _monitoringSettings.EnableDiskMonitoring,
|
|
Network = _monitoringSettings.EnableNetworkMonitoring,
|
|
Temperature = _monitoringSettings.EnableTemperatureMonitoring,
|
|
Processes = _monitoringSettings.EnableProcessMonitoring,
|
|
Games = _monitoringSettings.EnableGameDetection,
|
|
Alerts = _monitoringSettings.EnableAlerts
|
|
}
|
|
});
|
|
});
|
|
|
|
// Service control endpoints
|
|
app.MapPost($"{basePath}/service/stop", () =>
|
|
{
|
|
_logger.LogWarning("Service stop requested via API");
|
|
_lifetime.StopApplication();
|
|
return Results.Ok(new { Message = "Stopping the service..." });
|
|
});
|
|
|
|
// Configuration endpoint
|
|
app.MapGet($"{basePath}/config", () => Results.Ok(new
|
|
{
|
|
MonitoringSettings = new
|
|
{
|
|
UpdateInterval = _monitoringSettings.UpdateIntervalMs,
|
|
EnableGpuMonitoring = _monitoringSettings.EnableGpuMonitoring,
|
|
EnableDiskMonitoring = _monitoringSettings.EnableDiskMonitoring,
|
|
EnableNetworkMonitoring = _monitoringSettings.EnableNetworkMonitoring,
|
|
EnableTemperatureMonitoring = _monitoringSettings.EnableTemperatureMonitoring,
|
|
EnableProcessMonitoring = _monitoringSettings.EnableProcessMonitoring,
|
|
EnableGameDetection = _monitoringSettings.EnableGameDetection,
|
|
EnableAlerts = _monitoringSettings.EnableAlerts
|
|
},
|
|
ApiSettings = new
|
|
{
|
|
BasePath = _apiSettings.BasePath,
|
|
RequireApiKey = _apiSettings.RequireApiKey,
|
|
AllowedOrigins = _apiSettings.AllowedOrigins
|
|
}
|
|
}));
|
|
|
|
// Service metrics endpoint
|
|
app.MapGet($"{basePath}/metrics", async () =>
|
|
{
|
|
var usage = await _resourceMonitorService.GetResourceUsageAsync();
|
|
var systemInfo = await _systemInfoService.GetSystemInfoAsync();
|
|
return Results.Ok(new
|
|
{
|
|
Service = "Resource Monitor Service",
|
|
Version = "2.0.0",
|
|
Status = "Running",
|
|
Timestamp = DateTime.Now,
|
|
Uptime = systemInfo.Uptime,
|
|
LastUpdate = usage.Timestamp,
|
|
Performance = new
|
|
{
|
|
CPU = usage.CPU.Usage,
|
|
Memory = usage.Memory.UsagePercentage,
|
|
GPU = usage.GPU.Usage,
|
|
ActiveProcesses = usage.TopProcesses.Count
|
|
}
|
|
});
|
|
});
|
|
|
|
// Root endpoint
|
|
app.MapGet("/", () => Results.Ok(new
|
|
{
|
|
Service = "Resource Monitor Service for Unraid VM",
|
|
Version = "2.0.0",
|
|
Status = "Running",
|
|
Timestamp = DateTime.Now,
|
|
ApiBasePath = _apiSettings.BasePath,
|
|
Endpoints = new[]
|
|
{
|
|
$"{_apiSettings.BasePath}/health",
|
|
$"{_apiSettings.BasePath}/system-info",
|
|
$"{_apiSettings.BasePath}/resource-usage",
|
|
$"{_apiSettings.BasePath}/cpu-usage",
|
|
$"{_apiSettings.BasePath}/memory-usage",
|
|
$"{_apiSettings.BasePath}/gpu-usage",
|
|
$"{_apiSettings.BasePath}/disk-usage",
|
|
$"{_apiSettings.BasePath}/network-usage",
|
|
$"{_apiSettings.BasePath}/top-processes",
|
|
$"{_apiSettings.BasePath}/current-game",
|
|
$"{_apiSettings.BasePath}/alerts/active",
|
|
$"{_apiSettings.BasePath}/vm/info",
|
|
$"{_apiSettings.BasePath}/config",
|
|
$"{_apiSettings.BasePath}/metrics"
|
|
}
|
|
}));
|
|
|
|
_logger.LogInformation("API endpoints configured. Base path: {BasePath}", _apiSettings.BasePath);
|
|
}
|
|
|
|
private async Task BackgroundMonitoringLoop(CancellationToken cancellationToken)
|
|
{
|
|
_logger.LogInformation("Background monitoring started");
|
|
int errorCount = 0;
|
|
int successfulCycles = 0;
|
|
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
// Get current resource usage
|
|
var resourceUsage = await _resourceMonitorService.GetResourceUsageAsync();
|
|
|
|
// Add current game info if game detection is enabled
|
|
if (_monitoringSettings.EnableGameDetection)
|
|
{
|
|
try
|
|
{
|
|
resourceUsage.RunningGame = await _gameDetectionService.GetCurrentlyRunningGameAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Only log game detection errors occasionally to avoid spam
|
|
if (errorCount % 12 == 0) // Every minute if 5-second intervals
|
|
{
|
|
_logger.LogDebug("Game detection error (suppressed): {Message}", ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for alerts
|
|
if (_monitoringSettings.EnableAlerts)
|
|
{
|
|
await _alertService.CheckAndGenerateAlertsAsync(resourceUsage);
|
|
}
|
|
|
|
successfulCycles++;
|
|
|
|
// Log performance metrics occasionally
|
|
if (successfulCycles % 6 == 0) // Every 30 seconds with 5-second intervals
|
|
{
|
|
_logger.LogDebug("Performance: CPU: {CpuUsage:F1}%, Memory: {MemoryUsage:F1}%, GPU: {GpuUsage}%",
|
|
resourceUsage.CPU.Usage,
|
|
resourceUsage.Memory.UsagePercentage,
|
|
resourceUsage.GPU.Usage);
|
|
}
|
|
|
|
// Log successful monitoring occasionally for health verification
|
|
if (successfulCycles % 120 == 0) // Every 10 minutes
|
|
{
|
|
_logger.LogInformation("Background monitoring healthy - completed {SuccessfulCycles} cycles", successfulCycles);
|
|
}
|
|
|
|
errorCount = 0; // Reset error count on success
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errorCount++;
|
|
|
|
// Only log errors occasionally to avoid spam, but always log the first few
|
|
if (errorCount <= 3 || errorCount % 12 == 0)
|
|
{
|
|
_logger.LogError(ex, "Error in background monitoring loop (occurrence #{ErrorCount})", errorCount);
|
|
}
|
|
|
|
// If too many consecutive errors, increase delay
|
|
if (errorCount > 10)
|
|
{
|
|
await Task.Delay(_monitoringSettings.UpdateIntervalMs * 2, cancellationToken);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
await Task.Delay(_monitoringSettings.UpdateIntervalMs, cancellationToken);
|
|
}
|
|
|
|
_logger.LogInformation("Background monitoring stopped after {SuccessfulCycles} successful cycles", successfulCycles);
|
|
}
|
|
}
|
|
}
|