| | | 1 | | using ArturRios.Heimdall.Domain.Enums; |
| | | 2 | | using ArturRios.Heimdall.Query.HealthChecks; |
| | | 3 | | using ArturRios.Heimdall.Query.Input; |
| | | 4 | | using ArturRios.Heimdall.Query.Output; |
| | | 5 | | using ArturRios.Heimdall.WebApi.Monitoring; |
| | | 6 | | using ArturRios.Heimdall.WebApi.Security; |
| | | 7 | | using ArturRios.Mediator.Query; |
| | | 8 | | using ArturRios.Output; |
| | | 9 | | using ArturRios.Util.Http; |
| | | 10 | | using ArturRios.Util.WebApi.AspNetCore; |
| | | 11 | | using ArturRios.Util.WebApi.Security.Attributes; |
| | | 12 | | using Microsoft.AspNetCore.Mvc; |
| | | 13 | | |
| | | 14 | | namespace ArturRios.Heimdall.WebApi.Controllers; |
| | | 15 | | |
| | | 16 | | /// <remarks> |
| | | 17 | | /// Under <c>api/</c> like every other route, because that prefix is all that carries a request on |
| | | 18 | | /// the web UI's host through to the API — see <c>OpenApiContractTests</c>. The container probe and |
| | | 19 | | /// yggdrasil's status catalog call the root <c>/healthcheck</c>, which the edge pipeline in |
| | | 20 | | /// <c>EdgePipeline</c> rewrites to this address. |
| | | 21 | | /// </remarks> |
| | | 22 | | [Route("api/healthcheck")] |
| | 26 | 23 | | public class HealthCheckController(QueryMediator queryMediator, DatabaseHealthCheck databaseHealthCheck, |
| | 26 | 24 | | ReadinessCache readinessCache) : Controller |
| | | 25 | | { |
| | | 26 | | /// <summary> |
| | | 27 | | /// Basic liveness check (UC-30, FR-HC-01): confirms the API process is up and responding. |
| | | 28 | | /// Public — no authentication required. |
| | | 29 | | /// </summary> |
| | | 30 | | [HttpGet] |
| | | 31 | | [Route("")] |
| | | 32 | | [AllowAnonymous] |
| | | 33 | | public ActionResult<DataOutput<string?>> HelloWorld() |
| | | 34 | | { |
| | 21 | 35 | | var result = DataOutput<string?>.New |
| | 21 | 36 | | .WithData("Hello world!") |
| | 21 | 37 | | .WithMessage("Heimdall API is working."); |
| | | 38 | | |
| | 21 | 39 | | return result.ToActionResult(HttpStatusCodes.Ok); |
| | | 40 | | } |
| | | 41 | | |
| | | 42 | | /// <summary> |
| | | 43 | | /// Readiness check (#215): <c>200 OK</c> when the database answers, <c>503 Service Unavailable</c> |
| | | 44 | | /// when it does not. Public — no authentication required. |
| | | 45 | | /// </summary> |
| | | 46 | | /// <remarks> |
| | | 47 | | /// Liveness above reads no database, so an instance that has lost it stays "healthy" and keeps |
| | | 48 | | /// receiving traffic it can only fail. This is the probe for that: an orchestrator or load |
| | | 49 | | /// balancer stops routing to the instance without restarting it. It asks |
| | | 50 | | /// <c>DatabaseHealthCheck</c> alone and answers with the bare status, never a service |
| | | 51 | | /// breakdown or a cause, which is why it can be anonymous where <c>detailed</c> cannot. The |
| | | 52 | | /// cause is in the log, where <c>DatabaseHealthCheck</c> writes it. |
| | | 53 | | /// The answer is held for five seconds (<c>ReadinessCache</c>), so an anonymous caller |
| | | 54 | | /// cannot make the probe a database read per request. |
| | | 55 | | /// </remarks> |
| | | 56 | | [HttpGet] |
| | | 57 | | [Route("ready")] |
| | | 58 | | [AllowAnonymous] |
| | | 59 | | [SuppressServerErrorLog] |
| | | 60 | | public async Task<ActionResult<DataOutput<string?>>> Ready() |
| | | 61 | | { |
| | 3 | 62 | | var healthy = await readinessCache.IsReadyAsync(databaseHealthCheck.IsHealthyAsync); |
| | | 63 | | |
| | 3 | 64 | | var result = DataOutput<string?>.New |
| | 3 | 65 | | .WithData(healthy ? HealthStatuses.Healthy : HealthStatuses.Unhealthy); |
| | | 66 | | |
| | 3 | 67 | | return result.ToActionResult(healthy ? HttpStatusCodes.Ok : HttpStatusCodes.ServiceUnavailable); |
| | 3 | 68 | | } |
| | | 69 | | |
| | | 70 | | /// <summary> |
| | | 71 | | /// Detailed health check (UC-30, FR-HC-02…07): reports the status of each verified service |
| | | 72 | | /// plus an aggregate general status. Restricted to System Admins (AF-30a → 403, AF-30b → 401). |
| | | 73 | | /// Returns <c>200 OK</c> when healthy and <c>503 Service Unavailable</c> when any verified |
| | | 74 | | /// service is down (FR-HC-07, AF-30c). |
| | | 75 | | /// </summary> |
| | | 76 | | [HttpGet] |
| | | 77 | | [Route("detailed")] |
| | | 78 | | [RoleRequirement((int)Roles.SystemAdmin)] |
| | | 79 | | public async Task<ActionResult<DataOutput<HealthCheckOutput?>>> Detailed() |
| | | 80 | | { |
| | 2 | 81 | | var result = await queryMediator |
| | 2 | 82 | | .ExecuteQueryAsync<DetailedHealthQuery, HealthCheckOutput>(new DetailedHealthQuery(), HttpContext.RequestAbo |
| | | 83 | | |
| | 2 | 84 | | var statusCode = result.Data?.Status == HealthStatuses.Healthy |
| | 2 | 85 | | ? HttpStatusCodes.Ok |
| | 2 | 86 | | : HttpStatusCodes.ServiceUnavailable; |
| | | 87 | | |
| | 2 | 88 | | return result.ToActionResult(statusCode); |
| | 2 | 89 | | } |
| | | 90 | | } |