この記事は約 5 分で読めます。
筆者プロフィール: ソフトウェアエンジニア。「知った気にならない。いつまでも学び続ける」を信条に、業務と個人開発の両輪で技術を磨いています。AI 駆動開発で複数の個人開発アプリを構築・運用中。
👉 ポートフォリオ: 筆者ホームページ
cron 監視を「失敗検知」だけで済ませると、「cron 自体が登録されていない / 削除された / サーバが死んだ」 ケースで何も検知できません。本記事では、運用中の SaaS 「たすきば Knowledge Relay」 で採用した 2 段構え監視 を整理します。
サービスの機能紹介・画面イメージ・コンセプトは公式プロダクトページをご覧ください。
👉 たすきば Knowledge Relay — 公式プロダクトページ
失敗検知だけの落とし穴
通常、cron 監視と聞くと:
cron 実行 → 失敗したら通知
という設計を考えます。しかし、これだと 致命的な盲点 があります。
実例シナリオ
1. cron-job.org に登録した cron が、何らかの理由で削除された (アカウントロック等)
2. cron が一切実行されない
3. 失敗ステータスすら記録されない (= 通知も来ない)
4. 月末になって「月次集計が動いていなかった」と気づく
これはたすきばで実際に踏んだ罠。failure 検知では何も気づけませんでした。
1. 解決 — 2 段構え監視
段 1: failure 検知
cron が実行されて失敗を返したら検知。
// cron 実行のたびに記録
await prisma.cronExecutionLog.create({
data: {
cronName: 'monthly-billing-report',
startedAt,
finishedAt: new Date(),
status: success ? 'success' : 'failure',
errorMessage: success ? null : error.message,
},
});
// 監視 cron で failure を検知
const failures = await prisma.cronExecutionLog.findMany({
where: { status: 'failure', detectedAt: null },
});
if (failures.length > 0) {
await notifySuperAdmin({ type: 'cron_failure', details: failures });
}
段 2: 「N 時間記録なし」検知
cron が一切実行されていない場合を検知。
const expectedCrons = [
{ name: 'daily-drift-check', maxIntervalHours: 24 },
{ name: 'monthly-billing-report', maxIntervalHours: 24 * 31 },
{ name: 'warm-up-ping', maxIntervalHours: 1 },
];
for (const cron of expectedCrons) {
const lastExecution = await prisma.cronExecutionLog.findFirst({
where: { cronName: cron.name },
orderBy: { finishedAt: 'desc' },
});
const hoursAgo = lastExecution
? (Date.now() - lastExecution.finishedAt.getTime()) / 3600000
: Infinity;
if (hoursAgo > cron.maxIntervalHours) {
await notifySuperAdmin({
type: 'cron_silence',
cronName: cron.name,
lastSeenHoursAgo: hoursAgo,
});
}
}
「期待される実行頻度」を maxIntervalHours として定義し、それを超えたら死亡疑いとして通知 します。
2. 監視 cron 自体が死んだら?
ここでメタな問題が発生します。
監視 cron 自体が死んだら、どうやって検知するか?
たすきばの答え:
1. 監視 cron は最も信頼性の高いプラットフォーム (cron-job.org) で稼働
2. 監視 cron は別 endpoint で死活監視 (GitHub Actions schedule で 1 日 1 回 ping)
3. GitHub Actions も死んだら? → そこは諦める (現実的な妥協点)
完全な死活監視は無理。ただし、段数を増やすほど死亡確率は指数的に下がる ので、3 層あれば十分。
3. expectedCrons リストを Code で管理
監視対象の cron 一覧は、コードで管理します。
// src/config/cron-watchdog.ts
export const EXPECTED_CRONS = [
{
name: 'daily-drift-check',
maxIntervalHours: 24,
description: '日次 drift 検知',
},
{
name: 'monthly-billing-report',
maxIntervalHours: 24 * 31,
description: '月次請求レポート',
},
{
name: 'embedding-backfill',
maxIntervalHours: 24 * 31,
description: '月初 embedding 補完',
},
];
| 運用 | 内容 |
|---|---|
| 新規 cron 追加時 | この配列に登録 |
| 登録漏れ | 「監視されない cron」になる |
| PR レビュー | 必須確認項目にする |
4. 通知を Discord + Email で多層化
「通知が出ても誰も見ていない」状態を防ぐため、通知は Discord と Email の 両方 に送ります。
async function notifySuperAdmin(payload: NotificationPayload) {
await Promise.all([
notifyDiscord(payload),
notifyEmail(payload, process.env.SUPER_ADMIN_EMAIL!),
]);
}
| シナリオ | フォロー |
|---|---|
| Discord を見ていないとき | メールで気づく |
| メールが埋もれたとき | Discord で気づく |
個人開発で「通知を見落とす」リスクを減らす 多層防御 です。
5. CronExecutionLog のスキーマ
cron 実行ログには cronName を必須カラムにします。
model CronExecutionLog {
id String @id @default(uuid())
cronName String // 必須
startedAt DateTime
finishedAt DateTime
status String // 'success' | 'failure'
errorMessage String?
detectedAt DateTime?
@@index([cronName, finishedAt])
}
これにより:
- ある cron がいつ最後に動いたか即座に分かる
- 監視テストで「この cron が今日 1 回動いたか」を verify できる
監視を仕組み化するには、データ構造から逆算 します。
6. 横展開できる場面
このパターンは cron に限らず、
| 対象 | 監視内容 |
|---|---|
| バッチ処理 | 期待される実行頻度を超えたら通知 |
| 定期メール送信 | N 時間以上送信記録がなければ通知 |
| 外部 API ping | 連続失敗 + 「全く動いていない」を別々に監視 |
「定期的に何かが起きるはず」 の処理に広く適用できます。
おわりに — 失敗検知と無音検知は別物
「失敗検知」と「無音検知」は別物。
サービスを運用するときは、
| カテゴリ | 意味 |
|---|---|
| 失敗 | 動いたが結果が NG |
| 無音 | そもそも動かなかった |
の 2 つを区別して監視する必要があります。
「動かなかった」を検知するには、「何時間以内に動くべきか」を事前に定義し、それを超えたら通知する仕組み が必要です。
| 仕組み | 効果 |
|---|---|
| failure 検知 (cron 実行ログ) | 実行されて失敗したケースを検知 |
| 「N 時間記録なし」検知 | cron 自体が止まったケースを検知 |
| expectedCrons リストで Code 管理 | 新規 cron の登録漏れを防ぐ |
| 通知を Discord + Email | 見落とし防止 |
個人開発で「気づいたら何ヶ月も動いていなかった」を防ぐための、最小限の防御策 です。
本記事の cron 監視は、運用中の SaaS 「たすきば Knowledge Relay」 で実装しています。
👉 たすきば Knowledge Relay — 公式プロダクトページ