using Cleanuparr.Domain.Enums; using Cleanuparr.Infrastructure.Features.DownloadClient; using Cleanuparr.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Cleanuparr.Infrastructure.Health; /// /// Service for checking the health of download clients /// public class HealthCheckService : IHealthCheckService { private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; private readonly DownloadServiceFactory _downloadServiceFactory; private readonly Dictionary _healthStatuses = new(); private readonly object _lockObject = new(); /// /// Occurs when a client's health status changes /// public event EventHandler? ClientHealthChanged; public HealthCheckService( ILogger logger, IServiceProvider serviceProvider, DownloadServiceFactory downloadServiceFactory) { _logger = logger; _serviceProvider = serviceProvider; _downloadServiceFactory = downloadServiceFactory; } /// public async Task CheckClientHealthAsync(Guid clientId) { _logger.LogDebug("Checking health for client {clientId}", clientId); try { var dataContext = _serviceProvider.GetRequiredService(); // Get the client configuration var downloadClientConfig = await dataContext.DownloadClients .Where(x => x.Id == clientId) .FirstOrDefaultAsync(); if (downloadClientConfig is null) { _logger.LogWarning("Client {clientId} not found in configuration", clientId); var notFoundStatus = new HealthStatus { ClientId = clientId, IsHealthy = false, LastChecked = DateTime.UtcNow, ErrorMessage = "Client not found in configuration" }; UpdateHealthStatus(notFoundStatus); return notFoundStatus; } // Get the client instance var client = _downloadServiceFactory.GetDownloadService(downloadClientConfig); // Execute the health check var healthResult = await client.HealthCheckAsync(); // Create health status object var status = new HealthStatus { ClientId = clientId, ClientName = downloadClientConfig.Name, ClientTypeName = downloadClientConfig.TypeName, IsHealthy = healthResult.IsHealthy, LastChecked = DateTime.UtcNow, ErrorMessage = healthResult.ErrorMessage, ResponseTime = healthResult.ResponseTime }; UpdateHealthStatus(status); return status; } catch (Exception ex) { _logger.LogError(ex, "Error performing health check for client {clientId}", clientId); var status = new HealthStatus { ClientId = clientId, IsHealthy = false, LastChecked = DateTime.UtcNow, ErrorMessage = $"Error: {ex.Message}" }; UpdateHealthStatus(status); return status; } } /// public async Task> CheckAllClientsHealthAsync() { _logger.LogDebug("Checking health for all enabled clients"); try { var dataContext = _serviceProvider.GetRequiredService(); // Get all enabled client configurations var enabledClients = await dataContext.DownloadClients .Where(x => x.Enabled) .ToListAsync(); var results = new Dictionary(); // Check health of each enabled client foreach (var clientConfig in enabledClients) { var status = await CheckClientHealthAsync(clientConfig.Id); results[clientConfig.Id] = status; } return results; } catch (Exception ex) { _logger.LogError(ex, "Error checking health for all clients"); return new Dictionary(); } } /// public HealthStatus? GetClientHealth(Guid clientId) { lock (_lockObject) { return _healthStatuses.TryGetValue(clientId, out var status) ? status : null; } } /// public IDictionary GetAllClientHealth() { lock (_lockObject) { return new Dictionary(_healthStatuses); } } private void UpdateHealthStatus(HealthStatus newStatus) { HealthStatus? previousStatus; lock (_lockObject) { // Get previous status for comparison _healthStatuses.TryGetValue(newStatus.ClientId, out previousStatus); // Update status _healthStatuses[newStatus.ClientId] = newStatus; } // Determine if there's a significant change bool isStateChange = previousStatus == null || previousStatus.IsHealthy != newStatus.IsHealthy; // Raise event if there's a significant change if (isStateChange) { _logger.LogInformation( "Client {clientId} health changed: {status}", newStatus.ClientId, newStatus.IsHealthy ? "Healthy" : "Unhealthy"); OnClientHealthChanged(new ClientHealthChangedEventArgs( newStatus.ClientId, newStatus, previousStatus)); } } private void OnClientHealthChanged(ClientHealthChangedEventArgs e) { ClientHealthChanged?.Invoke(this, e); } }