pm2-slack-alerts
Version:
A PM2 module that sends formatted Slack notifications for process events with support for clustering, mentions, metrics, and per-app config.
185 lines (184 loc) âĸ 8.81 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const pm2_1 = __importDefault(require("pm2"));
const axios_1 = __importDefault(require("axios"));
const os_1 = __importDefault(require("os"));
const hostname = os_1.default.hostname();
const eventTimers = {};
const globalSlackUrl = process.env.PM2_SLACK_URL;
const eventFilterRaw = process.env.PM2_SLACK_EVENTS || "";
const eventFilterList = eventFilterRaw.split(",").map(s => s.trim()).filter(Boolean);
const mentionsRaw = process.env.PM2_SLACK_MENTIONS || "";
const mentions = mentionsRaw
.split(",")
.map(id => id.trim())
.filter(Boolean)
.map(id => `<@${id}>`)
.join(" ");
const filterRaw = process.env.PM2_SLACK_FILTER || "";
const filterList = filterRaw.split(",").map(s => s.trim()).filter(Boolean);
if (!globalSlackUrl) {
console.error("[pm2-slack-alerts] Missing Slack URL. Set environment variable PM2_SLACK_URL.\nUsage Example: pm2 set pm2-slack-alerts:PM2_SLACK_URL https://hooks.slack.com/services/XXXXX/YYYYY/ZZZZZ\n");
process.exit(1);
}
pm2_1.default.connect((err) => {
if (err) {
console.error("[pm2-slack-alerts] Failed to connect to PM2:", err);
process.exit(2);
}
pm2_1.default.launchBus((err, bus) => {
if (err) {
console.error("[pm2-slack-alerts] Failed to launch PM2 bus:", err);
process.exit(3);
}
const DEBOUNCE_MS = 2000;
const restartFlags = {};
const timers = {};
bus.on("process:event", (data) => {
const app = data.process?.name || "unknown-app";
const id = data.process?.pm_id;
const event = data.event;
if (id === undefined)
return;
if (eventFilterList.length && !eventFilterList.includes(event))
return;
if (filterList.length && !filterList.includes(app))
return;
if (!restartFlags[app]) {
restartFlags[app] = {
exited: new Set(),
online: new Set(),
};
}
if (event === "exit") {
restartFlags[app].exited.add(id);
if (timers[app])
clearTimeout(timers[app]);
timers[app] = setTimeout(() => {
const { exited, online } = restartFlags[app];
if (exited.size && online.size && online.size === exited.size) {
sendSlackMessage(app, "restarted", `${app} has been restarted (${online.size} instance${online.size > 1 ? "s" : ""})`, [...online]);
}
else if (exited.size && online.size === 0) {
sendSlackMessage(app, "stopped", `${app} has been stopped (${exited.size} instance${exited.size > 1 ? "s" : ""})`, [...exited]);
}
restartFlags[app] = { exited: new Set(), online: new Set() };
}, DEBOUNCE_MS);
}
else if (event === "online") {
restartFlags[app].online.add(id);
if (timers[app])
clearTimeout(timers[app]);
timers[app] = setTimeout(() => {
const { exited, online } = restartFlags[app];
if (exited.size && online.size && online.size === exited.size) {
sendSlackMessage(app, "restarted", `${app} has been restarted (${online.size} instance${online.size > 1 ? "s" : ""})`, [...online]);
}
else if (online.size && exited.size === 0) {
sendSlackMessage(app, "started", `${app} has been started (${online.size} instance${online.size > 1 ? "s" : ""})`, [...online]);
}
restartFlags[app] = { exited: new Set(), online: new Set() };
}, DEBOUNCE_MS);
}
else if (["errored", "stopped", "disconnected"].includes(event)) {
sendSlackMessage(app, event, `${app} is now ${event}`);
}
else if (["log", "error", "kill", "exception", "reload", "delete", "restart overlimit",].includes(event)) {
const debounceKey = `${app}-${event}`;
if (eventTimers[debounceKey])
return;
eventTimers[debounceKey] = setTimeout(() => {
delete eventTimers[debounceKey];
}, 2000);
sendSlackMessage(app, event, `${app} triggered ${event}`);
}
});
function sendSlackMessage(app, event, text, pids = []) {
pm2_1.default.describe(app, (err, descriptions) => {
if (err || !descriptions || !descriptions.length) {
console.error("[pm2-slack-alerts] Failed to get PM2 process description:", err);
return;
}
const desc = descriptions[0];
const monit = desc.monit || {};
const uptime = desc.pm2_env?.pm_uptime
? `${Math.floor((Date.now() - desc.pm2_env.pm_uptime) / 1000)}s`
: "N/A";
const memory = monit.memory ? `${(monit.memory / 1024 / 1024).toFixed(2)} MB` : "N/A";
const cpu = monit.cpu !== undefined ? `${monit.cpu}%` : "N/A";
const colorMap = {
restarted: "#4caf50",
started: "#2196f3",
stopped: "#f44336",
errored: "#e53935",
disconnected: "#ff9800",
exit: "#f44336",
log: "#607d8b",
error: "#d32f2f",
kill: "#9e9e9e",
exception: "#ab47bc",
reload: "#03a9f4",
delete: "#757575",
"restart overlimit": "#e91e63",
};
const emojiMap = {
restarted: "âģī¸",
started: "đ",
stopped: "đ",
errored: "â",
disconnected: "â ī¸",
exit: "đ",
log: "đ",
error: "đ",
kill: "â ī¸",
exception: "đĨ",
reload: "đ",
delete: "đī¸",
"restart overlimit": "đĢ",
};
const color = colorMap[event] || "#cccccc";
const emoji = emojiMap[event] || "âšī¸";
const now = new Date();
const timestamp = Math.floor(now.getTime() / 1000);
const formattedTime = now.toLocaleString("tr-TR", {
timeZone: "Europe/Istanbul",
hour12: false,
});
const pidLine = pids.length
? `đ§Š PIDs : ${pids.length} instance${pids.length > 1 ? "s" : ""} (${pids.join(", ")})`
: "";
const mentionsLine = mentions ? `đĨ Mentions : ${mentions}` : "";
const messageText = [
"```",
`đ Time : ${formattedTime}`,
`đĨ Host : ${hostname}`,
`đ Event : ${event}`,
`đ Mode : ${pids.length > 1 ? "cluster" : "fork"}`,
`đ Uptime : ${uptime}`,
`đž Memory : ${memory}`,
`âī¸ CPU : ${cpu}`,
pidLine,
mentionsLine,
"```",
].filter(Boolean).join("\n");
const slackUrlKey = `PM2_SLACK_URL_${app.replace(/-/g, "_").toUpperCase()}`;
const appSlackUrl = process.env[slackUrlKey] || globalSlackUrl;
axios_1.default.post(appSlackUrl, {
username: `PM2 Notify (${hostname})`,
attachments: [
{
fallback: `${app} ${event}`,
color,
title: `${emoji} SERVER STATUS UPDATED ${emoji}`,
text: "ââââââââââââââââââââââââââ\n" + ` *${app.toUpperCase()}* â *\`${event.toUpperCase()}\`* \n\n` + messageText,
ts: timestamp,
},
],
}).catch(console.error);
});
}
});
});