303 lines
11 KiB
C#
303 lines
11 KiB
C#
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<DatabaseContext> _dbFactory;
|
|
private readonly ILogger<MonitoringService> _log;
|
|
|
|
public MonitoringService(IDbContextFactory<DatabaseContext> dbFactory, ILogger<MonitoringService> log)
|
|
{
|
|
_dbFactory = dbFactory;
|
|
_log = log;
|
|
}
|
|
|
|
public async Task<MonitorCheck> 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<CertificateEntry> 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<DashboardStats> 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<MonitoringBackgroundService> _log;
|
|
|
|
public MonitoringBackgroundService(IServiceScopeFactory scopeFactory, ILogger<MonitoringBackgroundService> 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<IDbContextFactory<DatabaseContext>>();
|
|
var monitorSvc = scope.ServiceProvider.GetRequiredService<MonitoringService>();
|
|
|
|
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<MonitoringService>();
|
|
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<MonitoringService>();
|
|
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<DatabaseContext> _dbFactory;
|
|
|
|
public IngestionService(IDbContextFactory<DatabaseContext> 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<LogEntry> 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();
|
|
}
|
|
} |