mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
109 lines
3.7 KiB
TypeScript
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(),
|
|
};
|
|
}
|
|
}
|