Bot 部署到伺服器後,有一個問題你遲早會遇到:Bot 掛掉了,但你不知道。
用戶在頻道輸入指令沒有反應,截圖傳過來問你「Bot 是不是壞了」,你這時才打開 SSH 進去看 log。這個流程本身就是問題。
這篇實作兩層監控機制:
- Heartbeat:Bot 還活著時,定時主動發訊息回報狀態
- 崩潰告警:Bot 要掛掉的瞬間,先把通知推到頻道再結束
兩個機制互補——heartbeat 確認「Bot 是活的」,告警負責「崩潰當下的即時通知」。
環境變數準備
# 既有設定
BOT_TOKEN=你的BotToken
GUILD_ID=你的伺服器ID
LOG_CHANNEL_ID=系統日誌頻道ID
# 本篇新增
MONITOR_CHANNEL_ID=監控頻道ID
ALERT_WEBHOOK_URL=Discord Webhook URL
HEARTBEAT_INTERVAL_MS=300000MONITOR_CHANNEL_ID 建議開一個只有管理員看得到的私人頻道,不要用公開頻道。
取得 Discord Webhook URL
告警要用 Webhook 而不是 Bot client,原因是:Bot 崩潰時 client 可能已經無法正常運作,Webhook 是一個獨立的 HTTP endpoint,不依賴 Bot 的連線狀態。
取得步驟:
- 進入目標頻道 → 「編輯頻道」
- 整合 → Webhook → 建立 Webhook
- 複製 Webhook URL,貼入
.env
Webhook URL 格式:https://discord.com/api/webhooks/{id}/{token}
Heartbeat 實作
Heartbeat 的概念:Bot 每隔固定時間,把目前狀態推到監控頻道。如果管理員超過預期時間沒看到最新一筆心跳,就代表 Bot 可能已經停止運作。
import { EmbedBuilder } from "discord.js";
let heartbeatTimer = null;
function buildHeartbeatEmbed(client) {
const uptime = process.uptime();
const hours = Math.floor(uptime / 3600);
const minutes = Math.floor((uptime % 3600) / 60);
const seconds = Math.floor(uptime % 60);
const uptimeStr = `${hours}h ${minutes}m ${seconds}s`;
const memUsage = process.memoryUsage();
const heapUsedMB = (memUsage.heapUsed / 1024 / 1024).toFixed(1);
const heapTotalMB = (memUsage.heapTotal / 1024 / 1024).toFixed(1);
return new EmbedBuilder()
.setColor(0x57f287) // 綠色代表正常
.setTitle("💚 Bot 狀態心跳")
.addFields(
{ name: "Bot 名稱", value: client.user?.tag ?? "未知", inline: true },
{ name: "持續運行時間", value: uptimeStr, inline: true },
{ name: "Heap 使用量", value: `${heapUsedMB} / ${heapTotalMB} MB`, inline: true },
{ name: "WebSocket 延遲", value: `${client.ws.ping} ms`, inline: true },
{ name: "已連接的伺服器數", value: `${client.guilds.cache.size}`, inline: true }
)
.setTimestamp()
.setFooter({ text: "下次心跳將在 5 分鐘後更新" });
}
export function startHeartbeat(client) {
const intervalMs = Number(process.env.HEARTBEAT_INTERVAL_MS) || 300_000;
// 啟動時立即發一次,確認 Bot 上線
sendHeartbeat(client);
heartbeatTimer = setInterval(() => sendHeartbeat(client), intervalMs);
}
export function stopHeartbeat() {
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
}
async function sendHeartbeat(client) {
const channelId = process.env.MONITOR_CHANNEL_ID;
if (!channelId) return;
try {
const channel = await client.channels.fetch(channelId);
if (!channel?.isTextBased()) return;
await channel.send({ embeds: [buildHeartbeatEmbed(client)] });
} catch (err) {
// 發心跳失敗不應讓 Bot 崩潰,只記 log
console.error("[Monitor] 心跳發送失敗", err);
}
}心跳 embed 包含五項關鍵數字:
- 持續運行時間:確認 Bot 是否意外重啟過
- Heap 使用量:排查記憶體洩漏
- WebSocket 延遲:Discord 連線品質
- 已連接伺服器數:確認 Bot 沒有被移除
崩潰告警實作
崩潰告警的關鍵:要在 unhandledRejection 和 uncaughtException 觸發時,先把通知送出去,才讓程式結束。
這裡用 fetch 直接打 Webhook URL,不透過 discord.js client,確保就算 client 已經壞掉也能送出。
const WEBHOOK_URL = process.env.ALERT_WEBHOOK_URL;
async function sendAlertWebhook(title, description, errorDetail = "") {
if (!WEBHOOK_URL) return;
const payload = {
embeds: [
{
color: 0xed4245, // 紅色代表告警
title: `🚨 ${title}`,
description,
fields: errorDetail
? [{ name: "錯誤詳細", value: `\`\`\`\n${errorDetail.slice(0, 1000)}\n\`\`\`` }]
: [],
timestamp: new Date().toISOString(),
},
],
};
try {
await fetch(WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
} catch (err) {
// Webhook 也失敗了,只能記 log,不能再嘗試避免無限迴圈
console.error("[Monitor] 告警 Webhook 發送失敗", err);
}
}
export function setupCrashAlert(botTag = "Bot") {
process.on("unhandledRejection", async (reason) => {
const detail = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
console.error("[Monitor] unhandledRejection:", detail);
await sendAlertWebhook(
"未處理的 Promise 拒絕",
`**${botTag}** 發生未捕獲的 Promise 錯誤,即將嘗試重啟。`,
detail
);
});
process.on("uncaughtException", async (err) => {
console.error("[Monitor] uncaughtException:", err);
await sendAlertWebhook(
"未捕獲的例外錯誤",
`**${botTag}** 發生嚴重錯誤,即將結束程序。`,
err.stack ?? err.message
);
// 等待 Webhook 發送完成後再結束,確保通知送達
process.exit(1);
});
}uncaughtException 的 handler 裡加了 process.exit(1),讓 Docker --restart unless-stopped 接手重啟。這是上一篇 Docker 部署提到的外部重啟機制的配合。
整合進主流程
import { startHeartbeat, setupCrashAlert } from "./monitor.js";
client.once("ready", async () => {
console.log(`Bot 已上線:${client.user.tag}`);
// 崩潰告警要盡早掛上,放在 ready 最前面
setupCrashAlert(client.user.tag);
// 其他初始化...
await registerSlashCommands();
setupScheduler({ client, onDailyReset: resetDailyCheckIn });
// 最後啟動 heartbeat
startHeartbeat(client);
console.log("監控與排程已啟動");
});順序很重要:
setupCrashAlert()先掛,確保後續任何初始化錯誤都能被攔截並告警- 其他初始化(指令、排程)在中間執行
startHeartbeat()最後才啟動,避免初始化還沒完成就發心跳
新增 /status 指令(即時查詢)
除了定時心跳,也可以加一個 /status 指令讓管理員隨時手動查詢 Bot 狀態。
import { buildHeartbeatEmbed } from "./monitor.js"; // 需要把這個 function export 出來
// 在 COMMANDS 陣列中加入
{
name: "status",
description: "查詢 Bot 目前的運行狀態",
default_member_permissions: "0", // 只有管理員可見
}
// 在 interactionCreate 中加入
if (interaction.commandName === "status") {
// 重用 heartbeat 的 embed,保持格式一致
const embed = buildHeartbeatEmbed(client);
embed.setTitle("📊 Bot 狀態(手動查詢)");
embed.setFooter({ text: `由 ${interaction.user.tag} 查詢` });
await interaction.reply({ embeds: [embed], flags: 64 }); // ephemeral,只有查詢者看到
}Bot 重啟後的「上線通知」
除了崩潰告警,Bot 重啟後自動發「已恢復上線」也很有用,讓管理員確認重啟成功。
export async function sendOnlineNotice(botTag) {
if (!WEBHOOK_URL) return;
await fetch(WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
embeds: [
{
color: 0x57f287,
title: "✅ Bot 已恢復上線",
description: `**${botTag}** 已成功重新連線。`,
timestamp: new Date().toISOString(),
},
],
}),
}).catch((err) => console.error("[Monitor] 上線通知發送失敗", err));
}在 client.once("ready") 裡呼叫:
import { startHeartbeat, setupCrashAlert, sendOnlineNotice } from "./monitor.js";
client.once("ready", async () => {
setupCrashAlert(client.user.tag);
await sendOnlineNotice(client.user.tag); // 上線時主動通知
// ...
startHeartbeat(client);
});這樣每次 Bot 重啟(不論是手動重啟還是崩潰後自動恢復),頻道都會收到通知。
實際崩潰場景演示
模擬一個 uncaughtException:
// 觸發崩潰告警
setTimeout(() => {
throw new Error("測試崩潰告警");
}, 5000);5 秒後終端會印出錯誤訊息,同時 Discord 告警頻道收到紅色 Embed,Docker 的 --restart unless-stopped 接手重啟後,又會收到綠色的「已恢復上線」通知。
整體監控架構
Bot 運行中
├── startHeartbeat()
│ └── 每 N 分鐘 → 更新監控頻道的心跳 Embed(綠色)
│
└── setupCrashAlert()
├── unhandledRejection → sendAlertWebhook()(紅色)→ 繼續嘗試重啟
└── uncaughtException → sendAlertWebhook()(紅色)→ process.exit(1)
└── Docker 重啟
└── client.once("ready")
└── sendOnlineNotice()(綠色)
三種訊號對應三種顏色:
| 顏色 | 代表意義 |
|---|---|
| 💚 綠色 | Bot 正常運行中(heartbeat / 上線通知) |
| 🚨 紅色 | Bot 發生錯誤或崩潰(告警) |
小結
這篇實作的兩層監控:
- Heartbeat:Bot 每隔固定時間主動回報狀態,管理員能從更新時間判斷 Bot 是否存活,並掌握記憶體和延遲趨勢
- 崩潰告警 + Webhook:Bot 在 crash 前先把錯誤細節推到頻道,配合 Docker 重啟機制,讓整個恢復流程有跡可循
這兩個加上去後,Bot 的可觀測性(observability)就建立起來了——不只是「Bot 有沒有在」,而是「Bot 出了什麼問題、什麼時候出的、現在狀態如何」都能從 Discord 頻道直接掌握。