UNPKG

bodhi-node-profiler

Version:

A lightweight, zero-configuration performance profiler for Node.js applications with real-time dashboard

281 lines (280 loc) 8.77 kB
// src/index.ts import express from "express"; import winston from "winston"; import "winston-daily-rotate-file"; import path from "path"; import fs from "fs"; import os from "os"; var BodhiProfiler = class { constructor(options = {}) { this.lastCpuUsage = process.cpuUsage(); this.lastCpuCheck = Date.now(); this.options = { serviceName: "app", enableWebDashboard: true, port: 45678, logPath: "./logs/profiler", sampleInterval: 5e3, logging: { console: false, file: true, format: "json", rotation: { maxFiles: "7d", maxSize: "5m", datePattern: "YYYY-MM-DD", compress: true }, cleanup: { enabled: true, maxAge: "30d" } }, metrics: { cpu: { enabled: true, threshold: 70 }, memory: { enabled: true, threshold: 80 }, eventLoop: { enabled: true, threshold: 100 }, api: { responseTime: { enabled: true, threshold: 1e3 }, errorRate: true, throughput: { enabled: true, interval: "1m" } } }, ...options }; this.setupLogger(); this.startTime = Date.now(); this.stats = this.initializeStats(); this.apiResponseTimes = []; this.maxResponseTimes = 100; this.startMetricsCollection(); if (this.options.enableWebDashboard) { this.setupDashboard(); } } setupLogger() { var _a, _b, _c, _d, _e, _f; const logDir = this.options.logPath; const archiveDir = path.join(path.dirname(logDir), "archive"); [logDir, archiveDir].forEach((dir) => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } }); const formats = [ winston.format.timestamp(), winston.format.json() ]; const transports = []; if ((_a = this.options.logging) == null ? void 0 : _a.file) { transports.push( new winston.transports.DailyRotateFile({ dirname: logDir, filename: "profiler-%DATE%.log", datePattern: ((_b = this.options.logging.rotation) == null ? void 0 : _b.datePattern) || "YYYY-MM-DD", maxSize: ((_c = this.options.logging.rotation) == null ? void 0 : _c.maxSize) || "5m", maxFiles: ((_d = this.options.logging.rotation) == null ? void 0 : _d.maxFiles) || "7d", zippedArchive: ((_e = this.options.logging.rotation) == null ? void 0 : _e.compress) || true }) ); } if ((_f = this.options.logging) == null ? void 0 : _f.console) { transports.push(new winston.transports.Console()); } this.logger = winston.createLogger({ format: winston.format.combine(...formats), transports }); } shouldLogMetric(type, value) { var _a; const config = (_a = this.options.metrics) == null ? void 0 : _a[type]; if (!(config == null ? void 0 : config.enabled)) return false; return value >= (config.threshold || 0); } calculateCpuUsage() { const now = Date.now(); const currentCpuUsage = process.cpuUsage(); const userDiff = currentCpuUsage.user - this.lastCpuUsage.user; const systemDiff = currentCpuUsage.system - this.lastCpuUsage.system; const timeDiff = now - this.lastCpuCheck; const cpuPercent = (userDiff + systemDiff) / (timeDiff * 1e3) * 100; this.lastCpuUsage = currentCpuUsage; this.lastCpuCheck = now; return Math.min(100, Math.max(0, cpuPercent)); } async collectMetrics() { try { const heapStats = process.memoryUsage(); const totalMem = os.totalmem(); const cpuUsage = this.calculateCpuUsage(); this.stats = { timestamp: Date.now(), cpu: { usage: cpuUsage, system: cpuUsage * 0.3, user: cpuUsage * 0.7 }, memory: { used: heapStats.heapUsed, total: totalMem, rss: heapStats.rss, heapUsed: heapStats.heapUsed, heapTotal: heapStats.heapTotal, percentage: (heapStats.heapUsed / totalMem * 100).toFixed(1) }, eventLoop: { latency: this.getEventLoopLag(), lag: this.getEventLoopLag() } }; console.log("Current Stats:", { cpu: this.stats.cpu.usage.toFixed(1) + "%", memory: Math.round(this.stats.memory.heapUsed / (1024 * 1024)) + "MB", eventLoop: Math.round(this.stats.eventLoop.latency) + "ms" }); if (this.shouldLogMetric("cpu", this.stats.cpu.usage) || this.shouldLogMetric("memory", this.stats.memory.used / this.stats.memory.total * 100) || this.shouldLogMetric("eventLoop", this.stats.eventLoop.latency)) { this.logger.info("metrics", this.stats); } } catch (error) { console.error("Error collecting metrics:", error); this.logger.error("Error collecting metrics:", error); } } cleanupLogs() { var _a, _b; if (!((_b = (_a = this.options.logging) == null ? void 0 : _a.cleanup) == null ? void 0 : _b.enabled)) return; const archiveDir = path.join(path.dirname(this.options.logPath), "archive"); if (!fs.existsSync(archiveDir)) return; const maxAge = this.options.logging.cleanup.maxAge || "30d"; const maxAgeMs = this.parseTimeString(maxAge); const now = Date.now(); fs.readdirSync(archiveDir).forEach((file) => { const filePath = path.join(archiveDir, file); const stats = fs.statSync(filePath); if (now - stats.mtimeMs > maxAgeMs) { fs.unlinkSync(filePath); } }); } parseTimeString(time) { const unit = time.slice(-1); const value = parseInt(time.slice(0, -1)); switch (unit) { case "d": return value * 24 * 60 * 60 * 1e3; case "h": return value * 60 * 60 * 1e3; case "m": return value * 60 * 1e3; case "s": return value * 1e3; default: return value; } } initializeStats() { return { timestamp: Date.now(), cpu: { usage: 0, system: 0, user: 0 }, memory: { used: 0, total: 0, rss: 0, heapUsed: 0, heapTotal: 0 }, eventLoop: { latency: 0, lag: 0 } }; } startMetricsCollection() { this.collectMetrics(); setInterval(() => { this.collectMetrics(); }, 1e3); } setupDashboard() { const app = express(); const dashboardPath = path.join(__dirname, "..", "src", "dashboard"); app.use("/profiler", express.static(dashboardPath)); app.get("/profiler/stats", async (req, res) => { try { const systemStats = this.stats; const avgResponseTime = this.apiResponseTimes.length > 0 ? this.apiResponseTimes.reduce((a, b) => a + b, 0) / this.apiResponseTimes.length : 0; const uptime = process.uptime(); const uptimeStr = uptime.toFixed(0); res.json({ cpu: { percentage: parseFloat(systemStats.cpu.usage.toFixed(1)), system: systemStats.cpu.system, user: systemStats.cpu.user }, memory: { heapUsed: Math.round(systemStats.memory.heapUsed / 1024 / 1024), heapTotal: Math.round(systemStats.memory.heapTotal / 1024 / 1024), percentage: Math.round(systemStats.memory.heapUsed / systemStats.memory.heapTotal * 100), rss: Math.round(systemStats.memory.rss / 1024 / 1024) }, eventLoop: { latency: systemStats.eventLoop.latency }, api: { responseTime: avgResponseTime }, uptime: uptimeStr, timestamp: Date.now() }); } catch (error) { console.error("Error getting stats:", error); res.status(500).json({ error: "Failed to get stats" }); } }); app.listen(this.options.port, () => { console.log(`Profiler dashboard available at http://localhost:${this.options.port}/profiler`); }); } getEventLoopLag() { const start = process.hrtime(); const NS_PER_MS = 1e6; return process.hrtime(start)[1] / NS_PER_MS; } middleware() { return (req, res, next) => { const start = process.hrtime(); res.on("finish", () => { const [seconds, nanoseconds] = process.hrtime(start); const duration = seconds * 1e3 + nanoseconds / 1e6; this.apiResponseTimes.push(duration); if (this.apiResponseTimes.length > this.maxResponseTimes) { this.apiResponseTimes.shift(); } }); next(); }; } }; export { BodhiProfiler };