import * as plugins from './smartmetrics.plugins.js'; import * as interfaces from './smartmetrics.interfaces.js'; export class SmartMetrics { public started = false; public sourceNameArg: string; public logger: plugins.smartlog.Smartlog; public registry: plugins.prom.Registry; public maxMemoryMB: number; // Prometheus gauges for custom metrics private cpuPercentageGauge: plugins.prom.Gauge; private memoryPercentageGauge: plugins.prom.Gauge; private memoryUsageBytesGauge: plugins.prom.Gauge; private systemCpuPercentGauge: plugins.prom.Gauge; private systemMemUsedPercentGauge: plugins.prom.Gauge; private systemMemUsedBytesGauge: plugins.prom.Gauge; private systemLoadAvg1Gauge: plugins.prom.Gauge; private systemLoadAvg5Gauge: plugins.prom.Gauge; private systemLoadAvg15Gauge: plugins.prom.Gauge; // HTTP server for Prometheus endpoint private prometheusServer?: plugins.http.Server; private prometheusPort?: number; public setup() { this.registry = new plugins.prom.Registry(); plugins.prom.collectDefaultMetrics(this.registry); // Initialize custom gauges this.cpuPercentageGauge = new plugins.prom.Gauge({ name: 'smartmetrics_cpu_percentage', help: 'Current CPU usage percentage', registers: [this.registry] }); this.memoryPercentageGauge = new plugins.prom.Gauge({ name: 'smartmetrics_memory_percentage', help: 'Current memory usage percentage', registers: [this.registry] }); this.memoryUsageBytesGauge = new plugins.prom.Gauge({ name: 'smartmetrics_memory_usage_bytes', help: 'Current memory usage in bytes', registers: [this.registry] }); this.systemCpuPercentGauge = new plugins.prom.Gauge({ name: 'smartmetrics_system_cpu_percent', help: 'System-wide CPU usage percentage', registers: [this.registry] }); this.systemMemUsedPercentGauge = new plugins.prom.Gauge({ name: 'smartmetrics_system_memory_used_percent', help: 'System-wide memory usage percentage', registers: [this.registry] }); this.systemMemUsedBytesGauge = new plugins.prom.Gauge({ name: 'smartmetrics_system_memory_used_bytes', help: 'System-wide memory used in bytes', registers: [this.registry] }); this.systemLoadAvg1Gauge = new plugins.prom.Gauge({ name: 'smartmetrics_system_load_avg_1', help: 'System 1-minute load average', registers: [this.registry] }); this.systemLoadAvg5Gauge = new plugins.prom.Gauge({ name: 'smartmetrics_system_load_avg_5', help: 'System 5-minute load average', registers: [this.registry] }); this.systemLoadAvg15Gauge = new plugins.prom.Gauge({ name: 'smartmetrics_system_load_avg_15', help: 'System 15-minute load average', registers: [this.registry] }); } constructor(loggerArg: plugins.smartlog.Smartlog, sourceNameArg: string) { this.logger = loggerArg; this.sourceNameArg = sourceNameArg; this.setup(); this.checkMemoryLimits(); } private checkMemoryLimits() { const heapStats = plugins.v8.getHeapStatistics(); const maxHeapSizeMB = heapStats.heap_size_limit / 1024 / 1024; const totalSystemMemoryMB = plugins.os.totalmem() / 1024 / 1024; let dockerMemoryLimitMB = totalSystemMemoryMB; // Try cgroup v2 first, then fall back to cgroup v1 try { const cgroupV2 = plugins.fs.readFileSync('/sys/fs/cgroup/memory.max', 'utf8').trim(); if (cgroupV2 !== 'max') { dockerMemoryLimitMB = parseInt(cgroupV2, 10) / 1024 / 1024; } } catch { try { const cgroupV1 = plugins.fs.readFileSync( '/sys/fs/cgroup/memory/memory.limit_in_bytes', 'utf8' ).trim(); dockerMemoryLimitMB = parseInt(cgroupV1, 10) / 1024 / 1024; } catch { // Not running in a container — use system memory } } // Pick the most restrictive limit this.maxMemoryMB = Math.min(totalSystemMemoryMB, dockerMemoryLimitMB, maxHeapSizeMB); } public start() { const unattendedStart = async () => { if (this.started) { return; } this.started = true; while (this.started) { this.logger.log('info', `sending heartbeat for ${this.sourceNameArg} with metrics`, { eventType: 'heartbeat', metrics: await this.getMetrics(), }); await plugins.smartdelay.delayFor(20000, null, true); } }; unattendedStart(); } public formatBytes(bytes: number, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } public async getMetrics() { let pids: number[] = []; try { pids = await plugins.pidtree.getChildPids(process.pid); } catch { // pidtree can fail if process tree cannot be read } const stats = await plugins.pidusage.getPidUsage([process.pid, ...pids]); // Aggregate normalized CPU (0-100% of total machine) across process tree let cpuPercentage = 0; for (const stat of Object.values(stats)) { if (!stat) continue; cpuPercentage += stat.cpuNormalizedPercent; } let cpuUsageText = `${Math.round(cpuPercentage * 100) / 100} %`; let memoryUsageBytes = 0; for (const stat of Object.values(stats)) { if (!stat) continue; memoryUsageBytes += stat.memory; } let memoryPercentage = Math.round((memoryUsageBytes / (this.maxMemoryMB * 1024 * 1024)) * 100 * 100) / 100; let memoryUsageText = `${memoryPercentage}% | ${this.formatBytes( memoryUsageBytes )} / ${this.formatBytes(this.maxMemoryMB * 1024 * 1024)}`; // Get system-wide metrics const systemUsage = await plugins.sysusage.getSystemUsage(); // Update Prometheus gauges with current values if (this.cpuPercentageGauge) { this.cpuPercentageGauge.set(cpuPercentage); } if (this.memoryPercentageGauge) { this.memoryPercentageGauge.set(memoryPercentage); } if (this.memoryUsageBytesGauge) { this.memoryUsageBytesGauge.set(memoryUsageBytes); } if (this.systemCpuPercentGauge) { this.systemCpuPercentGauge.set(systemUsage.cpuPercent); } if (this.systemMemUsedPercentGauge) { this.systemMemUsedPercentGauge.set(systemUsage.memUsedPercent); } if (this.systemMemUsedBytesGauge) { this.systemMemUsedBytesGauge.set(systemUsage.memUsedBytes); } if (this.systemLoadAvg1Gauge) { this.systemLoadAvg1Gauge.set(systemUsage.loadAvg1); } if (this.systemLoadAvg5Gauge) { this.systemLoadAvg5Gauge.set(systemUsage.loadAvg5); } if (this.systemLoadAvg15Gauge) { this.systemLoadAvg15Gauge.set(systemUsage.loadAvg15); } // Calculate Node.js metrics directly const cpuUsage = process.cpuUsage(); const process_cpu_seconds_total = (cpuUsage.user + cpuUsage.system) / 1000000; const heapStats = plugins.v8.getHeapStatistics(); const nodejs_heap_size_total_bytes = heapStats.total_heap_size; const nodejs_active_handles_total = 0; const nodejs_active_requests_total = 0; const returnMetrics: interfaces.IMetricsSnapshot = { process_cpu_seconds_total, nodejs_active_handles_total, nodejs_active_requests_total, nodejs_heap_size_total_bytes, cpuPercentage, cpuUsageText, memoryPercentage, memoryUsageBytes, memoryUsageText, systemCpuPercent: systemUsage.cpuPercent, systemMemTotalBytes: systemUsage.memTotalBytes, systemMemAvailableBytes: systemUsage.memAvailableBytes, systemMemUsedBytes: systemUsage.memUsedBytes, systemMemUsedPercent: systemUsage.memUsedPercent, systemLoadAvg1: systemUsage.loadAvg1, systemLoadAvg5: systemUsage.loadAvg5, systemLoadAvg15: systemUsage.loadAvg15, }; return returnMetrics; } public async getPrometheusFormattedMetrics(): Promise { // Update metrics to ensure gauges have latest values await this.getMetrics(); // Return Prometheus text exposition format return await this.registry.metrics(); } public enablePrometheusEndpoint(port: number = 9090): void { if (this.prometheusServer) { this.logger.log('warn', 'Prometheus endpoint is already running'); return; } this.prometheusServer = plugins.http.createServer(async (req, res) => { if (req.url === '/metrics' && req.method === 'GET') { try { const metrics = await this.getPrometheusFormattedMetrics(); res.writeHead(200, { 'Content-Type': 'text/plain; version=0.0.4' }); res.end(metrics); } catch (error) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error generating metrics'); this.logger.log('error', 'Error generating Prometheus metrics', error); } } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } }); this.prometheusPort = port; this.prometheusServer.listen(port, () => { this.logger.log('info', `Prometheus metrics endpoint available at http://localhost:${port}/metrics`); }); } public disablePrometheusEndpoint(): void { if (!this.prometheusServer) { return; } const port = this.prometheusPort; this.prometheusServer.close(() => { this.logger.log('info', `Prometheus metrics endpoint on port ${port} has been shut down`); }); this.prometheusServer = undefined; this.prometheusPort = undefined; } public stop() { this.started = false; this.disablePrometheusEndpoint(); } }