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);
}
}
}