using System.Diagnostics; using System.Reflection; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SameBeat.Api.Monitoring; using SameBeat.Infrastructure.BazaDanych; namespace SameBeat.Api.Controllers; /// /// Health/monitoring endpoint bez autoryzacji -- do uzytku przez zewnetrzny monitoring /// (uptime-kuma, healthcheck itp.). ETAP 1 zwracal tylko {status, wersja, czasUtc}; ETAP 8 dolozyl /// obok (bez zmiany istniejacych pol) stan bazy danych, liczbe otwartych polaczen SignalR i uptime /// procesu. /// /// UWAGA: monitoring LiveKit (jakosc WebRTC, obciazenie pokoi) jest CELOWO poza tym endpointem -- /// LiveKit ma wlasny system monitoringu po swojej stronie (dashboard/API LiveKit Server), integracja /// tego z /api/health byla poza zakresem MVP. /// /// UWAGA bezpieczenstwa: ten endpoint jest publiczny (AllowAnonymous) -- nigdy nie dodawaj tu /// connection stringow, szczegolow wyjatkow/stack trace ani innych danych wrazliwych. Bledy bazy /// sa logowane po stronie serwera (ILogger), a do klienta wraca tylko czytelny status. /// [ApiController] [Route("api/health")] public class HealthController : ControllerBase { private static readonly string Wersja = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "nieznana"; // Rzeczywisty czas startu procesu (nie chwila pierwszego zadania) -- niezalezny od tego, kiedy // ktos pierwszy raz odpyta /api/health. private static readonly DateTime CzasStartuProcesu = Process.GetCurrentProcess().StartTime.ToUniversalTime(); // Health check ma odpowiadac szybko nawet gdy Postgres wisi/nie odpowiada -- 3s to sowity zapas // na SELECT 1 na dzialajacej bazie, a jednoczesnie nie trzyma zewnetrznego monitoringu w nieskonczonosc. private static readonly TimeSpan TimeoutBazy = TimeSpan.FromSeconds(3); private readonly KontekstBazyDanych _kontekst; private readonly LicznikPolaczenSignalR _licznikPolaczen; private readonly ILogger _logger; public HealthController(KontekstBazyDanych kontekst, LicznikPolaczenSignalR licznikPolaczen, ILogger logger) { _kontekst = kontekst; _licznikPolaczen = licznikPolaczen; _logger = logger; } [HttpGet] [AllowAnonymous] public async Task SprawdzAsync(CancellationToken token) { var (bazaDostepna, czasOdpowiedziMs) = await SprawdzBazeAsync(token); var uptimeSekund = (DateTime.UtcNow - CzasStartuProcesu).TotalSeconds; return Ok(new { status = bazaDostepna ? "OK" : "DEGRADED", wersja = Wersja, czasUtc = DateTime.UtcNow, baza = new { dostepna = bazaDostepna, status = bazaDostepna ? "OK" : "niedostepna", czasOdpowiedziMs }, signalR = new { polaczeniaOtwarte = _licznikPolaczen.LiczbaPolaczenOgolem }, uptime = new { sekundy = Math.Round(uptimeSekund, 3), startUtc = CzasStartuProcesu } }); } /// /// SELECT 1 (Database.CanConnectAsync) z wlasnym timeoutem -- awaria/timeout bazy NIGDY nie /// wywala tego endpointu (brak throw na zewnatrz), zwraca po prostu "niedostepna". Szczegoly /// wyjatku (mogace zawierac np. fragmenty connection stringa) laduja WYLACZNIE w logu serwera. /// private async Task<(bool Dostepna, long CzasOdpowiedziMs)> SprawdzBazeAsync(CancellationToken token) { var stoper = Stopwatch.StartNew(); using var zrodloTimeoutu = CancellationTokenSource.CreateLinkedTokenSource(token); zrodloTimeoutu.CancelAfter(TimeoutBazy); try { var dostepna = await _kontekst.Database.CanConnectAsync(zrodloTimeoutu.Token); stoper.Stop(); return (dostepna, stoper.ElapsedMilliseconds); } catch (OperationCanceledException) when (!token.IsCancellationRequested) { // Przekroczony TimeoutBazy (nie anulowanie calego requestu przez klienta) -- baza nie // odpowiedziala na czas. stoper.Stop(); _logger.LogWarning("Health check bazy danych: przekroczony timeout {TimeoutBazy}.", TimeoutBazy); return (false, stoper.ElapsedMilliseconds); } catch (Exception wyjatek) { stoper.Stop(); _logger.LogWarning(wyjatek, "Health check bazy danych: baza niedostepna."); return (false, stoper.ElapsedMilliseconds); } } }