import Hummingbird import NIOCore import Persistence import Infrastructure /// Serves the website's health-check routes. /// /// The controller exposes its routes through its `RouterController` conformance, so the application that composes it registers them declaratively: /// /// ```swift /// router.addController { /// HealthController(probe: probe) /// } /// ``` /// /// It always serves a liveness check at `/health`; when a `Probe` is supplied it also serves a readiness check at `/health/ready` that reports /// whether the service's database is reachable. The two are kept distinct so an orchestrator can restart on liveness failure but only withhold traffic on /// readiness failure. /// /// - Note: `Context` is the request context the routes are resolved against, and must match the context of the router the routes are added to. public struct HealthController { // MARK: Properties /// The probe consulted for the readiness check, or `nil` when only liveness is served. private let probe: Probe? // MARK: Initializers /// Creates a health controller. /// - Parameter probe: the probe consulted by the readiness route; when `nil`, only the liveness route is served. public init( probe: Probe? = nil ) { self.probe = probe } } // MARK: - RouterController extension HealthController: RouterController { // MARK: Properties public var routes: RouteCollection { let routes = RouteCollection(context: Context.self) routes.get( .Health.check, use: check ) if probe != nil { routes.get( .Health.ready, use: ready ) } return routes } } // MARK: - Helpers private extension HealthController { // MARK: Methods /// Handles a request for the liveness check. /// /// Returns a constant JSON body built directly per request — the payload is a tiny literal with no rendering step, so there is nothing to pre-render or /// cache. It reports only that the process is up, with no dependency check, so an orchestrator restarts the process only when the process itself is /// unresponsive. /// - Parameters: /// - request: the incoming request. /// - context: the context the request is resolved against. /// - Returns: a `200 OK` response carrying the static JSON status payload. @Sendable func check( request: Request, context: some RequestContext ) -> Response { json( status: .ok, payload: .Payload.live ) } /// Handles a request for the readiness check. /// /// Consults the `Probe` supplied at initialization and reports `200 OK` when the service's database is reachable, or `503 Service Unavailable` /// otherwise, so a load balancer withholds traffic from an instance that cannot yet serve it without restarting the process. /// - Parameters: /// - request: the incoming request. /// - context: the context the request is resolved against. /// - Returns: a `200 OK` response when ready, or `503 Service Unavailable` when not. @Sendable func ready( request: Request, context: some RequestContext ) async -> Response { guard await probe?() == true else { return json( status: .serviceUnavailable, payload: .Payload.unavailable ) } return json( status: .ok, payload: .Payload.ready ) } /// Builds a JSON response carrying the given status and payload. /// - Parameters: /// - status: the HTTP status of the response. /// - payload: the JSON body of the response. /// - Returns: the configured JSON response. func json( status: HTTPResponse.Status, payload: String ) -> Response { Response( status: status, headers: [.contentType: "application/json"], body: .init(byteBuffer: .init(string: payload)) ) } } // MARK: - RouterPath+Constants private extension RouterPath { /// A namespace for the ``HealthController`` route paths. enum Health { /// The path of the liveness endpoint. static let check: RouterPath = "/health" /// The path of the readiness endpoint. static let ready: RouterPath = "/health/ready" } } // MARK: - String+Constants private extension String { enum Payload { static let live = #"{"status":"ok"}"# static let ready = #"{"status":"ready"}"# static let unavailable = #"{"status":"unavailable"}"# } }