Files
edr-platform/apps/edr-freight-api/src/modules/health/health.controller.ts
Nathnael 0ac85ebc1f fix
2026-08-31 11:45:55 +00:00

143 lines
4.9 KiB
TypeScript

// health.controller.ts
import { Controller, Get, HttpStatus, Res } from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { InjectDataSource } from "@nestjs/typeorm";
import { Public } from "@edr/api-common";
import { Response } from "express";
import { DataSource } from "typeorm";
import { MatrixClient } from "../chat/matrix.client";
import { EmailClientService } from "../notifications/email-client.service";
import { SmsClientService } from "../notifications/sms-client.service";
type CheckStatus = "ok" | "error" | "unknown";
/**
* Readiness normally stays green when only the broker is down.
*
* A 503 pulls the pod out of the load balancer, which would take booking,
* tracking and billing offline because SMS is unreachable — a strictly worse
* outcome than degraded notifications. The broker check is therefore reported,
* not enforced, and `READINESS_REQUIRES_BROKER=true` opts into hard-failing for
* deployments where a silent OTP black hole is the greater risk.
*/
const READINESS_REQUIRES_BROKER =
process.env.READINESS_REQUIRES_BROKER === "true";
@ApiTags("Health")
@Controller("health")
export class HealthController {
constructor(
@InjectDataSource()
private readonly dataSource: DataSource,
private readonly smsClient: SmsClientService,
private readonly emailClient: EmailClientService,
private readonly matrix: MatrixClient,
) {}
@Get()
@Public()
@ApiOperation({ summary: "Liveness probe" })
liveness() {
return { status: "ok", timestamp: new Date().toISOString() };
}
@Get("ready")
@Public()
@ApiOperation({
summary:
"Readiness probe — database, SMS/email broker connectivity, and the Matrix admin token. Broker failures report as degraded unless READINESS_REQUIRES_BROKER=true; chat failures always report as degraded.",
})
async readiness(@Res() res: Response) {
const startedAt = Date.now();
let database: { status: CheckStatus; latencyMs: number; error?: string };
try {
await this.dataSource.query("SELECT 1");
database = { status: "ok", latencyMs: Date.now() - startedAt };
} catch (error) {
database = {
status: "error",
latencyMs: Date.now() - startedAt,
error: error instanceof Error ? error.message : "Unknown error",
};
}
// `null` from the client means the connection manager was not reachable
// through Nest's internals — surfaced as "unknown" so a shape change in
// @nestjs/microservices degrades to honest ignorance, not a false "ok".
const toStatus = (connected: boolean | null): CheckStatus =>
connected === null ? "unknown" : connected ? "ok" : "error";
const broker = {
sms: { status: toStatus(this.smsClient.brokerConnected) },
email: { status: toStatus(this.emailClient.brokerConnected) },
// Every OTP, and every booking/billing notification, publishes through
// these. `error` here means codes are being generated and silently dropped.
enabled: process.env.RABBITMQ_ENABLED !== "false",
};
const chat = await this.chatCheck();
const brokerDown =
broker.sms.status === "error" || broker.email.status === "error";
const failed =
database.status === "error" ||
(READINESS_REQUIRES_BROKER && brokerDown);
const status = failed
? "error"
: brokerDown || chat.status === "error"
? "degraded"
: "ok";
return res
.status(failed ? HttpStatus.SERVICE_UNAVAILABLE : HttpStatus.OK)
.json({
status,
timestamp: new Date().toISOString(),
checks: { database, broker, chat },
});
}
/**
* Chat provisioning runs entirely on MATRIX_ADMIN_TOKEN, and a token that is
* valid but not *server admin* fails only the `/_synapse/admin` half: rooms
* are never created, joins never happen, and the sole symptom is an empty
* Element for every employee. Nothing else in the probe would catch that.
*
* Degraded, never a 503 — chat is not worth pulling the pod out of the load
* balancer for, by the same reasoning as the broker check above. `unknown`
* when MATRIX_ENABLED is off: a feature that is switched off is not a fault.
*/
private async chatCheck(): Promise<{
status: CheckStatus;
enabled: boolean;
actingAs?: string;
error?: string;
}> {
if (!this.matrix.enabled) return { status: "unknown", enabled: false };
const check = await this.matrix.adminCheck();
return {
status: check.ok ? "ok" : "error",
enabled: true,
actingAs: check.actingAs,
error: check.error,
};
}
@Get("info")
@Public()
@ApiOperation({ summary: "App info — version, environment, uptime" })
info() {
return {
name: "edr-freight-api",
version: process.env.npm_package_version ?? "1.0.0",
environment: process.env.NODE_ENV ?? "development",
uptimeSeconds: Math.floor(process.uptime()),
timestamp: new Date().toISOString(),
};
}
}