Skip to content

Commit aa2fb9c

Browse files
committed
feat: enhance health check functionality by adding CPU, memory, and disk usage metrics, along with future checks for external exposure and leaked passwords
1 parent 7f14a71 commit aa2fb9c

File tree

3 files changed

+378
-14
lines changed

3 files changed

+378
-14
lines changed

apps/api/src/core/health/health.controller.ts

Lines changed: 185 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,50 @@
1-
21
import { Controller, Get } from '@nestjs/common'
2+
import { statfs } from 'fs/promises'
3+
import { cpus, loadavg, totalmem } from 'os'
34
import { ApiTags } from '@nestjs/swagger'
4-
import { HealthCheckService, HttpHealthIndicator, HealthCheck, DiskHealthIndicator, MemoryHealthIndicator, MongooseHealthIndicator, HealthCheckResult } from '@nestjs/terminus'
5+
import { DiskHealthIndicator, HealthCheck, HealthCheckError, HealthCheckResult, HealthCheckService, HttpHealthIndicator, MemoryHealthIndicator, MongooseHealthIndicator } from '@nestjs/terminus'
56
import { Public } from '~/_common/decorators/public.decorator'
67

78
/**
89
* Multiplicateur pour convertir les octets en mégaoctets.
910
* @constant {number}
1011
*/
1112
const MEMORY_MULTIPLIER = 1024 * 1024
13+
const GIGABYTE_MULTIPLIER = 1024 * 1024 * 1024
14+
const CPU_LOAD_THRESHOLD = 0.85
15+
const DISK_THRESHOLD_PERCENT = 0.95
16+
const HEAP_MEMORY_THRESHOLD_MB = 512
17+
const RSS_MEMORY_THRESHOLD_MB = 512
18+
19+
type HealthResponse = HealthCheckResult & {
20+
system: {
21+
memory: {
22+
heapUsedMb: number
23+
heapTotalMb: number
24+
rssMb: number
25+
totalSystemMemoryMb: number
26+
}
27+
cpu: {
28+
load1mPerCore: number
29+
load5mPerCore: number
30+
load15mPerCore: number
31+
cores: number
32+
threshold: number
33+
}
34+
}
35+
futureChecks: {
36+
externalExposure: {
37+
enabled: boolean
38+
status: 'not_configured'
39+
note: string
40+
}
41+
leakedPasswords: {
42+
enabled: boolean
43+
status: 'not_implemented'
44+
note: string
45+
}
46+
}
47+
}
1248

1349
/**
1450
* Contrôleur pour la vérification de l'état de santé du système.
@@ -59,25 +95,160 @@ export class HealthController {
5995
* et ses dépendances sont opérationnelles. Retourne un statut global
6096
* ainsi que le détail de chaque indicateur.
6197
*
62-
* @returns {Promise<HealthCheckResult>} Résultat complet du health check
98+
* @returns {Promise<HealthResponse>} Résultat complet du health check enrichi
6399
*/
64100
@Get()
65101
@HealthCheck()
66-
public async check(): Promise<HealthCheckResult> {
67-
return await this.health.check([
68-
() => this.mongoose.pingCheck('mongoose'),
102+
public async check(): Promise<HealthResponse> {
103+
const healthResult = await this.health.check([
104+
() => this.checkMongoose(),
69105

70106
() => this.http.pingCheck('http-github', 'https://github.com'),
71107

72-
// DISK en GB
73-
() => this.disk.checkStorage('storage', {
74-
path: '/',
75-
thresholdPercent: 0.95,
76-
}),
108+
() => this.checkStorage(),
77109

78-
// MB pour heap et RSS
79-
() => this.memory.checkHeap('memory_heap', 512 * MEMORY_MULTIPLIER),
80-
() => this.memory.checkRSS('memory_rss', 512 * MEMORY_MULTIPLIER),
110+
() => this.checkMemoryHeap(),
111+
() => this.checkMemoryRss(),
112+
() => this.checkCpu(),
81113
])
114+
115+
const memoryUsage = process.memoryUsage()
116+
const cpuCount = Math.max(cpus().length, 1)
117+
const [load1m, load5m, load15m] = loadavg()
118+
119+
return {
120+
...healthResult,
121+
system: {
122+
memory: {
123+
heapUsedMb: Number((memoryUsage.heapUsed / MEMORY_MULTIPLIER).toFixed(2)),
124+
heapTotalMb: Number((memoryUsage.heapTotal / MEMORY_MULTIPLIER).toFixed(2)),
125+
rssMb: Number((memoryUsage.rss / MEMORY_MULTIPLIER).toFixed(2)),
126+
totalSystemMemoryMb: Number((totalmem() / MEMORY_MULTIPLIER).toFixed(2)),
127+
},
128+
cpu: {
129+
load1mPerCore: Number((load1m / cpuCount).toFixed(3)),
130+
load5mPerCore: Number((load5m / cpuCount).toFixed(3)),
131+
load15mPerCore: Number((load15m / cpuCount).toFixed(3)),
132+
cores: cpuCount,
133+
threshold: CPU_LOAD_THRESHOLD,
134+
},
135+
},
136+
futureChecks: {
137+
externalExposure: {
138+
enabled: false,
139+
status: 'not_configured',
140+
note: 'Reserved for a future check against a configured public URL (ex: APP_PUBLIC_URL).',
141+
},
142+
leakedPasswords: {
143+
enabled: false,
144+
status: 'not_implemented',
145+
note: 'Reserved for future leaked-password detection integration (k-anonymity/HIBP style).',
146+
},
147+
},
148+
}
149+
}
150+
151+
private checkCpu(): Record<string, { status: 'up' | 'down'; load1mPerCore: number; threshold: number; cores: number }> {
152+
const cpuCount = Math.max(cpus().length, 1)
153+
const perCoreLoad = loadavg()[0] / cpuCount
154+
const indicator = {
155+
status: perCoreLoad <= CPU_LOAD_THRESHOLD ? 'up' : 'down',
156+
load1mPerCore: Number(perCoreLoad.toFixed(3)),
157+
threshold: CPU_LOAD_THRESHOLD,
158+
cores: cpuCount,
159+
} as const
160+
161+
if (indicator.status === 'down') {
162+
throw new HealthCheckError('cpu_check_failed', { cpu: indicator })
163+
}
164+
165+
return { cpu: indicator }
166+
}
167+
168+
private async checkMongoose(): Promise<Record<string, { status: 'up' | 'down'; pingMs: number }>> {
169+
const start = Date.now()
170+
171+
try {
172+
await this.mongoose.pingCheck('mongoose')
173+
return {
174+
mongoose: {
175+
status: 'up',
176+
pingMs: Date.now() - start,
177+
},
178+
}
179+
} catch {
180+
throw new HealthCheckError('mongoose_check_failed', {
181+
mongoose: {
182+
status: 'down',
183+
pingMs: Date.now() - start,
184+
},
185+
})
186+
}
187+
}
188+
189+
private async checkStorage(): Promise<Record<string, { status: 'up' | 'down'; usedPercent: number; thresholdPercent: number; totalGb: number; usedGb: number; freeGb: number }>> {
190+
await this.disk.checkStorage('storage', {
191+
path: '/',
192+
thresholdPercent: DISK_THRESHOLD_PERCENT,
193+
})
194+
195+
const fsStats = await statfs('/')
196+
const totalBytes = fsStats.blocks * fsStats.bsize
197+
const freeBytes = fsStats.bavail * fsStats.bsize
198+
const usedBytes = Math.max(totalBytes - freeBytes, 0)
199+
const usedPercent = totalBytes > 0 ? usedBytes / totalBytes : 0
200+
201+
const indicator = {
202+
status: usedPercent <= DISK_THRESHOLD_PERCENT ? 'up' : 'down',
203+
usedPercent: Number((usedPercent * 100).toFixed(2)),
204+
thresholdPercent: Number((DISK_THRESHOLD_PERCENT * 100).toFixed(2)),
205+
totalGb: Number((totalBytes / GIGABYTE_MULTIPLIER).toFixed(2)),
206+
usedGb: Number((usedBytes / GIGABYTE_MULTIPLIER).toFixed(2)),
207+
freeGb: Number((freeBytes / GIGABYTE_MULTIPLIER).toFixed(2)),
208+
} as const
209+
210+
if (indicator.status === 'down') {
211+
throw new HealthCheckError('storage_check_failed', { storage: indicator })
212+
}
213+
214+
return { storage: indicator }
215+
}
216+
217+
private async checkMemoryHeap(): Promise<Record<string, { status: 'up' | 'down'; usedMb: number; thresholdMb: number; usedPercent: number }>> {
218+
await this.memory.checkHeap('memory_heap', HEAP_MEMORY_THRESHOLD_MB * MEMORY_MULTIPLIER)
219+
const heapUsedMb = process.memoryUsage().heapUsed / MEMORY_MULTIPLIER
220+
const usedPercent = heapUsedMb / HEAP_MEMORY_THRESHOLD_MB
221+
222+
const indicator = {
223+
status: heapUsedMb <= HEAP_MEMORY_THRESHOLD_MB ? 'up' : 'down',
224+
usedMb: Number(heapUsedMb.toFixed(2)),
225+
thresholdMb: HEAP_MEMORY_THRESHOLD_MB,
226+
usedPercent: Number((usedPercent * 100).toFixed(2)),
227+
} as const
228+
229+
if (indicator.status === 'down') {
230+
throw new HealthCheckError('memory_heap_check_failed', { memory_heap: indicator })
231+
}
232+
233+
return { memory_heap: indicator }
234+
}
235+
236+
private async checkMemoryRss(): Promise<Record<string, { status: 'up' | 'down'; usedMb: number; thresholdMb: number; usedPercent: number }>> {
237+
await this.memory.checkRSS('memory_rss', RSS_MEMORY_THRESHOLD_MB * MEMORY_MULTIPLIER)
238+
const rssMb = process.memoryUsage().rss / MEMORY_MULTIPLIER
239+
const usedPercent = rssMb / RSS_MEMORY_THRESHOLD_MB
240+
241+
const indicator = {
242+
status: rssMb <= RSS_MEMORY_THRESHOLD_MB ? 'up' : 'down',
243+
usedMb: Number(rssMb.toFixed(2)),
244+
thresholdMb: RSS_MEMORY_THRESHOLD_MB,
245+
usedPercent: Number((usedPercent * 100).toFixed(2)),
246+
} as const
247+
248+
if (indicator.status === 'down') {
249+
throw new HealthCheckError('memory_rss_check_failed', { memory_rss: indicator })
250+
}
251+
252+
return { memory_rss: indicator }
82253
}
83254
}

apps/web/src/pages/settings.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ export default defineNuxtComponent({
9595
label: 'Tâches planifiés',
9696
_acl: '/core/cron',
9797
},
98+
// {
99+
// route: '/settings/health',
100+
// icon: 'mdi-heart-pulse',
101+
// label: 'Santé applicative',
102+
// },
98103
])
99104
100105
const navItems = computed(() => {

0 commit comments

Comments
 (0)