技術ログ

Discord webhookで個人開発の異常検知基盤を作る

公開: 2026-05-19 · 著者: Sasaki Ryuji

個人開発でモニタリング基盤を簡素に作るなら、Discord webhookが優れたコスパです

Discord webhookで個人開発の異常検知基盤を作る

あなたが個人開発でSaaSと複数のWorkerを並走させているなら、異常検知基盤を最初から組んでおくべきです。私はGramShift VPSと6つの自動化Worker、複数のWindowsタスクの異常通知をすべて1つのDiscordチャネル #gramshift-alerts に集約して運用しています。月コストはゼロ、実装も1日で済みます。この記事では、運用1年の中で踏み抜いた具体的な落とし穴と、それを踏まえた現行設計を共有します。

なぜSlackやemailではなくDiscordか

個人開発のアラート基盤としてDiscordを選んだ理由は3つあります。第一に、Discord webhookは無料で、レート制限がSlackより緩い (1秒に5メッセージまで)。第二に、スマホアプリの通知が即届くため、本業中でも異常に気づける。第三に、過去のアラート履歴が消えずに永続的に残るため、再現性の低いバグを後で追跡できます。Slackの無料プランはメッセージ保存期間に制限があるため、長期運用には向きませんでした。

最小実装はNode.js関数1個

Discord webhookの最小実装はこれだけです。Discordのチャネル設定からwebhook URLを取得し、環境変数 DISCORD_WEBHOOK_URL に保存しておきます。

// lib/discord-notify.mjs
import fetch from 'node-fetch';

const WEBHOOK = process.env.DISCORD_WEBHOOK_URL;

export async function notify(level, message, extra = {}) {
 if (!WEBHOOK) return;
 const emoji = { info: 'ℹ', warn: '⚠', error: '🚨' }[level] || 'ℹ';
 const content = `${emoji} **[${level.toUpperCase()}]** ${message}`;
 const embed = Object.keys(extra).length ? {
 color: { info: 0x3b82f6, warn: 0xf59e0b, error: 0xef4444 }[level],
 fields: Object.entries(extra).map(([name, value]) => ({
 name, value: String(value).slice(0, 1024), inline: false
 }))
 } : null;

 await fetch(WEBHOOK, {
 method: 'POST',
 headers: { 'Content-Type': 'application/json' },
 body: JSON.stringify({ content, embeds: embed ? [embed] : [] })
 }).catch(e => console.error('Discord notify failed:', e.message));
}

使い方は単純です。await notify('error', 'Stripe webhook failed', { event_id: id, error: e.message }) のように呼び出します。アプリ本体を絶対に止めないために .catch でエラーを必ず受けます。これを忘れて await のまま try-catch の外に書くと、Discord側が500を返したときにメインの処理ごと落ちるので注意が必要です。

レート制限と「アラート嵐」対策 - notifyOnceの実装

個人開発の運用で最も困るのは「同じエラーが大量に発生してアラートが100通届く」状況です。Stripeの仕様変更でWebhookシークレットの検証が一時的に失敗したとき、1分間で47回、その後さらに53回のアラートがDiscordに飛んだことがあります。Discord webhook自体には1秒5メッセージのレート制限がありますが、それ以前にユーザー (自分) の通知体験を壊します。これを防ぐため、同一エラーキーの抑制ロジックを早い段階で導入しました。

const recentAlerts = new Map(); // key -> last sent timestamp

export async function notifyOnce(level, message, key, extra = {}) {
 const now = Date.now();
 const last = recentAlerts.get(key) || 0;
 if (now - last < 5 * 60 * 1000) return; // 同じkeyは5分に1回まで
 recentAlerts.set(key, now);
 await notify(level, message, extra);
}

例えば notifyOnce('error', 'Stripe webhook failed', 'stripe_webhook_fail', {...}) のように呼べば、同じkeyのアラートは5分間に1通だけ発火します。これだけで「100通の嵐」が翌月は0件になりました。同じバグでもエラーキーを変えれば別カウントになるため、「Stripe失敗」「DB接続失敗」「Discord失敗」は独立して通知されます。

注意点として、プロセス再起動でMapがリセットされるため、pm2が頻繁に落ちる環境では抑制が効きません。本格的にやるならRedisかSQLiteに永続化すべきですが、個人開発レベルなら気にしなくて構いません。再起動自体が異常なので、その時はアラートが多少飛んでも正しい挙動です。

サイレント時間設計 - 深夜のアラートを抑制

同一キー抑制を入れても、初発の1回目は通ります。重大障害ならそれでいいのですが、warnレベルの通知は深夜には不要です。私は深夜帯 (23時から7時) のアラートを翌朝7時のサマリーにまとめる設計にしています。エラー自体は記録するけれど、Discord通知は朝に集約する形です。

function isQuietHour() {
 const h = new Date().getHours();
 return h >= 23 || h < 7;
}

export async function notifySmart(level, message, extra) {
 if (isQuietHour() && level !== 'error') {
 appendToMorningSummary(level, message, extra);
 return;
 }
 await notify(level, message, extra);
}

ルールはシンプルです。errorは即時送る、warnとinfoは朝7時のまとめに溜める。完全にサイレントにすると重大障害を見逃すリスクがあるため、エラーレベルだけは例外にしています。実運用では月4-5回ほど深夜のerrorアラートで飛び起きますが、Stripe決済の障害などは即座に対応すべきものなので、この設計が現状の最適解だと判断しています。

pm2との組み合わせ運用

GramShift VPSのNode.jsプロセスはpm2で管理しています。ecosystem.config.jsmax_memory_restart: '500M' を設定し、メモリリーク時に自動再起動。再起動イベントを pm2-logrotate とカスタムスクリプトで拾って、Discordに送る構成です。

運用1年で月3-5回のプロセス再起動が記録されています。多くはWorker側でPlaywright起動回数が増えた日にメモリが膨らみ、OOM Killer発動前にpm2が予防再起動するパターンです。Discord通知を見て翌朝ログを掘ると、特定の処理パスで無限ループが発生していたなど、放置すれば顧客に影響が出る系のバグが見つかります。深夜には鳴らない設定ですが、朝のサマリーで必ず気づけます。

1チャネル運用の利点

初期は #stripe-alerts、#worker-alerts、#vps-alerts と複数チャネルに分けていました。3ヶ月運用した結果、すべて #gramshift-alerts 1本に統合する設計に落ち着きました。複数チャネルに分けると「重要な通知を別チャネルで見逃す」リスクが上がります。1チャネルでも notifyOnce とサイレント時間で十分整理されるので、運用負荷は最小化できます。

現在の運用は、朝7時にDiscordを開いて、infoレベルの通知 (前日の自走バッチ完了通知) が5-7件並んでいれば「すべて正常」と判断、warn/errorが混じっていればログを確認する、というシンプルなフローです。月コストはゼロ、DatadogやSentryの契約も現状は不要です。個人開発SaaSの監視基盤としては、これで十分な品質を保てています。

個人開発の運用パターンはSaaSビジネスカテゴリ、技術実装は技術ログに他にも蓄積しています。