using System.Net; using System.Net.NetworkInformation; using System.Net.Security; using System.Net.Sockets; using System.Security.Cryptography.X509Certificates; using System.Diagnostics; using Microsoft.EntityFrameworkCore; using EonaCat.LogStack.Status.Data; using EonaCat.LogStack.Status.Models; using Monitor = EonaCat.LogStack.Status.Models.Monitor; namespace EonaCat.LogStack.Status.Services; // This file is part of the EonaCat project(s) which is released under the Apache License. // See the LICENSE file or go to https://EonaCat.com/License for full license details. public class MonitoringService { private readonly IDbContextFactory _dbFactory; private readonly ILogger _log; public MonitoringService(IDbContextFactory dbFactory, ILogger log) { _dbFactory = dbFactory; _log = log; } public async Task CheckMonitorAsync(Monitor monitor) { var sw = Stopwatch.StartNew(); MonitorStatus status; string? message = null; try { (status, message) = monitor.Type switch { MonitorType.TCP => await CheckTcpAsync(monitor.Host, monitor.Port ?? 80, monitor.TimeoutMs), MonitorType.UDP => await CheckUdpAsync(monitor.Host, monitor.Port ?? 53, monitor.TimeoutMs), MonitorType.AppLocal => CheckLocalProcess(monitor.ProcessName ?? monitor.Name), MonitorType.AppRemote => await CheckTcpAsync(monitor.Host, monitor.Port ?? 80, monitor.TimeoutMs), MonitorType.HTTP => await CheckHttpAsync(monitor.Url ?? $"http://{monitor.Host}", monitor.TimeoutMs), MonitorType.HTTPS => await CheckHttpAsync(monitor.Url ?? $"https://{monitor.Host}", monitor.TimeoutMs), _ => (MonitorStatus.Unknown, "Unknown monitor type") }; } catch (Exception ex) { status = MonitorStatus.Down; message = ex.Message; } sw.Stop(); var check = new MonitorCheck { MonitorId = monitor.Id, Status = status, ResponseMs = sw.Elapsed.TotalMilliseconds, Message = message, CheckedAt = DateTime.UtcNow }; await using var db = await _dbFactory.CreateDbContextAsync(); db.MonitorChecks.Add(check); monitor.LastChecked = DateTime.UtcNow; monitor.LastStatus = status; monitor.LastResponseMs = check.ResponseMs; db.Monitors.Update(monitor); await db.SaveChangesAsync(); return check; } private async Task<(MonitorStatus, string?)> CheckTcpAsync(string host, int port, int timeoutMs) { using var client = new TcpClient(); var cts = new CancellationTokenSource(timeoutMs); try { await client.ConnectAsync(host, port, cts.Token); return (MonitorStatus.Up, $"Connected to {host}:{port}"); } catch (OperationCanceledException) { return (MonitorStatus.Down, $"Timeout connecting to {host}:{port}"); } catch (Exception ex) { return (MonitorStatus.Down, ex.Message); } } private async Task<(MonitorStatus, string?)> CheckUdpAsync(string host, int port, int timeoutMs) { try { using var udp = new UdpClient(); udp.Connect(host, port); var data = new byte[] { 0x00 }; await udp.SendAsync(data, data.Length); return (MonitorStatus.Up, $"UDP {host}:{port} reachable"); } catch (Exception ex) { return (MonitorStatus.Warning, $"UDP check: {ex.Message}"); } } private (MonitorStatus, string?) CheckLocalProcess(string processName) { var procs = Process.GetProcessesByName(processName); if (procs.Length > 0) { return (MonitorStatus.Up, $"Process '{processName}' running (PID: {procs[0].Id})"); } return (MonitorStatus.Down, $"Process '{processName}' not found"); } private async Task<(MonitorStatus, string?)> CheckHttpAsync(string url, int timeoutMs) { using var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromMilliseconds(timeoutMs) }; try { var resp = await client.GetAsync(url); var code = (int)resp.StatusCode; if (code >= 200 && code < 400) { return (MonitorStatus.Up, $"HTTP {code}"); } if (code >= 400 && code < 500) { return (MonitorStatus.Warning, $"HTTP {code}"); } return (MonitorStatus.Down, $"HTTP {code}"); } catch (TaskCanceledException) { return (MonitorStatus.Down, "Timeout"); } catch (Exception ex) { return (MonitorStatus.Down, ex.Message); } } public async Task CheckCertificateAsync(CertificateEntry cert) { try { using var client = new TcpClient(); await client.ConnectAsync(cert.Domain, cert.Port); using var ssl = new SslStream(client.GetStream(), false, (_, c, _, _) => true); await ssl.AuthenticateAsClientAsync(cert.Domain); var x509 = ssl.RemoteCertificate as X509Certificate2 ?? new X509Certificate2(ssl.RemoteCertificate!); cert.ExpiresAt = x509.NotAfter.ToUniversalTime(); cert.IssuedAt = x509.NotBefore.ToUniversalTime(); cert.Issuer = x509.Issuer; cert.Subject = x509.Subject; cert.Thumbprint = x509.Thumbprint; cert.LastError = null; } catch (Exception ex) { cert.LastError = ex.Message; } cert.LastChecked = DateTime.UtcNow; await using var db = await _dbFactory.CreateDbContextAsync(); db.Certificates.Update(cert); await db.SaveChangesAsync(); return cert; } public async Task GetStatsAsync(bool isAdmin) { await using var db = await _dbFactory.CreateDbContextAsync(); var monitors = await db.Monitors.Where(m => m.IsActive && (isAdmin || m.IsPublic)).ToListAsync(); var certs = await db.Certificates.ToListAsync(); var now = DateTime.UtcNow; return new DashboardStats { TotalMonitors = monitors.Count, UpCount = monitors.Count(m => m.LastStatus == MonitorStatus.Up), DownCount = monitors.Count(m => m.LastStatus == MonitorStatus.Down), WarnCount = monitors.Count(m => m.LastStatus == MonitorStatus.Warning || m.LastStatus == MonitorStatus.Degraded), UnknownCount = monitors.Count(m => m.LastStatus == MonitorStatus.Unknown), CertCount = certs.Count, CertExpiringSoon = certs.Count(c => c.ExpiresAt.HasValue && c.ExpiresAt.Value > now && (c.ExpiresAt.Value - now).TotalDays <= 30), CertExpired = certs.Count(c => c.ExpiresAt.HasValue && c.ExpiresAt.Value <= now), TotalLogs = await db.Logs.LongCountAsync(), ErrorLogs = await db.Logs.LongCountAsync(l => l.Level == "error" || l.Level == "critical"), OverallUptime = monitors.Count > 0 ? (double)monitors.Count(m => m.LastStatus == MonitorStatus.Up) / monitors.Count * 100 : 0 }; } } public class MonitoringBackgroundService : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _log; public MonitoringBackgroundService(IServiceScopeFactory scopeFactory, ILogger log) { _scopeFactory = scopeFactory; _log = log; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { using var scope = _scopeFactory.CreateScope(); var dbFactory = scope.ServiceProvider.GetRequiredService>(); var monitorSvc = scope.ServiceProvider.GetRequiredService(); await using var db = await dbFactory.CreateDbContextAsync(stoppingToken); var monitors = await db.Monitors.Where(m => m.IsActive).ToListAsync(stoppingToken); var now = DateTime.UtcNow; foreach (var m in monitors) { if (m.LastChecked == null || (now - m.LastChecked.Value).TotalSeconds >= m.IntervalSeconds) { _ = Task.Run(async () => { using var checkScope = _scopeFactory.CreateScope(); var svc = checkScope.ServiceProvider.GetRequiredService(); await svc.CheckMonitorAsync(m); }, stoppingToken); } } // Check certs every hour var certs = await db.Certificates.ToListAsync(stoppingToken); foreach (var c in certs) { if (c.LastChecked == null || (now - c.LastChecked.Value).TotalHours >= 1) { _ = Task.Run(async () => { using var certScope = _scopeFactory.CreateScope(); var svc = certScope.ServiceProvider.GetRequiredService(); await svc.CheckCertificateAsync(c); }, stoppingToken); } } } catch (Exception ex) { _log.LogError(ex, "Error in monitor loop"); } await Task.Delay(10000, stoppingToken); } } } public class IngestionService { private readonly IDbContextFactory _dbFactory; public IngestionService(IDbContextFactory dbFactory) { _dbFactory = dbFactory; } public async Task IngestAsync(LogEntry entry) { await using var db = await _dbFactory.CreateDbContextAsync(); db.Logs.Add(entry); await db.SaveChangesAsync(); } public async Task IngestBatchAsync(IEnumerable entries) { await using var db = await _dbFactory.CreateDbContextAsync(); db.Logs.AddRange(entries); await db.SaveChangesAsync(); } public async Task PurgeOldLogsAsync(int retentionDays) { await using var db = await _dbFactory.CreateDbContextAsync(); var cutoff = DateTime.UtcNow.AddDays(-retentionDays); var old = await db.Logs.Where(l => l.Timestamp < cutoff).ToListAsync(); db.Logs.RemoveRange(old); await db.SaveChangesAsync(); } }