Files
edr-platform/apps/edr-freight-api/src/modules/health/health.controller.ts

109 lines
3.7 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 { 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,
) {}
@Get()
@Public()
@ApiOperation({ summary: "Liveness probe" })
liveness() {
return { status: "ok", timestamp: new Date().toISOString() };
}
@Get("ready")
@Public()
@ApiOperation({
summary:
"Readiness probe — database plus SMS/email broker connectivity. Broker failures report as degraded unless READINESS_REQUIRES_BROKER=true.",
})
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 brokerDown =
broker.sms.status === "error" || broker.email.status === "error";
const failed =
database.status === "error" ||
(READINESS_REQUIRES_BROKER && brokerDown);
const status = failed ? "error" : brokerDown ? "degraded" : "ok";
return res
.status(failed ? HttpStatus.SERVICE_UNAVAILABLE : HttpStatus.OK)
.json({
status,
timestamp: new Date().toISOString(),
checks: { database, broker },
});
}
@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(),
};
}
}