Prometheus に追加されたルールのユニットテストの機能でアラートルールとレコーディングルールのテストを試してみました。この記事の執筆時点の最新バージョン Prometheus v2.4.3 にはまだこの機能が入っていないので、master ブランチの Docker イメージを利用しています。リリース前なので仕様変更が入る可能性があるのでご注意ください。
背景
以前より Prometheus のアラートルールのユニットテストは issue (#1695)で要望があがっていました。Google Summer of Code 2018 向けのプロジェクトアイデアとしても挙げられており、Ganesh Vernekarさんがこの実装を行いました。
promtool test
コマンド
アラートルールのユニットテストは promtool
という Prometheus のユーティリティコマンドに test rules
サブコマンドとして実装されています。
promtool は Promethus の Docker イメージに含まれているので、それを使ってコマンドを確認してみます。デフォルトの Entrypoint が prometheus
バイナリになっているので、/bin/sh
に変更してシェルを実行しその中で確認しています。ユニットテストの定義ファイルはローカルのものを使いたいのでカレントディレクトリをマウントしています。
# master の最新バージョンを pull する
$ docker pull prom/prometheus:master
# Prometheus のイメージでシェルを実行
$ docker run -it -v $PWD:/work -w /work --entrypoint /bin/sh prom/prometheus:master
以下の操作はコンテナ内のシェルでの実行になります。基本的に promtool test rules
にユニットテストの定義を書いた yaml ファイルを引数で渡すだけです。ここではテスト定義のファイルは別途カレントディレクトリに用意しています。
テスト実行(成功)
test.yaml はユニットテストの定義ファイルです(後述)。テストに成功すると SUCCESS と表示され戻り値が 0 で返ります。
/work $ promtool test rules test.yaml
Unit Testing: test.yaml
SUCCESS
# 戻り値は 0
/work $ echo $?
0
テスト実行(失敗)
テストに失敗した場合は FAILED と表示され、期待値(exp) と実際の値(got) が表示されます。戻り値は 1 になります。
/work $ promtool test rules failed.yaml
Unit Testing: failed.yaml
FAILED:
alertname:NodeNotReady, time:10m0s,
exp:"[Labels:{alertname=\"NodeNotReady\", condition=\"Ready\", node=\"mynode\", severity=\"notice\", status=\"true\"} Annotations:{text=\"node mynode is not ready for more than 10 minutes.\"}]",
got:"[Labels:{alertname=\"NodeNotReady\", condition=\"Ready\", node=\"othernode\", severity=\"notice\", status=\"true\"} Annotations:{text=\"node othernode is not ready for more than 10 minutes.\"}]"
# 戻り値は 1
/work $ echo $?
1
ヘルプ
# promtool test のヘルプを実行
/work $ promtool test --help
usage: promtool test <command> [<args> ...]
Unit testing.
Flags:
-h, --help Show context-sensitive help (also try --help-long and --help-man).
--version Show application version.
Subcommands:
test rules <test-rule-file>...
Unit tests for rules.
アラートルールのテスト定義
ユニットテストを定義を行ってみます。まずユニットテストの対象となるアラートルールのファイルを用意します。ここでは、Kubernetes でノードが 10分間 Ready にならない場合のアラートルール (KubeNotReady
) を例にテストしてみます。ファイル名は alerts.yaml
としています。
groups:
- name: kube-state-metrics
rules:
- alert: NodeNotReady
expr: kube_node_status_condition{condition="Ready",status="true"} == 0
for: 10m
labels:
severity: notice
annotations:
text: node {{ $labels.node }} is not ready for more than 10 minutes.
次にユニットテストの定義を記載します。想定する timeseries の入力 (input_series
)には特殊な展開記法を使うことができます。(後述)
# テスト対象のルールファイルを指定します
rule_files:
- alerts.yaml
# テストケースを記載します
tests:
- interval: 1m # input series の values の timeseries を記録する間隔
# 想定する入力の timeseries を定義します
input_series:
- series: 'kube_node_status_condition{condition="Ready",status="true", node="mynode"}'
values: 0+0x15 # 0 の値を 15 回 interval の間隔で繰り返す
# テストケースを定義します
alert_rule_test:
- eval_time: 10m # 評価するタイミング (テスト起動時を起点とした時間)
alertname: NodeNotReady # アラート名
exp_alerts: # 期待するアラートの状態
- exp_labels: # 期待するアラートのラベル。すべて記載する
severity: notice
node: mynode
condition: Ready
status: true
exp_annotations: # 期待するアラートのアノテーション
text: "node mynode is not ready for more than 10 minutes."
コンテナ内でテストを実行してみます。テストが通ったことが確認できます
/work $ promtool test rules test.yaml
Unit Testing: test.yaml
SUCCESS
試しに input_series の value を 1+0x15
に変えて、テストが通らなくなることを確認します。
# アラートの条件を満たさないのでアラートが上がらない
/work $ promtool test rules test.yaml
Unit Testing: test.yaml
FAILED:
alertname:NodeNotReady, time:10m0s,
exp:"[Labels:{alertname=\"NodeNotReady\", condition=\"Ready\", node=\"mynode\", severity=\"notice\", status=\"true\"} Annotations:{text=\"node mynode is not ready for more than 10 minutes.\"}]",
got:"[]"
試してみての感想
- いくつか既存のアラートのテストを書いてみたところ、複雑な PromQL はやはりテストがあると安心できました
- 特に他人が書いたアラートだとテストを書くことで意味が理解しやすくなりました
- アラートが上がらない境界値のテストをどうやるか
- 現状
exp_alerts: []
とするとアラートが上がらない場合と一致しますが仕様なのかは不明でした
- 現状
レコーディングルールのテスト定義
アラートルールと同じような方法でレコーディングもテストできます。ここではノードの idle でない CPU 時間を集計するレコーディングルールを例にテストしてみます。ファイル名は recording.yaml
としています。
groups:
- name: example-node-exporter-rules
rules:
# CPU in use by CPU.
- record: instance_cpu:node_cpu_seconds_not_idle:rate5m
expr: sum(rate(node_cpu_seconds_total{mode!="idle"}[5m])) without (mode)
アラートルールでは alert_rule_test
でアラートの期待値を定義しましたが、レコーディングルールでは promql_expr_test
で PromQL を使ってテストします。
rule_files:
- recording.yaml # レコーディングルールのファイル
tests:
- interval: 1m
# 想定する入力の timeseries を定義します
input_series:
- series: 'node_cpu_seconds_total{mode="system", cpu="0"}'
values: 0+20x10
- series: 'node_cpu_seconds_total{mode="user", cpu="0"}'
values: 0+40x10
- series: 'node_cpu_seconds_total{mode="system", cpu="1"}'
values: 0+10x10
- series: 'node_cpu_seconds_total{mode="user", cpu="1"}'
values: 0+20x10
# PromQL で値をテストします
promql_expr_test:
- eval_time: 10m
expr: 'instance_cpu:node_cpu_seconds_not_idle:rate5m' # PromQL
exp_samples: # 期待するする結果
- labels: 'instance_cpu:node_cpu_seconds_not_idle:rate5m{cpu="0"}'
value: 1
- labels: 'instance_cpu:node_cpu_seconds_not_idle:rate5m{cpu="1"}'
value: 0.5
テストが通ることを確認します。
/work/recoding $ promtool test rules recording-test.yaml
Unit Testing: recording-test.yaml
SUCCESS
試しに input_series の value を変えて、テストが通らなくなることを確認します。
/work/recoding $ promtool test rules recording-test.yaml
Unit Testing: recording-test.yaml
FAILED:
expr:'instance_cpu:node_cpu_seconds_not_idle:rate5m', time:10m0s,
exp:"{__name__=\"instance_cpu:node_cpu_seconds_not_idle:rate5m\", cpu=\"0\"} 1E+00, {__name__=\"instance_cpu:node_cpu_seconds_not_idle:rate5m\", cpu=\"0\"} 1E+00, {__name__=\"instance_cpu:node_cpu_seconds_not_idle:rate5m\", cpu=\"1\"} 5E-01",
got:"{__name__=\"instance_cpu:node_cpu_seconds_not_idle:rate5m\", cpu=\"0\"} 2.3333333333333335E+00, {__name__=\"instance_cpu:node_cpu_seconds_not_idle:rate5m\", cpu=\"0\"} 2.3333333333333335E+00, {__name__=\"instance_cpu:node_cpu_seconds_not_idle:rate5m\", cpu=\"1\"} 5E-01"
試してみての感想
- レコーディングルールの場合、どのように記録されるかがテストの期待値として記載できわかりやすかったです
input_series の表記方法
input_series
の values
は値を直接記載する表記と、展開記法を使う表記の 2つの表記方法があります。
直接表記
下記のように values に数値を並べる方式です。数値は interval の間隔で timeseries が記録されます (scrape 間隔に近いイメージ)。下記の例だと、最初は 1 分間隔で 0
が 5回 、その後 1分間隔で 1
が 5回と 10 分間のメトリクスの値となります。
- interval: 1m
input_series:
- series: 'kube_node_status_condition{condition="Ready",status="true", node="mynode"}'
values: 0 0 0 0 0 1 1 1 1 1
展開記法
同じ値もしくは一定の値で増加していく場合の値を展開記法で表記できます。前述の値は次のように 0+0x5
と 1+0x5
の 2つで表現することができます。
- interval: 1m
input_series:
- series: 'kube_node_status_condition{condition="Ready",status="true", node="mynode"}'
values: 0+0x5 1+0x5
ドキュメント に記載がありますが、下記のような表記になっています。増加値は counter タイプのメトリクスに便利です。
a = 初期値、b = 増加値、 c = 回数 とすると下記のように展開されます (x は繰り返しの意味)
'a+bxc' : 'a a+b a+(2*b) a+(3*b) … a+(c*b)'
'a-bxc' : 'a a-b a-(2*b) a-(3*b) … a-(c*b)'
例:
'-2+4x3' : '-2 2 6 10'
'1-2x4' : '1 -1 -3 -5 -7'
0+0x5 の場合増加値が 0 なので: '0 0 0 0 0' と 0 が5回になります
参考: PromQL のテスト用 DSL
実装の PR Unit testing for rules by codesome · Pull Request #4350 · prometheus/prometheus · GitHub を見る、ユニットテストの機能はコア部分の unittest.go
が 500 行以下とコンパクトに実装されています。
これは Prometheus の内部で使われている PromQL 用 DSL の仕組み (promql/test.go) を使っているからです。1+0x10
などの独特の記法もこの DSL の表記の仕方に由来しています。Prometheus で使われている テスト用 DSLは PromQL のテストの仕方の参考になるかと思います。
PromQL のテスト用 DSL の例 (increase()
関数のテストの一部)
# Tests for increase().
load 5m
http_requests{path="/foo"} 0+10x10
http_requests{path="/bar"} 0+10x5 0+10x5
# Tests for increase().
eval instant at 50m increase(http_requests[50m])
{path="/foo"} 100
{path="/bar"} 90
まとめ
-
promtool test rules
でアラートルール、レコーディングルールのテストが実行できるようになりました - PromQL のテスト用 DSL がベースとなっています
- メトリクスの想定入力値を書いて、それに対して期待するアラートもしくは PromQL の結果をテストできます
- 複雑なアラートやレコーディングルールはテストを書いてみると安心感があります