|
1 | | -import { cpuUsage } from "process"; |
2 | | -import { cpus, freemem, totalmem, hostname, loadavg } from "os"; |
3 | 1 | import { querySql as sql } from "../utils/db"; |
| 2 | +import { checkRedisConnectivity } from "../utils/cache"; |
4 | 3 |
|
5 | | -const CPU_HISTORY_HOURS = 24; |
6 | | -const cpuHistory: Array<{ timestamp: string; usage: string }> = []; |
| 4 | +const DEPENDENCY_CHECK_INTERVAL_MS = 5 * 60 * 1000; |
| 5 | +const DB_CHECK_TIMEOUT_MS = 1500; |
7 | 6 |
|
8 | | -function formatBytes(bytes: number) { |
9 | | - const gb = bytes / (1024 * 1024 * 1024); |
10 | | - return `${gb.toFixed(2)} GB`; |
11 | | -} |
| 7 | +type DependencyResult = { |
| 8 | + status: "OK" | "ERROR" | "DISABLED" | "UNKNOWN"; |
| 9 | + latencyMs?: number; |
| 10 | + checkedAt: string; |
| 11 | +}; |
12 | 12 |
|
13 | | -function formatUptime(seconds: number) { |
14 | | - const days = Math.floor(seconds / (24 * 60 * 60)); |
15 | | - const hours = Math.floor((seconds % (24 * 60 * 60)) / (60 * 60)); |
16 | | - const minutes = Math.floor((seconds % (60 * 60)) / 60); |
17 | | - return `${days}d ${hours}h ${minutes}m`; |
18 | | -} |
| 13 | +export type DependencyHealth = { |
| 14 | + db: DependencyResult; |
| 15 | + redis: DependencyResult; |
| 16 | + checkedAt: string; |
| 17 | +}; |
19 | 18 |
|
20 | | -function updateCpuHistory() { |
21 | | - const startUsage = cpuUsage(); |
| 19 | +const neverChecked = new Date(0).toISOString(); |
| 20 | +let dependencyHealth: DependencyHealth = { |
| 21 | + db: { status: "UNKNOWN", checkedAt: neverChecked }, |
| 22 | + redis: { status: process.env.REDIS_URL ? "UNKNOWN" : "DISABLED", checkedAt: neverChecked }, |
| 23 | + checkedAt: neverChecked, |
| 24 | +}; |
| 25 | +let dependencyCheck: Promise<void> | undefined; |
| 26 | +let monitoringStarted = false; |
22 | 27 |
|
23 | | - setTimeout(() => { |
24 | | - const endUsage = cpuUsage(startUsage); |
25 | | - const totalUsage = endUsage.user + endUsage.system; |
26 | | - const usagePercent = (totalUsage / 1000000 / 3600).toFixed(1); |
| 28 | +const checkDependencies = () => { |
| 29 | + if (dependencyCheck) return dependencyCheck; |
27 | 30 |
|
28 | | - cpuHistory.push({ |
29 | | - timestamp: new Date().toISOString(), |
30 | | - usage: `${usagePercent}%`, |
31 | | - }); |
| 31 | + dependencyCheck = (async () => { |
| 32 | + const startedAt = Date.now(); |
| 33 | + const dbCheck = Promise.race([ |
| 34 | + Promise.resolve(sql`SELECT 1`).then(() => ({ status: "OK" as const, latencyMs: Date.now() - startedAt })), |
| 35 | + new Promise<{ status: "ERROR"; latencyMs: number }>((resolve) => |
| 36 | + setTimeout(() => resolve({ status: "ERROR", latencyMs: Date.now() - startedAt }), DB_CHECK_TIMEOUT_MS) |
| 37 | + ), |
| 38 | + ]).catch(() => ({ status: "ERROR" as const, latencyMs: Date.now() - startedAt })); |
32 | 39 |
|
33 | | - if (cpuHistory.length > CPU_HISTORY_HOURS) { |
34 | | - cpuHistory.shift(); |
35 | | - } |
36 | | - }, 1000); |
37 | | -} |
| 40 | + const [db, redis] = await Promise.all([dbCheck, checkRedisConnectivity()]); |
| 41 | + const checkedAt = new Date().toISOString(); |
| 42 | + dependencyHealth = { |
| 43 | + db: { ...db, checkedAt }, |
| 44 | + redis: { ...redis, checkedAt }, |
| 45 | + checkedAt, |
| 46 | + }; |
| 47 | + })().finally(() => { |
| 48 | + dependencyCheck = undefined; |
| 49 | + }); |
| 50 | + |
| 51 | + return dependencyCheck; |
| 52 | +}; |
38 | 53 |
|
39 | 54 | export function startHealthMonitoring() { |
40 | | - setInterval(updateCpuHistory, 3600000); |
41 | | - updateCpuHistory(); |
| 55 | + if (monitoringStarted) return; |
| 56 | + monitoringStarted = true; |
| 57 | + void checkDependencies(); |
| 58 | + const interval = setInterval(() => void checkDependencies(), DEPENDENCY_CHECK_INTERVAL_MS); |
| 59 | + interval.unref?.(); |
42 | 60 | } |
43 | 61 |
|
44 | | -export async function checkDbConnectivity(): Promise<{ status: "OK" | "ERROR"; latencyMs?: number }> { |
45 | | - const start = Date.now(); |
46 | | - try { |
47 | | - await sql`SELECT 1`; |
48 | | - return { status: "OK", latencyMs: Date.now() - start }; |
49 | | - } catch { |
50 | | - return { status: "ERROR" }; |
51 | | - } |
| 62 | +export function getDependencyHealth() { |
| 63 | + return dependencyHealth; |
52 | 64 | } |
53 | 65 |
|
54 | 66 | export function getHealthStatus() { |
55 | | - const memoryTotal = totalmem(); |
56 | | - const memoryFree = freemem(); |
57 | | - const memoryUsed = memoryTotal - memoryFree; |
58 | | - const memoryUsagePercent = ((memoryUsed / memoryTotal) * 100).toFixed(1); |
59 | | - |
60 | | - const [oneMin, fiveMin, fifteenMin] = loadavg(); |
61 | | - const cpuCount = cpus().length; |
62 | | - |
63 | | - const health = { |
| 67 | + return { |
64 | 68 | status: "OK", |
65 | 69 | timestamp: new Date().toISOString(), |
66 | | - server: { |
67 | | - hostname: hostname(), |
68 | | - uptime: formatUptime(process.uptime()), |
69 | | - }, |
70 | | - memory: { |
71 | | - total: formatBytes(memoryTotal), |
72 | | - used: formatBytes(memoryUsed), |
73 | | - free: formatBytes(memoryFree), |
74 | | - usage: `${memoryUsagePercent}%`, |
75 | | - status: Number(memoryUsagePercent) > 90 ? "WARNING" : "OK", |
76 | | - }, |
77 | | - cpu: { |
78 | | - cores: cpuCount, |
79 | | - load: { |
80 | | - "1min": ((oneMin / cpuCount) * 100).toFixed(1) + "%", |
81 | | - "5min": ((fiveMin / cpuCount) * 100).toFixed(1) + "%", |
82 | | - "15min": ((fifteenMin / cpuCount) * 100).toFixed(1) + "%", |
83 | | - }, |
84 | | - history: cpuHistory, |
85 | | - status: oneMin / cpuCount > 0.8 ? "WARNING" : "OK", |
86 | | - }, |
87 | 70 | }; |
88 | | - |
89 | | - const statusCode = health.memory.status === "WARNING" || health.cpu.status === "WARNING" ? 207 : 200; |
90 | | - |
91 | | - return { health, statusCode }; |
92 | 71 | } |
0 commit comments