UNPKG

@texagon/nestjs-health

Version:

A comprehensive health monitoring package for NestJS applications with metrics and Kubernetes-ready health checks

259 lines (241 loc) 8.29 kB
import { Controller, Get, VERSION_NEUTRAL, Version, Query, Inject, Optional, UseGuards, } from "@nestjs/common"; import { ApiTags, ApiOperation, ApiQuery, ApiResponse, ApiSecurity, } from "@nestjs/swagger"; import { HealthCheck, HealthCheckService, MemoryHealthIndicator, DiskHealthIndicator, } from "@nestjs/terminus"; import { HealthService } from "./health.service"; import { MetricsService } from "./metrics.service"; import { HealthModuleOptions, MODULE_OPTIONS_TOKEN } from "./health.module"; import { HealthAuthGuard } from "./health-auth.guard"; @ApiTags("health") @ApiSecurity("auth-key") @Controller("health") export class HealthController { private readonly options: any; constructor( private health: HealthCheckService, private memory: MemoryHealthIndicator, private disk: DiskHealthIndicator, private healthService: HealthService, private metricsService: MetricsService, @Optional() @Inject(MODULE_OPTIONS_TOKEN) options: HealthModuleOptions, ) { this.options = options || { enableMemoryMonitoring: true, enableDiskMonitoring: true, diskPath: "/", diskThresholdPercent: 0.75, routePrefix: "health", }; } @Get() @UseGuards(HealthAuthGuard) @HealthCheck() @ApiOperation({ summary: "Check application health status" }) @ApiResponse({ status: 200, description: "Application is healthy" }) @ApiResponse({ status: 503, description: "Application is not healthy" }) check() { const checks = [() => this.healthService.isHealthy()]; // Add memory checks if enabled if (this.options.enableMemoryMonitoring) { checks.push(() => this.memory.checkHeap("memory_heap", 150 * 1024 * 1024), ); checks.push(() => this.memory.checkRSS("memory_rss", 150 * 1024 * 1024)); } // Add disk checks if enabled if (this.options.enableDiskMonitoring) { checks.push(() => this.disk.checkStorage("disk", { path: this.options.diskPath, thresholdPercent: this.options.diskThresholdPercent, }), ); } return this.health.check(checks); } @Get("live") @UseGuards(HealthAuthGuard) @HealthCheck() @ApiOperation({ summary: "Liveness probe for container orchestration" }) @ApiResponse({ status: 200, description: "Application is alive" }) @ApiResponse({ status: 503, description: "Application is not alive" }) liveness() { return this.health.check([() => this.healthService.isHealthy()]); } @Get("ready") @UseGuards(HealthAuthGuard) @HealthCheck() @ApiOperation({ summary: "Readiness probe for container orchestration" }) @ApiResponse({ status: 200, description: "Application is ready to accept traffic", }) @ApiResponse({ status: 503, description: "Application is not ready" }) readiness() { return this.health.check([ () => this.healthService.isHealthy(), () => this.memory.checkHeap("memory_heap", 300 * 1024 * 1024), ]); } @Get("stats") @UseGuards(HealthAuthGuard) @Version(VERSION_NEUTRAL) @ApiOperation({ summary: "Get detailed application statistics" }) @ApiQuery({ name: "includeHistory", required: false, type: "boolean", description: "Whether to include historical metrics data", }) @ApiResponse({ status: 200, description: "Application statistics retrieved successfully", }) async getDetailedStats(@Query("includeHistory") includeHistory: string) { const startTime = process.uptime(); const memoryStats = await this.healthService.getMemoryStats(); // Get health status safely with try/catch to handle possible HealthCheckError let healthStatus = "unhealthy"; let healthDetails = {}; try { const health = await this.healthService.isHealthy(); healthStatus = "healthy"; healthDetails = health; } catch (error) { healthStatus = "unhealthy"; if (error.response) { healthDetails = error.response; } } const diskStats = this.options.enableDiskMonitoring ? await this.disk.checkStorage("disk", { path: this.options.diskPath, thresholdPercent: this.options.diskThresholdPercent, }) : { disk: { status: "unknown" } }; const os = require("os"); const cpuUsage = process.cpuUsage(); const systemUptime = os.uptime(); const loadAvg = os.loadavg(); const metrics = this.metricsService.getMetrics(); return { status: healthStatus, timestamp: new Date().toISOString(), version: process.env.npm_package_version || "1.0.0", process: { pid: process.pid, ppid: process.ppid, title: process.title, uptime: { seconds: startTime, formatted: this.formatUptime(startTime), }, memoryUsage: { current: { rss: this.formatBytes(process.memoryUsage().rss), heapTotal: this.formatBytes(process.memoryUsage().heapTotal), heapUsed: this.formatBytes(process.memoryUsage().heapUsed), external: this.formatBytes(process.memoryUsage().external || 0), arrayBuffers: this.formatBytes( process.memoryUsage().arrayBuffers || 0, ), }, max: { rss: this.formatBytes(metrics.max.memory.rss), heapTotal: this.formatBytes(metrics.max.memory.heapTotal), heapUsed: this.formatBytes(metrics.max.memory.heapUsed), external: this.formatBytes(metrics.max.memory.external), arrayBuffers: this.formatBytes( metrics.max.memory.arrayBuffers || 0, ), }, min: { rss: this.formatBytes(metrics.min.memory.rss), heapTotal: this.formatBytes(metrics.min.memory.heapTotal), heapUsed: this.formatBytes(metrics.min.memory.heapUsed), external: this.formatBytes(metrics.min.memory.external), arrayBuffers: this.formatBytes( metrics.min.memory.arrayBuffers || 0, ), }, }, cpuUsage: { current: { user: cpuUsage.user, system: cpuUsage.system, percent: metrics.current?.cpu.percent.toFixed(2) + "%" || "0%", }, max: { user: metrics.max.cpu.user, system: metrics.max.cpu.system, percent: metrics.max.cpu.percent.toFixed(2) + "%", }, }, }, system: { platform: process.platform, arch: process.arch, nodeVersion: process.version, hostname: os.hostname(), uptime: { seconds: systemUptime, formatted: this.formatUptime(systemUptime), }, loadAvg: os.loadavg(), totalMemory: this.formatBytes(os.totalmem()), freeMemory: this.formatBytes(os.freemem()), cpus: os.cpus().length, }, disk: diskStats, environment: process.env.NODE_ENV || "development", requests: { total: metrics.requests.stats.total, responseTime: { avg: metrics.requests.stats.avgResponseTime.toFixed(2) + "ms", max: metrics.requests.stats.maxResponseTime + "ms", min: metrics.requests.stats.minResponseTime === Number.MAX_SAFE_INTEGER ? "0ms" : metrics.requests.stats.minResponseTime + "ms", }, byMethod: metrics.requests.stats.byMethod, byStatusCode: metrics.requests.stats.byStatusCode, recent: metrics.requests.recent, }, ...(includeHistory === "true" && { history: metrics.history }), }; } private formatBytes(bytes: number): string { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; } private formatUptime(seconds: number): string { const days = Math.floor(seconds / (3600 * 24)); const hours = Math.floor((seconds % (3600 * 24)) / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); return `${days}d ${hours}h ${minutes}m ${secs}s`; } }