This PR contains the latest updates from the generic Website template, which have been added while working on #loud-amsterdam. Reviewed-on: #1 Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
163 lines
5.0 KiB
Swift
163 lines
5.0 KiB
Swift
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<AppRequestContext>(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<Context: RequestContext> {
|
|
|
|
// 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<Context> {
|
|
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.
|
|
///
|
|
/// Every response is marked `noindex`: the checks answer `200 OK` to anyone, and `robots.txt` allows the whole site. A `Disallow` rule would
|
|
/// stop the crawl but not the indexing, and would publish the paths to everyone reading the file.
|
|
/// - 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",
|
|
.robotsTag: "noindex",
|
|
],
|
|
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"}"#
|
|
}
|
|
}
|