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
JavaScript
// 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
};