Claude CodeでPrometheusのアラートルールを書き、promtoolで検証する
Claude Codeにアラートルールを書かせるとき、promtool check rulesとtest rulesを検証コマンドにして反復する手順です。テストの書き方、hookでの自動実行、CLAUDE.mdの規約例まで扱います。
アラートルールは「書く」より「検証コマンドを渡す」で品質が決まる
Claude CodeにPrometheusのアラートルールを書かせるときは、先にpromtool check rulesとpromtool test rulesを実行できる状態にします。この2つが通るまでYAMLを直させる流れにすれば、構文ミスやPromQLの取り違えをレビュー前に潰せます。
アラートルールは、PromQLの式が1つ以上のベクトル要素を返した時点で「active」になる仕組みです。式が空のままなら、何も起きません。つまり「書いたつもりのアラートが鳴らない」失敗は、構文が正しくても起こります。目で読んで気づきにくいので、時間を進めて確かめるテストが効きます。
この記事で作るのは、5xxエラー比率のアラートルール1本と、それを検証するテストファイルです。PromQLの探索そのものをClaudeに任せたい場合は、ClaudeとPrometheusの連携でPromQLを書かずにメトリクスを聞く手順が別の入口になります。通知先の設計はGrafana側の話で、mcp-grafanaでアラートルールを作成し通知経路を調べる手順に分かれています。ここで扱うのは、Prometheusのルールファイルとそのテストです。
検証の入口になる2つのコマンド
promtoolには、ルールを検証するサブコマンドが2つあります。
2つのコマンドの役割
promtool check rules
ルールファイルが有効かどうかを調べます。引数のルールファイルを省くと標準入力から読みます。既定のlintは
duplicate-rulesで、--lint=noneで無効化、--lint-fatalでlintエラーを終了コード3にできます。promtool test rules
テストファイルを渡して、ルールの挙動を時間を進めながら検証します。
--runで名前が正規表現に合うテストグループだけを実行し、--debugでデバッグ出力、--diffで期待値と実際の差分を色付きで出します。--diffは実験的な扱いです。
checkは「読み込めるか」、testは「意図どおりに鳴るか」を見ます。Claudeに渡す完了条件は、両方が終了コード0で終わることです。
promtool check rules rules/*.rules.yml
promtool test rules rules/*.test.ymlファイルを分ける: ルールとテストを同じディレクトリに置く
テストファイルはrule_filesで読み込むルールファイルを指定します。公式の例は、ルールファイルをテストファイルと同じディレクトリに置いて実行する形です。この記事もそれに合わせ、拡張子で役割を分けます。
rules/
api.rules.yml # アラートルール本体
api.test.yml # promtool test rules に渡すテストcheck rulesのglobには*.rules.ymlだけ、test rulesには*.test.ymlだけを渡せます。形式の違うテストファイルをルール検証に混ぜずに済みます。
Claudeにアラートルールを書かせる
指示は「条件」「持続時間」「ラベル」「確認方法」を一度に渡します。例えば次のように頼みます。
rules/api.rules.yml に ApiHighErrorRate を追加して。
job ごとに、5xx の比率が 5 分間で 5% を超えたら 10 分後に発火する。
severity は page。書いたら rules/api.test.yml に、発火する場合と
しない場合のテストを足し、promtool check rules と test rules が
両方通るまで直して。Claudeが書いたルールは、例えば次のような形になります(公式のルール記法に沿った例です)。
groups:
- name: api
rules:
- alert: ApiHighErrorRate
expr: |
sum by (job) (rate(http_requests_total{code=~"5.."}[5m]))
/ sum by (job) (rate(http_requests_total[5m])) > 0.05
for: 10m
labels:
severity: page
annotations:
summary: "{{ $labels.job }} の 5xx 比率が高い"forは、条件が真になってから発火扱いにするまで待つ時間です。待っている間の要素は「pending」状態で、forを省くと最初の評価でactiveになります。keep_firing_forを足すと、条件が外れてもその時間だけ発火を維持でき、フラッピングの抑制に使えます。
テストファイルの書き方
テストは「入力の時系列」「評価する時刻」「期待するアラート」の3点でできています。次のapi.test.ymlは、上のルールを検証します。
rule_files:
- api.rules.yml
evaluation_interval: 1m
tests:
- name: 5xx が 10% で 15 分続くと発火する
interval: 1m
input_series:
- series: 'http_requests_total{job="api", code="200"}'
values: '0+90x30'
- series: 'http_requests_total{job="api", code="500"}'
values: '0+10x30'
alert_rule_test:
- eval_time: 5m
alertname: ApiHighErrorRate
exp_alerts: []
- eval_time: 15m
alertname: ApiHighErrorRate
exp_alerts:
- exp_labels:
severity: page
job: api
exp_annotations:
summary: "api の 5xx 比率が高い"
promql_expr_test:
- expr: http_requests_total{code="500"}
eval_time: 10m
exp_samples:
- labels: 'http_requests_total{job="api", code="500"}'
value: 100
- name: 5xx が 1% なら発火しない
interval: 1m
input_series:
- series: 'http_requests_total{job="api", code="200"}'
values: '0+99x30'
- series: 'http_requests_total{job="api", code="500"}'
values: '0+1x30'
alert_rule_test:
- eval_time: 15m
alertname: ApiHighErrorRate
exp_alerts: []要点は4つあります。
- 展開記法:
0+90x30は「0から始めて、90ずつ30回増やす」の意味で、31個のサンプルになります。1x4は1+0x4の省略で、同じ値が続きます - 欠損と失効:
_は取りこぼした1回分、staleはstaleなサンプルを表します。1 _x3 staleは1 _ _ _ staleに展開されます - 発火しないことの検証: 発火しないはずの時刻は、
alertnameとeval_timeを書いてexp_alertsを空にします - 期待ラベル:
exp_labelsには、アラートのラベルに加えて、元のサンプルのラベルも入ります。/alertsに出るものと同じ見え方で、__name__とalertnameは含みません
pendingとfiringの違いをテストで確かめる
exp_alertsに書くのは、指定した時刻に発火しているアラートです。上のテストでeval_time: 5mが空なのは、for: 10mが経過しておらずpendingのままだからです。15mでは10分以上条件が真なので、発火側に載ります。
eval_timeで変わる期待値
5m
条件は真でもfor: 10mが経過していません。exp_alertsは空になります。
15m
条件が10分以上続いています。exp_labelsとexp_annotationsを書いて期待します。
時刻の扱いにも注意点があります。テスト中はtime()やday_of_*()が一定の値を返し、既定の開始時刻は0(Unixエポック)です。eval_timeはstart_timestampからの相対時間で、start_timestampにはUnixタイムスタンプかRFC3339形式を渡せます。曜日や時間帯で条件が変わるルールは、start_timestamp: "2021-01-01T00:00:00Z"のように日付を固定して書きます。
失敗を読ませて直させる反復
テストが落ちたら、出力をそのままClaudeに読ませます。差分が欲しいときは、--runで1つのテストグループに絞り、--diffを付けます。
promtool test rules --run "5xx が 10%" --diff rules/api.test.yml--runに渡すのは、テストグループ名に合わせる正規表現です。
直させるときは「ルールとテストのどちらが間違っているか」を先に切り分けさせると、テストを期待値に合わせて書き換えてしまう失敗を避けられます。指示の例を挙げます。
test が落ちた。まず、入力の時系列から eval_time の時点で
式が何を返すかを手計算して、ルールとテストのどちらが
誤っているかを説明して。テストの期待値は、理由を説明するまで
変えないで。同じalertnameを複数のルールが共有する場合は注意が必要です。Prometheusは同名のアラートを別々のルールに持てるため、テストでは、そのalertnameで発火している全アラートの和集合を、1つのalert_rule_testに並べて書きます。
PostToolUse hookで保存のたびに検証する
指示に「通るまで直して」と書いても、実行を忘れる回があります。Claude CodeのPostToolUse hookを使うと、ルールやテストを編集するたびに検証を走らせられます。Edit|Writeのmatcherで、ファイル編集の直後だけ動かす構成です。
hookの組み込み手順
- 1
検証スクリプトを置く
.claude/hooks/promtool-check.shを作り、編集したファイルが対象のときだけpromtoolを実行します。 - 2
settings.jsonに登録する
PostToolUseにEdit|Writeのmatcherでスクリプトを登録します。パスは$CLAUDE_PROJECT_DIRから組み立てます。 - 3
失敗を終了コード2で返す
検証が落ちたら、出力をstderrに書いて終了コード2で終わります。
スクリプトは次のとおりです。hookにはJSONがstdinで渡され、編集対象のパスはtool_input.file_pathにあります。
#!/bin/bash
# .claude/hooks/promtool-check.sh
f=$(jq -r '.tool_input.file_path')
case "$f" in
*.rules.yml|*.test.yml) ;;
*) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR" || exit 0
if ! out=$({ promtool check rules rules/*.rules.yml \
&& promtool test rules rules/*.test.yml; } 2>&1); then
echo "$out" >&2
exit 2
fi{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/promtool-check.sh"
}
]
}
]
}
}PostToolUseでは、終了コード2を返すとstderrがClaudeに届きます。ツールはすでに実行済みなので編集は取り消されませんが、失敗の内容を見たClaudeが続けて直せます。終了コード0のときはstderrがClaudeに見えないため、失敗は必ず2で返します。スクリプトに実行権限(chmod +x)を付けるのも忘れないでください。
テストを走らせる方式の比較は、Claude Code hooksでテストを自動実行するにあります。PostToolUseの入力JSONの詳細はPostToolUse hookの解説が受け持ちます。
CLAUDE.mdに書いておく規約
hookを入れない環境や、人が編集するときのために、完了条件をCLAUDE.mdにも残します。毎回の指示で繰り返している内容が、書く価値のある項目です。
## Prometheus ルール
- ルールは `rules/*.rules.yml`、テストは同じ階層の `*.test.yml`
- ルールを変えたら、必ず次を実行して両方通すこと
- `promtool check rules rules/*.rules.yml`
- `promtool test rules rules/*.test.yml`
- 新しいアラートには、発火する場合としない場合の両方のテストを書く
- `for` を持つアラートは、`for` の前後で eval_time を2点取る
- テストが落ちたとき、原因を説明する前に期待値を書き換えないつまずきやすい点
落とし穴
式が空になるとアラートは鳴らない
アラートは式がベクトル要素を返したときにactiveになります。メトリクスそのものが消えると、比率の式は空になり、何も発火しません。「エクスポーターが落ちた」ことまで拾いたい場合は、up == 0のような別のルールを足します。テストで入力時系列を空にして、期待どおり静かか、別ルールが拾うかを確かめます。
落とし穴
期待値のラベル不足でテストが落ちる
exp_labelsは、ルールのlabelsだけでなく、アラートの元になったサンプルのラベルも含みます。sum by (job)で集約したならjobが残り、集約しなければinstanceなども残ります。ルールの形を変えたら、期待ラベルも見直します。
落とし穴
テスト用の独自項目で未知フィールドエラーになる
テストファイルやルールファイルに独自のメタデータを足したいときは、--ignore-unknown-fieldsが使えます。ただし、Prometheus本体は未知のフィールドを厳格に検査するため、本番に読み込ませる前に取り除く必要があります。
浮動小数点の比較で期待値が数ビットずれる場合は、テストファイルのfuzzy_compare: trueで、仮数部の最後のビット程度の差を無視できます。rateを含む式の期待値を厳密に書けないときの選択肢です。
まとめ
アラートルールの品質は、Claudeに何を書かせたかより、何で検証させたかで決まります。promtool check rulesで形を、promtool test rulesで鳴り方を確かめ、forの前後と「鳴らない場合」を必ずテストに入れる。この3点をhookとCLAUDE.mdに固定しておけば、ルールの追加や閾値の変更を、同じ検証の枠に乗せたまま繰り返せます。