discovery・scrape・storage・query・rule を分ける。up=1 は scrape 成功でアプリ正常を意味しない。series 不在・scrape 失敗・アプリ失敗を区別する。
target の存在を先に確認
readiness と discovery を分けて確認する
curl -fsS http://127.0.0.1:9090/-/ready
各 request の URL を実入口へ揃えて変更し、必要な認証・TLS を指定する。
curl -fsS http://127.0.0.1:9090/api/v1/targets |
jq '.data.activeTargets[] | {labels,scrapeUrl,health,lastError,lastScrape}'
lastError で timeout・証明書・接続拒否・形式を分け、lastScrape の新しさも見る。
curl -fsS 'http://127.0.0.1:9090/api/v1/targets?state=dropped' |
jq '.data.droppedTargets[] | .discoveredLabels'
保持件数で省かれる場合があり、見つからなくても discovery・設定を確認する。
curl -fsS --get http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query=up' | jq .
最初に status、次に data.result を読む。実 job/instance で絞り、空配列を 0 と解釈しない。
| 結果 | 意味 | 確認 |
| up = 0 | target は存在、scrape 失敗 | lastError, timeout, DNS, TLS, auth, format |
| up = 1 | scrape 成功でもアプリ障害はあり得る | 業務 metric と利用経路の probe |
| up が空 | discovery・relabel・時間/label 不一致 | discovered/dropped target と selector |
Prometheus 側 network から scrapeUrl と metrics を確認する。exporter 待受けだけでは DNS・認証・CA は不明。relabel 後の実 label で query し ready を全 target の健康としない。
step は評価間隔で scrape 間隔ではない
単一時点・区間の評価を選ぶ
curl -fsS --get http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query=rate(http_requests_total[5m])' | jq .
実在する counter を使う。値は毎秒の増分で、期間内の総件数ではない。
curl -fsS --get http://127.0.0.1:9090/api/v1/query_range \
--data-urlencode 'query=rate(http_requests_total[5m])' \
--data-urlencode 'start=2026-09-10T00:00:00Z' \
--data-urlencode 'end=2026-09-10T01:00:00Z' \
--data-urlencode 'step=30s' | jq .
前の query と別方式。問題区間へ絞り、resultType・時刻・values・warnings/infos を読む。
data のある期間を選ぶ。URL encode で式を保護する。各 step に再評価し rate の5m は sample window。step を細かくしても元 sample は増えない。counter ごとの reset を扱うため rate 後に sum し、短すぎる window に注意。
図を準備しています
一つの label が series 数を倍増させる全独立組合せが存在する場合 series 数は label 値数の積。user ID 次元で route×method×status が桁違いに増える。
{"id":"ops-prom-cardinality","title":"一つの label が series 数を倍増させる","summary":"全独立組合せが存在する場合 series 数は label 値数の積。user ID 次元で route×method×status が桁違いに増える。","height":600,"html":"<main><p class=\"quiet\" data-i18n=\"scope\"></p><label class=\"range\" for=\"routes\"><span data-i18n=\"routes\"></span><output id=\"routesValue\"></output><input type=\"range\" id=\"routes\" min=\"1\" max=\"20\" value=\"5\"></label><label class=\"range\" for=\"users\"><span data-i18n=\"users\"></span><output id=\"usersValue\"></output><input type=\"range\" id=\"users\" min=\"1\" max=\"1000\" value=\"100\"></label><div class=\"checks\"><label><input id=\"include\" type=\"checkbox\" ><span data-i18n=\"include\"></span></label></div><div class=\"row code\" id=\"formula\"></div><section class=\"row\"><h3 data-i18n=\"series\"></h3><output class=\"large\" id=\"series\"></output><div class=\"bar\"><i id=\"fill\"></i></div><p class=\"quiet\">0 — 200,000</p></section><div class=\"explain\"><div class=\"stable \"><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"hint\"></p><p id=\"message\" aria-live=\"polite\"></p></div></div></main>\n","css":"main {\n display: grid;\n gap: 18px;\n min-width: 0;\n}\np {\n overflow-wrap: anywhere;\n}\n.controls {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: center;\n}\n.presets {\n display: flex;\n flex-wrap: wrap;\n}\n.presets button {\n flex: 1 1 85px;\n font-size: 15px;\n}\n.cards {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 10px;\n}\n.card {\n border: 1px solid var(--rule);\n border-radius: 10px;\n padding: 14px;\n min-width: 0;\n background: var(--surface);\n}\n.card h3 {\n font-size: 15px;\n color: var(--muted);\n margin-bottom: 8px;\n}\n.card strong {\n font-size: 23px;\n font-weight: 500;\n overflow-wrap: anywhere;\n}\n.code {\n font-family: var(--viz-font-mono);\n font-size: 15px;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.stable {\n display: grid;\n min-width: 0;\n}\n.stable > * {\n grid-area: 1/1;\n margin: 0;\n}\n.stable > .reserve {\n visibility: hidden;\n pointer-events: none;\n}\n.explain {\n border-left: 3px solid var(--accent);\n padding: 12px 14px;\n background: var(--accent-soft);\n}\n.arrow {\n text-align: center;\n color: var(--accent);\n font-size: 25px;\n}\n.row {\n padding: 12px;\n border: 1px solid var(--rule);\n border-radius: 8px;\n min-width: 0;\n}\n.row.on {\n border-color: var(--accent);\n background: var(--accent-soft);\n}\n.row.off {\n color: var(--muted);\n}\n.bar {\n height: 18px;\n background: var(--surface);\n border: 1px solid var(--rule);\n border-radius: 5px;\n overflow: hidden;\n margin-top: 8px;\n}\n.bar > i {\n display: block;\n height: 100%;\n background: var(--accent);\n width: 0;\n transition: width 0.22s;\n}\n.checks {\n display: grid;\n gap: 8px;\n}\n.checks label {\n display: flex;\n gap: 10px;\n align-items: center;\n padding: 9px;\n border: 1px solid var(--rule);\n border-radius: 7px;\n font-size: 16px;\n}\n.checks input {\n width: 18px;\n height: 18px;\n accent-color: var(--accent);\n flex-shrink: 0;\n}\nlabel.range {\n display: block;\n font-size: 16px;\n}\nlabel.range output {\n float: right;\n font-variant-numeric: tabular-nums;\n}\n.pair {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 10px;\n}\n.flow {\n display: grid;\n gap: 6px;\n}\n.quiet {\n font-size: 14px;\n color: var(--muted);\n}\n.badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 5px;\n background: var(--accent-soft);\n color: var(--accent);\n}\nbutton {\n overflow-wrap: anywhere;\n}\n.large {\n font-size: 32px;\n color: var(--accent);\n font-weight: 500;\n}\n.layers {\n display: grid;\n gap: 12px;\n}\n.layer {\n border-left: 3px solid var(--rule);\n padding-left: 12px;\n}\n.layer.on {\n border-color: var(--accent);\n}\n@media (max-width: 520px) {\n .cards,\n .pair {\n grid-template-columns: 1fr;\n }\n .card {\n padding: 12px;\n }\n .card strong {\n font-size: 22px;\n }\n}\n#formula {\n min-height: 5.3em;\n}\n","js":"const $ = (s) => document.querySelector(s);\nconst $$ = (s) => [...document.querySelectorAll(s)];\nconst t = (k) => viz.t(k);\nconst put = (id, v) => ($(\"#\" + id).textContent = v);\nconst say = (k) => put(\"message\", t(k));\nconst state = (v) => ($(\"main\").dataset.state = JSON.stringify(v));\nconst press = (key, v) =>\n $$(\"[data-\" + key + \"]\").forEach((b) =>\n b.setAttribute(\"aria-pressed\", String(b.dataset[key] === v)),\n );\nfunction draw() {\n const routes = +$(\"#routes\").value,\n users = +$(\"#users\").value,\n include = $(\"#include\").checked,\n series = routes * 2 * 5 * (include ? users : 1);\n put(\"routesValue\", routes);\n put(\"usersValue\", users);\n put(\n \"formula\",\n routes + \" routes × 2 methods × 5 status\" + (include ? \" × \" + users + \" users\" : \" × 1\"),\n );\n put(\"series\", viz.number(series, 0));\n $(\"#fill\").style.width = (series / 200000) * 100 + \"%\";\n state({ routes, users, include, series });\n}\n$$(\"input\").forEach((e) => {\n e.oninput = draw;\n e.onchange = draw;\n});\nsay(\"hint\");\ndraw();\n","audio":false,"strings":{"hint":"同じ request 数でも raw URL/user ID 分割で series が増える。recording rule は集計を追加し元 series は消さない。","include":"user_id label を追加","routes":"route 値数","scope":"通常 metric 一つの全組合せ推定。histogram bucket・複数 instance・疎な組合せ・過去 churn を省略。","series":"時系列数","users":"user_id 値数"}}
図は全組合せが存在する場合の数で実在数とは限らない。user/request ID や raw URL は無制限 churn を生む。有限 label と route template を使う。recording rule は新 series を書き、元の基数を減らさない。
file と稼働状態の両方を検証
配備前:使用中の file を特定して検証する
systemctl cat prometheus.service
実 config・rule path に合わせ、旧 file を保存し、対応版の promtool を使う。
promtool check config /etc/prometheus/prometheus.yml
成功後に配備する。実 scrape・認証の検証は別途必要。
promtool check rules /etc/prometheus/rules/*.yml
個別 rule 向けの確認で、同じ検証を機械的に繰返す必要はない。glob が実 file に一致すること。
promtool test rules /etc/prometheus/tests/alerts.test.yml
通常の rule でなく test 定義。入力系列と期待結果を用意し、閾値・for・回復・欠損を扱う。
配備後:signal・結果・時刻を確認する
sudo systemctl kill --signal=HUP --kill-whom=main prometheus.service
signal 送信成功と読込成功は別。操作時刻を記録して次を確認する。
journalctl -u prometheus.service --since "5 minutes ago" --no-pager
curl -fsS http://127.0.0.1:9090/api/v1/status/config | jq -r '.data.yaml'
curl -fsS --get http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query={__name__=~"prometheus_config_last_reload_successful|prometheus_config_last_reload_success_timestamp_seconds"}' | jq .
self-scrape と新しい取得が必要。1 と今回の時刻を確認する。失敗時は旧 file を戻し検証・reload 後、targets・rule も確認する。
実 path と主 process に合わせる。POST reload は lifecycle 有効時のみ。success と時刻を確認し、構文検査を全 runtime 検査としない。promtool test rules で閾値・for・回復を試す。
cardinality・保持・disk を先に調べる
容量の原因を特定する
curl -fsS http://127.0.0.1:9090/api/v1/status/flags | jq '.data'
以下は storage.tsdb.path に合わせ、コンテナなら実 volume も確認する。
curl -fsS http://127.0.0.1:9090/api/v1/status/tsdb | jq '.data'
同 instance の履歴・負荷と比較し、単一閾値で判断しない。
df -hT /var/lib/prometheus
journalctl -u prometheus.service --since "1 hour ago" --no-pager
有効化済み snapshot API で backup する
承認されアクセス制限された admin API と空き容量が前提。以下は削除ではなく snapshot 作成。
curl -fsS -X POST 'http://127.0.0.1:9090/api/v1/admin/tsdb/snapshot?skip_head=false' | jq .
data.name を記録し、data 下 snapshots/名前 を別保存先へ複写・検証する。同じ disk だけでは独立 backup にならず、後の書込は含まない。
互換版と独立 directory で復元し、実通知を無効にする。履歴 query・期間・系列を検証し、同じ保守時点の設定記録を保持する。
storage.tsdb.path と head series・churn・query cost・retention を確認する。百万 series を万能閾値にしない。size retention は全 directory の厳密上限でなく WAL/head 余裕も必要。まず増加源を直す。
snapshot・削除は admin API が前提。head を含める方針と別保存の設定・rule・認証を確認し隔離復元する。delete_series・tombstone・clean は別段階で全消去を修復策としない。通知は実 Alertmanager/Grafana 経路を調べる。
HTTP API · Querying basics · Metric practices · Storage · Rule testing