組織(org_id)ごとに独立して動作するマルチテナント監視機能。1つのバックグラウンドスレッドが全組織を巡回する。
R1組織ごとに Loki(ログ)/ Mimir(メトリクス)/ HTTPヘルスチェック / TCP疎通確認 を個別に設定・有効化できる(いずれか一つだけの部分設定も許容)。
R2ログ異常・メトリクス閾値超過・ヘルスチェック失敗を検知したら、AIが原因分析(異常検知/重大度/要約/推奨対応)を行い、メール通知 + ヘルプデスクチケットを起票する。
R3同一障害の再通知を防ぐため、フィンガープリントによる時間ベース重複排除と、AIによる意味的重複排除(同じ問題の言い換えを検知)の二段構えを持つ。
R41日1回、指定時刻以降に日次レポートメールを送信する(アラート件数・稼働率・メトリクス統計を集計)。送信失敗時は1時間ごとにリトライする。
R5アラートメール・デイリーレポートのHTMLテンプレートは組織ごとにAI生成 or 手動編集できる。出荷時の共通デフォルトは持たない(組織ごとにログ/エンドポイントが異なるため)。
R6管理者(Adminロール)のみが監視設定・シークレット(Loki/Mimirのパスワード等)にアクセスできる。シークレットはブラウザには絶対に返さない(設定有無のみ)。
R7管理者はUIから「即時チェック実行」「テストメール送信」「ライブ取得(保存前の疎通確認)」ができ、バックグラウンドの巡回を待たずに動作確認できる。
R8サーバーがユーザー指定URLへHTTPリクエストするため(ヘルスチェック)、SSRF対策としてプライベート/ループバックアドレスへの到達をブロックする。
R9保持期間はDB容量を抑えるため最大3日(DB_RETENTION_DAYS、上限3にクランプ)。3日ごとにクリーンアップジョブが古いレコードを削除する。
1バックグラウンドスレッド(AIMonitorThread)が60秒ごとにTickし、有効な全組織を巡回する。組織ごとに独立した間隔設定(POLL_INTERVAL 等)を持つ。
フロントエンド(MonitorView.tsx)は REST API 経由で読み書きするのみで、監視サイクル自体はバックエンドが常時バックグラウンドで実行している(UIを開いていなくても動く)。
run_check()(ログ + メトリクス閾値)Test ID対応(issue #54): C/E/F/Gいずれも判定そのものはバックエンド専用(フロントエンドから分岐を直接駆動する手段が無い)。しきい値(THRESHOLD_*)・dedup関連(DEDUP_TTL/LOG_CONTENT_DEDUP_TTL/SEMANTIC_DEDUP_ENABLED)はSettings画面の入力フォームが無く、JSONインポート経由でしか設定できない — Playwrightからは「JSONインポート」カード経由でのみ到達可能。カスタムルール(CustomAlertRules.tsx)/Lokiルール(LokiAlertRules.tsx)の追加UIはあるが、各ルールのenabledフラグはUIにトグルが無く、バックエンドも読んでいない可能性がある(死んだフィールドの疑い — #54の副次的な発見、別途確認)。
run_health_checks()Test ID対応(issue #54): monitor-health-check-now-button(HealthTab.tsx)は手動実行だが、記録のみ・アラート未発火(D/E/Fには到達しない)— Cの真偽そのものはmonitor-health-rowのdata-healthy属性で確認できる。D以降(実際のアラート化)は本番の巡回サイクルでしか通らず、手動トリガーの等価物が無い。monitor-health-not-configured-card/monitor-health-goto-settings-buttonもR1(部分設定)の分岐として付与済み。テストは全て未実装。
run_network_check()Test ID対応(issue #54): このフローに分岐(diamond)ノードは無い — 到達可否はmonitor-network-rowのdata-reachable属性、monitor-network-not-configured-card/monitor-network-goto-settings-buttonで未設定時のR1分岐を確認できる。テストは未実装。
run_daily_report()(毎時判定)Test ID対応(issue #54): A/BのTick判定はバックエンド専用(monitor_state.report_stateはUIに一切表示されない)。Cの成功/失敗自体は「Send Daily Report Now」ボタン(monitor-settings-send-daily-report-button、SettingsTab.tsx)経由で手動に再現可能 — ただしこれは日次Tickとは別の呼び出し経路(同じsend_daily_report()関数を呼ぶが、TickのA/B判定は経由しない)。失敗時はmonitor-settings-daily-report-error-banner、テンプレート未設定が原因の場合はmonitor-settings-open-templates-tab-buttonも表示される(§3-Eとも関連)。テストは未実装。
_fire_alert()Test ID対応(issue #54): A/B/Cはバックエンド専用、対応するdedup関連の設定値もSettings画面に入力欄が無い(JSONインポートのみ)。Dはこの仕様書自体が古かった箇所 — 「両テンプレート」という文言はissue #34より前の実装を指しており、現在はalert_email.html1つだけが条件(修正済み、2026-07-28)。D/D1/Fの分岐結果は、Alertsタブの各行(monitor-alert-rowのdata-has-ticket属性)とmonitor-alert-ticket-id(チケット番号が表示されるかどうか)で観測可能 — Templatesタブ(TemplatesTab.tsx)でテンプレートを未設定/設定した状態を作り、実際にアラートを発火させてdata-has-ticketの違いを確認するテストが書ける。テストは未実装。
Prisma管理(prisma/schema.prisma)。全テーブルが org_id → organizations(id) のFKを持つ(2026-07 DDLレビューで追加)。日時カラムは全て TIMESTAMPTZ。
CREATE TABLE monitor_alerts (
id SERIAL PRIMARY KEY,
org_id INTEGER NOT NULL REFERENCES organizations(id),
tenant TEXT NOT NULL,
severity TEXT NOT NULL, -- low | medium | high | critical
title TEXT NOT NULL,
summary TEXT,
ticket_id TEXT, -- ヘルプデスクチケットID(起票失敗時はNULL)
fingerprint TEXT, -- 重複排除キー: md5(...)[:12]
created_at TIMESTAMPTZ
);
CREATE INDEX ON monitor_alerts (org_id, created_at);
CREATE INDEX ON monitor_alerts (org_id, severity);
CREATE TABLE monitor_metrics (
id SERIAL PRIMARY KEY,
org_id INTEGER NOT NULL REFERENCES organizations(id),
tenant TEXT NOT NULL,
metric_name TEXT NOT NULL, -- cpu_usage_pct | memory_usage_pct | disk_usage_pct | ...
value DOUBLE PRECISION NOT NULL,
recorded_at TIMESTAMPTZ
);
CREATE INDEX ON monitor_metrics (org_id, recorded_at);
CREATE TABLE monitor_network_checks (
id SERIAL PRIMARY KEY,
org_id INTEGER NOT NULL REFERENCES organizations(id),
host TEXT NOT NULL,
reachable BOOLEAN NOT NULL,
latency_ms DOUBLE PRECISION,
checked_at TIMESTAMPTZ
);
CREATE INDEX ON monitor_network_checks (org_id, checked_at);
CREATE TABLE monitor_station_status (
id SERIAL PRIMARY KEY,
org_id INTEGER NOT NULL REFERENCES organizations(id),
station_id TEXT NOT NULL,
is_online BOOLEAN NOT NULL,
checked_at TIMESTAMPTZ
);
CREATE INDEX ON monitor_station_status (org_id, checked_at);
現状バックエンドに書き込みロジックなし(将来の充電ステーション等デバイス監視用に予約済みのテーブル)。
CREATE TABLE monitor_health_checks (
id SERIAL PRIMARY KEY,
org_id INTEGER NOT NULL REFERENCES organizations(id),
app TEXT NOT NULL, -- HEALTH_CHECKS[].name
healthy BOOLEAN NOT NULL,
status_code INTEGER,
checked_at TIMESTAMPTZ,
body TEXT -- レスポンス本文(先頭4000字) — デイリーレポートの
-- {{ avg("App.json.path") }} 集計に使われる
);
CREATE INDEX ON monitor_health_checks (org_id, checked_at);
CREATE TABLE monitor_state (
org_id INTEGER PRIMARY KEY REFERENCES organizations(id),
seen_fingerprints JSONB NOT NULL DEFAULT '{}', -- { fingerprint: unix_timestamp }
report_state JSONB NOT NULL DEFAULT '{}' -- { last_sent, pending_date,
-- scheduled_time, retry_count, first_attempt }
);
組織1件につき1行(1対1)。旧 state/seen_fingerprints.json ファイルの後継。
専用テーブルではなく organizations テーブルの1カラム(JSONB、フラットなキー文字列の辞書)に格納。キー一覧は §7。シークレット(LOKI_PASS, MIMIR_PASS)もここに平文で入る(読み出し時にAPI層でマスクする設計。DB自体は暗号化していない)。
全エンドポイント Depends(auth.current_user) で認証必須。設定・テンプレート・アクション系はさらに _require_monitor_admin() で Admin ロール必須(§9)。ベースパス /api/monitor。
| メソッド/パス | 認可 | 説明 | Request / Response 概要 |
|---|---|---|---|
GET /status |
ログイン済 | 設定状況のサマリ(シークレットなし) | {configured, enabled, tenants[], loki_url, mimir_url, ai_enabled, poll_interval_minutes, health_checks[], templates_configured, daily_report_template_configured} |
GET /alerts |
ログイン済 | 本日のアラート一覧(monitor_alerts) |
Response: 配列。org未所属は空配列 |
GET /metrics |
ログイン済 | 本日のメトリクス統計(tenant×metric_nameでavg/min/max集計) | Response: [{tenant, metric_name, avg_val, min_val, max_val, samples}] |
GET /network |
ログイン済 | 本日のTCP疎通履歴 | Response: [{host, reachable, latency_ms, checked_at}] |
GET /health-history |
ログイン済 | 本日のHTTPヘルスチェック履歴 | Response: [{app, healthy, status_code, checked_at}] |
GET /settings |
Admin | 組織の監視設定を取得(シークレットは "******" or "" にマスク) |
Response: MONITOR_SETTING_KEYS の全キーを含むフラット辞書(§7) |
PUT /settings |
Admin | 監視設定を保存 | Request: {settings: {...}}。値が "******" のキーは既存シークレットを維持。保存前に _validate_monitor_settings() でCRLF注入・SSRF・重複名をチェック |
POST /send-daily-report |
Admin | デイリーレポートをテスト送信(自分宛て) | テンプレート未設定時は 400 {code:"TEMPLATE_NOT_CONFIGURED", template_type, message} |
GET /templates?template_type= |
ログイン済 | 保存済みテンプレートHTML取得 | template_type: alert_email |
GET /templates/fields?template_type= |
ログイン済 | そのテンプレートで使えるプレースホルダー一覧(動的: 設定済みヘルスチェック名等を反映) | Response: {template_type, groups:[{...}]} |
PUT /templates |
Admin | テンプレートHTMLを保存(Storage経由) | Request: {template_type, content} |
POST /templates/generate?lang= |
Admin | AIでテンプレートHTML生成/調整 | Request: {template_type, prompt, mode: "create"|"update", current_content?}。生成後に厳密検証→NG時は最大2回自己修復(§6)。Response: {template_type, content, warnings:[]} |
POST /templates/preview |
ログイン済 | テンプレートをサンプルデータでレンダリングし、実送信時に壊れる式を警告 | Request: {template_type, content}。Response: {html, warnings:[]} |
POST /trigger 202 |
Admin | 即時に1サイクル分のポーリングを実行(実アラート/実チケット/実メールが発生しうる) | BackgroundTasksで非同期実行。Response: {status:"triggered"} |
POST /test-email |
Admin | サンプルアラートメールを自分宛てに送信(SMTP疎通確認) | Response: {sent: true, to} |
POST /fetch-live |
Admin | Loki/Mimirから今すぐ生データ取得(保存もアラートもしない、読み取り専用) | Request(任意): {settings:{...}}(未保存のフォーム値を一時マージして疎通確認)。Response: {metrics[], logs[], tenants[], sources:{mimir,loki}} |
POST /check-now |
Admin | ネットワーク/ヘルスチェックを今すぐ実行(保存もアラートもしない) | Request: {kind?: "network"|"health"}(省略時は両方)。Response: {network[], network_configured, health[], health_configured} |
POST /test-connection |
Admin | Loki/Mimirへの接続テスト(保存前のフォーム値でも可) | Response: {results: {...}} |
fingerprint(md5ハッシュ、アラート種別ごとに生成規則が異なる)を monitor_state.seen_fingerprints に {fp: unix_timestamp} で記録。DEDUP_TTL(既定3600秒)以内の再出現は抑制。ステートは DEDUP_TTL × 24 秒より古いものを毎サイクル刈り取り(prune_state)。LOG_CONTENT_DEDUP_TTL(既定300秒)キャッシュし、同一内容なら分析自体をスキップ。SEMANTIC_DEDUP_ENABLED) — 新規アラートを本日の既存アラート(最大20件)と比較。まずタイトルから数値を除去した正規化文字列で完全一致チェック(_keyword_duplicate、無料)。一致しなければ1回だけOpenAIに「同じ問題か」を YES/NO で判定させる(monitor_duplicate_check プロンプト)。MAX_DAILY_TICKETS(既定20)を超えたら以降のアラートはログのみで抑制。HIGH / CRITICAL の2閾値(組織ごとに設定可能、既定 80/90, 85/95, 80/90)。CUSTOM_ALERT_RULES(metric, operator(>,>=,<,<=,==), value, severity, label)を評価。md5(tenant + metric_key + severity) — 同じテナント・メトリクス・重大度が続く限り再通知しない。[namespace/pod] line[:250] 形式)後、OpenAI JSON modeへ(monitor_analyze プロンプト、temperature=0.1)。{anomaly_detected, severity, title, summary, affected_components, recommended_action}。anomaly_detected=false なら握りつぶして終了(アラート化しない)。None を返し、監視ループは異常なしとして継続(AI障害で監視全体が止まらない設計)。プレビュー(POST /templates/preview)も同じ検証を通し、warnings[] として画面にオレンジ色のバナーで表示する。以前はプレビューが常に成功する甘いスタブだったため、実送信で初めて壊れていることが発覚するという問題があった。
date.today() - 1日)の monitor_alerts / monitor_network_checks / monitor_health_checks / monitor_metrics を集計。avg()/sum()/min()/max()(ヘルスチェックJSONパス集計)、count_eq()/rate_eq()、health_rate()、latency_avg/min/max()、metric_avg/min/max() のヘルパー関数を提供。report_state に retry_count を記録し、翌時のTickで再試行(最大回数制限なし、成功するまで1時間おき)。organizations.monitor_settings(JSONB)に格納されるキー。GET/PUT /api/monitor/settings で読み書きする MONITOR_SETTING_KEYS。
| キー | 既定値 | 説明 |
|---|---|---|
LOKI_URL / LOKI_USER / LOKI_PASS* |
— | Lokiエンドポイントと認証情報 |
LOKI_TENANT_ID / LOKI_QUERY |
— | テナント上書き(カンマ区切り)、カスタムLogQL(未設定時は自動検出) |
MIMIR_URL / MIMIR_USER / MIMIR_PASS* |
— | Mimirエンドポイントと認証情報 |
MIMIR_TENANT_ID |
— | テナント上書き(カンマ区切り) |
TENANT_IDS |
[] | 共通テナントIDリスト(Loki/Mimir個別上書きが無い場合のフォールバック) |
HEALTH_CHECKS |
[] | JSON配列: {name,url,expected?,auth_type,auth_username?,auth_password?,auth_token?} |
NETWORK_CHECK_HOSTS |
[] | TCP疎通確認対象(カンマ区切り) |
POLL_INTERVAL |
5分 | ログ/メトリクスポーリング間隔 |
HEALTH_CHECK_INTERVAL / NETWORK_CHECK_INTERVAL |
1分 | 各チェックの実行間隔 |
DAILY_REPORT_TIME |
08:00 | 日次レポートを送信する最短時刻(HH:MM) |
ALERT_RECIPIENT_ROLES |
[] | このロールを持つ組織メンバー全員にアラートメールを送る |
ALERT_EMAIL_LIST_IDS |
[] | Email Lists機能のリストIDを宛先に追加 |
ALERT_EXTRA_EMAILS |
[] | 追加の固定宛先アドレス |
ENABLED |
true | "false"で組織全体の監視を停止 |
THRESHOLD_CPU_HIGH/CRITICAL |
80/90 | CPU使用率閾値(%) |
THRESHOLD_MEM_HIGH/CRITICAL |
85/95 | メモリ使用率閾値(%) |
THRESHOLD_DISK_HIGH/CRITICAL |
80/90 | ディスク使用率閾値(%) |
CUSTOM_ALERT_RULES |
[] | 任意メトリクスの追加しきい値ルール |
LOKI_ALERT_RULES |
[] | ログ本文への正規表現マッチルール |
* LOKI_PASS / MIMIR_PASS は MONITOR_SECRET_KEYS — API応答では "******"(設定済み)または ""(未設定)にマスクされる。SMTPは組織別ではなくサーバー環境変数(SMTP_HOST 等)で一元管理。
#monitor/{alerts|metrics|health|network|settings|templates} の6タブ構成。共通ヘッダー(3枚のサマリーカード: Infrastructure Endpoints / HTTP Health Checks / Alert Destinations)は全タブで共通表示。

本日のアラートをカード一覧表示(重大度バッジ + タイトル + 要約 + テナント + 時刻)。空の場合は設定タブへの導線を表示。

tenant × component(cpu/memory/disk usage等)のテーブル(avg/min/max/samples)。「Fetch & view latest」でライブ取得と保存済み履歴を切替表示。

サマリー(最終チェック時刻・healthy/unhealthy件数・保存済みエンドポイント数)+ 過去24時間のサーバー稼働状況グリッド(緑=Available/赤=Outage、アプリごとに1行)+ 個別チェック結果テーブル。「Check Now」で即時実行。

TCP疎通統計(到達/未到達件数)+ サーバー障害グラフ(Health Logsと同じ意匠)。「Check Now」で即時実行。

「Monitoring active」トグル、JSON設定のインポート/エクスポート、Infrastructure Endpoints(Loki/Mimir設定)、HTTPヘルスチェック編集、アラート宛先(ロール/リスト/固定アドレス)、カスタムアラートルール、Loki正規表現ルールのセクションを縦に配置。「Save Settings」で一括保存。

左カラム: テンプレート選択(Alert Email / Daily Report)+ AIジェネレーター(プロンプト入力、Create new / Adjust current の切替)+ 利用可能フィールド一覧(クリックでエディタに挿入)。右カラム: HTML/Jinja2コードエディタ + プレビュー切替(実データに近いサンプルでレンダリング、壊れる式は警告バナー表示)。
sanitize.py)— ヘルスチェックURL・Loki/MimirURLについて、プライベート/ループバック/リンクローカルアドレスへの到達を保存時とfetch時の両方でブロック。DNS解決結果も検査(パブリックDNS名がプライベートIPを指すケースに対応)。_validate_monitor_settings)。SandboxedEnvironment でレンダリング(SSTI/RCE対策)。"Admin" in user.roles を要求。一般メンバーは /status・/alerts 等の閲覧系のみ。LOKI_PASS/MIMIR_PASS はAPI応答で常にマスク。保存APIは "******" を「変更なし」として扱い、既存値を上書きしない。X-Scope-OrgID ヘッダに使うテナントIDは safe_tenant() で検証してから送出。