喂!我是 Wei

Front-End Engineer

Be a Problem Solver.

⌘K

導覽

所有文章緣起互動小功能

文章分類

目錄
環境變數準備取得 Discord Webhook URLHeartbeat 實作崩潰告警實作整合進主流程新增 /status 指令(即時查詢)Bot 重啟後的「上線通知」實際崩潰場景演示整體監控架構小結

相關文章

Discord Bot 串接 Groq:打造高速 AI 對話助理

2026年4月1日

Discord Bot 排程任務:node-cron 做每日公告與資料重置

2026年3月31日

Discord Bot Autocomplete:Slash Command 即時搜尋實戰

2026年3月30日

最新文章
全部 →
前端 CI/CD 與正式環境除錯:從 Pull Request 到事故排查
2026-06-24
即時資料怎麼選?Polling、SSE、WebSocket 比較
2026-06-23
前端系統設計:如何拆元件、資料流與大型專案架構?
2026-06-22
無障礙不是加 ARIA:語意化 HTML、鍵盤操作與焦點管理
2026-06-21
CSS 與 RWD 面試整理:Flexbox、Grid、定位與層疊脈絡
2026-06-19
← 返回文章列表

Discord Bot 監控與告警:Bot 掛掉時自動發通知到頻道

2026年5月18日·約 8 分鐘閱讀·
Discord.jsBotNode.js監控Webhook

Bot 部署到伺服器後,有一個問題你遲早會遇到:Bot 掛掉了,但你不知道。

用戶在頻道輸入指令沒有反應,截圖傳過來問你「Bot 是不是壞了」,你這時才打開 SSH 進去看 log。這個流程本身就是問題。

這篇實作兩層監控機制:

  1. Heartbeat:Bot 還活著時,定時主動發訊息回報狀態
  2. 崩潰告警:Bot 要掛掉的瞬間,先把通知推到頻道再結束

兩個機制互補——heartbeat 確認「Bot 是活的」,告警負責「崩潰當下的即時通知」。


環境變數準備

.env
# 既有設定
BOT_TOKEN=你的BotToken
GUILD_ID=你的伺服器ID
LOG_CHANNEL_ID=系統日誌頻道ID
 
# 本篇新增
MONITOR_CHANNEL_ID=監控頻道ID
ALERT_WEBHOOK_URL=Discord Webhook URL
HEARTBEAT_INTERVAL_MS=300000

MONITOR_CHANNEL_ID 建議開一個只有管理員看得到的私人頻道,不要用公開頻道。


取得 Discord Webhook URL

告警要用 Webhook 而不是 Bot client,原因是:Bot 崩潰時 client 可能已經無法正常運作,Webhook 是一個獨立的 HTTP endpoint,不依賴 Bot 的連線狀態。

取得步驟:

  1. 進入目標頻道 → 「編輯頻道」
  2. 整合 → Webhook → 建立 Webhook
  3. 複製 Webhook URL,貼入 .env

Webhook URL 格式:https://discord.com/api/webhooks/{id}/{token}


Heartbeat 實作

Heartbeat 的概念:Bot 每隔固定時間,把目前狀態推到監控頻道。如果管理員超過預期時間沒看到最新一筆心跳,就代表 Bot 可能已經停止運作。

monitor.js
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 已經壞掉也能送出。

monitor.js(接上)
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 部署提到的外部重啟機制的配合。


整合進主流程

index.js(節錄)
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("監控與排程已啟動");
});

順序很重要:

  1. setupCrashAlert() 先掛,確保後續任何初始化錯誤都能被攔截並告警
  2. 其他初始化(指令、排程)在中間執行
  3. startHeartbeat() 最後才啟動,避免初始化還沒完成就發心跳

新增 /status 指令(即時查詢)

除了定時心跳,也可以加一個 /status 指令讓管理員隨時手動查詢 Bot 狀態。

index.js(節錄)
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 重啟後自動發「已恢復上線」也很有用,讓管理員確認重啟成功。

monitor.js(接上)
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") 裡呼叫:

index.js(節錄)
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 頻道直接掌握。

分享:XLinkedIn
← 上一篇Discord Bot 串接 Groq:打造高速 AI 對話助理
下一篇 →從 jQuery 到 TypeScript:打造轉盤抽獎元件