bodhi-node-profiler
Version:
A lightweight, zero-configuration performance profiler for Node.js applications with real-time dashboard
316 lines (314 loc) • 10.8 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
BodhiProfiler: () => BodhiProfiler
});
module.exports = __toCommonJS(index_exports);
var import_express = __toESM(require("express"));
var import_winston = __toESM(require("winston"));
var import_winston_daily_rotate_file = require("winston-daily-rotate-file");
var import_path = __toESM(require("path"));
var import_fs = __toESM(require("fs"));
var import_os = __toESM(require("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 = import_path.default.join(import_path.default.dirname(logDir), "archive");
[logDir, archiveDir].forEach((dir) => {
if (!import_fs.default.existsSync(dir)) {
import_fs.default.mkdirSync(dir, { recursive: true });
}
});
const formats = [
import_winston.default.format.timestamp(),
import_winston.default.format.json()
];
const transports = [];
if ((_a = this.options.logging) == null ? void 0 : _a.file) {
transports.push(
new import_winston.default.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 import_winston.default.transports.Console());
}
this.logger = import_winston.default.createLogger({
format: import_winston.default.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 = import_os.default.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 = import_path.default.join(import_path.default.dirname(this.options.logPath), "archive");
if (!import_fs.default.existsSync(archiveDir)) return;
const maxAge = this.options.logging.cleanup.maxAge || "30d";
const maxAgeMs = this.parseTimeString(maxAge);
const now = Date.now();
import_fs.default.readdirSync(archiveDir).forEach((file) => {
const filePath = import_path.default.join(archiveDir, file);
const stats = import_fs.default.statSync(filePath);
if (now - stats.mtimeMs > maxAgeMs) {
import_fs.default.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 = (0, import_express.default)();
const dashboardPath = import_path.default.join(__dirname, "..", "src", "dashboard");
app.use("/profiler", import_express.default.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();
};
}
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
BodhiProfiler
});