自動売買システムのパイプラインが「exit 0 で終了しているのに、実は何も生成していない」——こうしたサイレント障害に気付くまで8日〜16日かかっていました。この問題を解決するために、5つの監視モジュールと78のテストケースで構成する「監視ハーネス」を設計・実装しました。検知遅延は24時間超から1時間以内に短縮されています。
サイレント障害はなぜ厄介か
一般的な障害はプロセスがクラッシュし、ログにエラーが残ります。ところが個人運用の自動化パイプラインでは、次のような「静かな故障」が起きます。
| インシデント | 検知までの日数 | 表面的な動作 |
|---|---|---|
| 環境変数未設定でスクリプトが即終了 | 16日 | exit 0、ログ正常 |
| SSD未マウントでパイプライン全停止 | 8日 | launchdジョブ自体は起動 |
| 上流パイプラインの出力消失 | 9日 | 下流は「入力なし」で正常終了 |
| ログは更新されているが entries=0 が連続 | 10営業日 | exit 0、ログに完了メッセージあり |
共通しているのは、プロセスの終了コードは0なのに、生成されるべきファイルやトレード結果が存在しないという点です。exit code を見ているだけでは永遠に気付けません。
設計方針:「生成物の存在」で判断する
監視の基本方針を「プロセスが動いたか」から「期待する生成物が存在するか」に切り替えました。
1従来: launchd → スクリプト実行 → exit 0 → 正常と判断2今回: launchd → スクリプト実行 → 生成物チェック → 鮮度・件数・内容を検証この方針を5つのモジュールに分解して実装しています。
5つの監視モジュール
全体アーキテクチャ
1monitoring_harness.py(エントリーポイント)2 │3 ├─ infra_monitor (FR-5: インフラ前提条件)4 │ SSD マウント / symlink / DB接続 / ディスク容量5 │6 ├─ pipeline_monitor (FR-1: 生成物監視)7 │ ディレクトリ存在 / ファイル数 / 鮮度 / ログパターン8 │9 ├─ business_monitor (FR-2: 業務メトリクス)10 │ entries=0 連続日数 / フォールバック検知11 │12 ├─ causal_tracker (FR-3: 因果追跡)13 │ 上流障害 → 下流アラート抑制14 │15 └─ alert_manager (FR-4: 通知制御)1 collapsed line
16 新規通知 / 復旧通知 / エスカレーション / 状態永続化FR-5: インフラ前提条件チェック
パイプライン全体が依存する「土台」を最初にチェックします。SSD未マウントやsymlink切れはCRITICALとして即通知・即終了です。
1infra:2 ssd_mount_path: "/Volumes/work2"3 tmp_symlink: "_tmp"4 db_check_enabled: true5 disk_warn_gb: 5.0CRITICALが検出された時点でパイプラインチェックをスキップします。土台が崩れている状態で下流のエラーを数えても意味がないためです。
FR-1: パイプライン生成物監視
各パイプラインをYAMLで定義し、生成物の存在・鮮度・件数をチェックします。
1pipelines:2 - name: fx_swing3 chain:4 - name: getFXStocks5 output_dir: _tmp/getFXStocks2calcFXAnalyticsDatas6 pattern: "*.parquet"7 min_active: 0 # 下流が即消費するため空が正常8 max_age_hours: 49 weekday_only: true # 土日は +48h 緩和10 schedule: hourlyここで重要なのが Consumed Pipeline パターン です。下流ジョブが上流の出力を即座に消費する場合、ディレクトリは空になります。これを「障害」と誤検知しないために、.bak ファイルの鮮度で判定します。
1Active ファイルあり → 鮮度チェック → OK or ERROR2Active ファイルなし → .bak ファイル確認3 ├─ .bak が新鮮 → OK(正常に消費された)4 └─ .bak が古い or なし → ERROR(パイプライン停止)FR-2: 業務メトリクス監視
exit 0 でもビジネス上の異常を検知します。具体的には2つのチェックです。
- entries=0 連続日数: ログから
entries=Nを抽出し、N=0 が3日連続で WARNING - フォールバック検知:
"SwingTrade.*フォールバック"パターンをログから検出したら ERROR
FR-3: パイプライン因果追跡
上流ジョブが障害を起こすと、下流ジョブも連鎖的にエラーになります。そのまま通知すると「getFXStocks ERROR」「calcFX ERROR」「simFX ERROR」と3件のアラートが飛びます。
因果追跡モジュールは、パイプラインのチェーン定義を使って根本原因だけを通知し、下流のアラートを抑制します。
1getFXStocks: ERROR ← 根本原因として通知(影響範囲を付記)2calcFX: ERROR ← 抑制(上流 'getFXStocks' の障害による影響)3simFX: ERROR ← 抑制FR-4: 通知制御と状態永続化
Slack通知のルールは4つです。
| 状態遷移 | 通知 | 理由 |
|---|---|---|
| 新規障害 | する | 即時対応のため |
| 継続障害 | しない | 同じ通知の繰り返しを防ぐ |
| 復旧 | する | 対応完了の確認 |
| エスカレーション(WARNING→ERROR) | する | 6時間放置されたWARNINGを昇格 |
アラート状態はJSONファイルに永続化されます。書き込みは tmp → rename のアトミック操作で、途中クラッシュによる破損を防いでいます。
1[監視ハーネス] 07:352🚨 [CRITICAL] infra:ssd_mount: /Volumes/work2 未マウント3❌ [ERROR] pipeline:fx_swing:getFXStocks: 最新ファイルが28h前 (閾値: 4h)4✅ [復旧] pipeline:js:getJQuantsStocks: 正常に復旧5⬆️ [エスカレーション WARNING→ERROR] infra:disk_space: 6h超過4つの実行モードとスケジュール
4つのlaunchdジョブで、時間帯ごとに監視範囲を変えています。
| モード | 実行時刻 | 監視対象 |
|---|---|---|
hourly | 毎時00分 | インフラ + FXパイプライン(hourlyスケジュールのみ) |
post-fx | 07:35 | インフラ + FX全ステージ + FX業務メトリクス |
post-js | 09:30 | インフラ + 日本株パイプライン + JS業務メトリクス |
post-us | 06:15 | インフラ + 米国株パイプライン + US業務メトリクス |
post-* モードは各市場のパイプライン完了後の時刻に設定しています。「結果が出ているはずの時刻に、結果を確認する」というシンプルな考え方です。
共通データ型:CheckResult
すべてのモジュールが返す統一データ型です。
1@dataclass(frozen=True)2class CheckResult:3 source: str # "pipeline:fx_swing:getFXStocks"4 severity: Severity # OK / INFO / WARNING / ERROR / CRITICAL5 message: str # 人間が読むメッセージ6 pipeline: str = "" # 因果追跡用7 stage: str = "" # 因果追跡用frozen=True にすることで、チェック結果が後から書き換えられることを防いでいます。因果追跡で結果を差し替える場合も、新しいインスタンスを生成します。
YAML駆動の設定管理
パイプラインの追加や閾値の変更はYAMLファイルの編集だけで完結します。コードを触る必要はありません。
1# 新しいパイプラインを追加する場合2pipelines:3 - name: new_pipeline4 chain:5 - name: step16 output_dir: _tmp/step1_output7 pattern: "*.csv"8 min_active: 19 max_age_hours: 2410 schedule: daily設定ファイルは config_loader.py が frozen dataclass に変換します。型安全性が担保され、設定ミスは起動時に即座にエラーになります。
78テストの設計
テストは8カテゴリ、78ケースです。
| カテゴリ | テスト数 | 主な検証内容 |
|---|---|---|
| Types | 3 | frozen dataclass、デフォルト値 |
| Config Loader | 8 | YAML解析、異常系(FileNotFound等) |
| Infra Monitor | 7 | SSD/symlink/DB/ディスク容量 |
| Pipeline Monitor | 18 | 正常/異常/境界値/Consumedパターン/週末緩和 |
| Business Monitor | 7 | entries追跡、フォールバック検知 |
| Causal Tracker | 7 | 上流障害→下流抑制、独立パイプライン |
| Alert Manager | 15 | 状態遷移、永続化、エスカレーション |
| E2E + Incident | 13 | 全モード結合テスト + 過去インシデント再現 |
特に重視したのが過去インシデント再現テストです。「SSD未マウント8日間放置」「exit 0 だがパイプライン停止」といった実際の事故シナリオを再現し、ハーネスが正しく検知できることを保証しています。
過去インシデントのカバレッジ
このハーネスが検知可能な過去の障害をまとめました。
| インシデント | 旧検知遅延 | 今回の検知メカニズム |
|---|---|---|
| 環境変数未設定で即終了 | 16日 | インフラチェック |
| SSD未マウント | 8日 | マウント状態確認(CRITICAL) |
| ログ未更新(symlink書き込み不可) | 8日 | ファイル鮮度チェック |
| exit 0 + パイプライン停止 | 不明 | 生成物カウント確認 |
| ログ正常だが entries=0 連続 | 10営業日 | 業務メトリクス監視 |
| 上流停止→下流正常終了 | 9日 | 因果追跡 + 生成物監視 |
すべてのケースで、検知遅延が1時間以内に短縮されます。
実装で得た知見
1. exit code は信用できない
最も重要な学びです。exit 0 は「プロセスが異常終了しなかった」を意味するだけで、「期待する処理が完了した」を意味しません。監視は必ず「生成物の存在」で判断すべきです。
2. Consumed Pipeline は専用ロジックが必要
下流ジョブが上流の出力を即座に消費するパイプラインでは、ディレクトリが空であることが「正常」です。一律に「ファイルがない=障害」と判定すると誤検知が多発します。.bak ファイルのタイムスタンプで鮮度を判定する設計にしています。
3. 因果追跡でアラート疲れを防ぐ
上流障害で下流も全部アラートになると、通知が大量に飛びます。障害対応中にノイズが増えるのは逆効果です。根本原因だけを通知し、影響範囲を付記する設計にしたことで、1回の障害で1通知に収まります。
4. 週末緩和は市場連動システムに必須
FX市場は土日閉場です。「金曜夜に生成されたファイルが日曜朝に古い」と判定されないよう、weekday_only: true のステージは土日に閾値を +48h 緩和しています。
まとめ
監視ハーネスは、「プロセスが動いたか」ではなく「生成物が意図通りに存在するか」を検証する仕組みです。5モジュール78テストという規模になりましたが、核となる設計方針はシンプルです。パイプラインの期待する出力をYAMLに定義し、実態と突き合わせる。一致しなければ通知する。
個人運用のシステムでは「誰も見ていない間に静かに壊れる」ことが最大のリスクです。このリスクを構造的に潰す仕組みとして、同様の課題を持つ方の参考になれば幸いです。