TT Lab
はじめる
学ぶ 学習パス コース

Grafana — ダッシュボードは問いだ

問い一つに答えるダッシュボードを作る

TT Labで続きを見る

目標

本物のGrafanaを起動して本物のPrometheusにつなぎ、質問1つに答えるダッシュボードをゼロから作ります。作り終えたら、それをプロビジョニングファイルとして固め、このGrafanaを消しても同じ画面を立て直せるようにします。

このラボは、画面がある最初のラボです。Grafanaをhttp://127.0.0.1:3000で起動すると、ターミナル上部のWebプレビューボタンで、実際のGrafana画面を開けます。パネルはその画面でクリックして作ってもAPIでアップロードしてもかまいません。採点ツールは、どちらの方法で作ったかを問わず、Grafanaに上がった結果だけを見ます。

なぜ重要なのか

ダッシュボードは絵ではなく、質問に答える道具です。そのため、このラボはパネルを描く順ではなく、質問 → クエリ → パネル → アラート → ファイルの順に進みます。

最後のステップが特に重要です。クリックで作ったダッシュボードは、Grafanaのデータベースの中にしかないため、そのデータベースが消えると一緒に消え、誰がいつ何を直したのかも残りません。実務で「あのときのダッシュボード」が見つからないことは、ほとんどいつもこの理由です。

このPodには、shop-apiという架空のサービスのメトリクスが12時間分入っています。http_requests_totalにはhandlerラベルが4つ(/api/orders、/api/search、/api/users、/healthz)あり、応答時間はhttp_request_duration_seconds_bucketヒストグラムとして入ってきます。

ステップ

  1. /root/graf/provisioning/datasources/prometheus.ymlにデータソースを宣言し、Grafanaをhttp://127.0.0.1:3000で起動します。データソースのuidはlabprom、アドレスはhttp://127.0.0.1:9090です。
  2. このダッシュボードが答える質問を/root/graf/02-question.mdに1文で、その質問に答えるPromQLを/root/graf/02-question.promqlに1行で書きます。
  3. そのクエリを入れた、パネル1つのダッシュボードを作ってアップロードします。uidはshop-apiです。
  4. p95応答時間と現在のリクエスト率のパネルを追加します。タイプは質問の形に従います。
  5. handlerテンプレート変数を入れ、すべてのパネルがその変数で絞り込まれるようにします。
  6. /root/graf/runbook.mdにランブックを書きます。「何が壊れたか」「最初に見るもの」「元に戻す方法」の3節です。
  7. /root/graf/provisioning/alerting/shop-api.ymlにアラートルールを作り、ランブックを指すようにします。
  8. ダッシュボードをJSONでエクスポートして/root/graf/dashboards/shop-api.jsonに置き、プロバイダーファイルで、Grafanaがそのディレクトリを見るようにします。

参考

Grafanaを起動し、データソースをファイルで接続する

/root/graf/provisioning/datasources/prometheus.ymlに、uidがlabpromのprometheusデータソースを宣言し、そのディレクトリをプロビジョニングのパスに指定して、Grafanaをhttp://127.0.0.1:3000で起動してください。

データソースをUIでクリックして追加すると、GrafanaのDBにしか残りません。採点ツールは、APIレスポンスのreadOnlyがtrueかどうかを調べます。それが「ファイルから来た」という印です。

mkdir -p /root/graf/provisioning/datasources /root/graf/provisioning/dashboards \
         /root/graf/provisioning/alerting /root/graf/dashboards \
         /tmp/gf/data /tmp/gf/logs /tmp/gf/plugins

GF_PATHS_DATA=/tmp/gf/data GF_PATHS_LOGS=/tmp/gf/logs GF_PATHS_PLUGINS=/tmp/gf/plugins \
GF_PATHS_PROVISIONING=/root/graf/provisioning \
GF_SERVER_HTTP_PORT=3000 \
GF_AUTH_ANONYMOUS_ENABLED=true GF_AUTH_ANONYMOUS_ORG_ROLE=Admin \
GF_PLUGINS_PREINSTALL_DISABLED=true \
  setsid nohup grafana server --homepath /opt/grafana >/var/log/grafana.log 2>&1 </dev/null &

curl -s http://127.0.0.1:3000/api/health          # "database": "ok" 가 나올 때까지 20~40초
curl -s http://127.0.0.1:3000/api/datasources/uid/labprom/health

環境変数3つの理由は次のとおりです。データ・ログ・プラグインのパスを/tmpに向けるのは、このPodにはcapabilityがなく、既定のパスに書き込めないことがあるためです。匿名アクセスを有効にするのは、Webプレビューで開いたときに、最初にログイン画面に出会わないようにするためです。プラグインの事前インストールを無効にするのは、Grafana 11.4が起動するたびにgrafana-lokiexplore-appをインターネットから取得しようとしますが、このPodは外へ出られないためです。

このダッシュボードが答える質問を先に書く

/root/graf/02-question.mdに、このダッシュボードが答える質問を、疑問符で終わる1文で書き、/root/graf/02-question.promqlに、その質問に答えるPromQLを書いてください。質問は「shop-apiのレスポンスのうち、5xxは何%か」です。

件数ではなく割合です。トラフィックが2倍になれば5xxの件数も2倍になりますが、ユーザーが失敗を経験する確率は変わりません。

5xxリクエストの秒あたりの件数を、全リクエストの秒あたりの件数で割ります。statusラベルとrate()を使います。ウィンドウは[5m]にしてください。

promq 'sum(rate(http_requests_total[5m]))'
promq 'sum by (status) (rate(http_requests_total[5m]))'
promq "$(cat /root/graf/02-question.promql)"

採点ツールは、皆さんが書いたクエリをGrafanaのデータソース経由で実際に実行して得られた値を見ます。書き方が違っても、数字が合っていれば合格です。

質問に答えるパネルを作る

uidがshop-apiのダッシュボードを作り、ステップ2のクエリを描くtimeseriesパネルを1つ入れてください。ダッシュボードのタイトルは、答える質問がわかるようにつけます。

Webプレビューを開き、Grafanaで新しいダッシュボードを作ってパネルを追加してもよく(その際、ダッシュボードの設定でuidをshop-apiに指定してください)、次のようにAPIでアップロードしてもかまいません。

curl -s -XPOST -H 'Content-Type: application/json' \
  -d @/tmp/dash.json http://127.0.0.1:3000/api/dashboards/db

/tmp/dash.jsonは{"overwrite": true, "dashboard": { ... }}の形です。

タイプはtimeseriesでなければなりません。インシデントで必要な答えは「いま何%か」ではなく「いつから上がったか」であり、時間軸のないタイプでは、それを見られません。

アップロードした結果は、次のように確認します。

curl -s http://127.0.0.1:3000/api/dashboards/uid/shop-api | jq '.dashboard.panels'

パネルのタイプを質問の形に合わせる

同じダッシュボードにp95応答時間パネルと現在のリクエスト率パネルを追加して、パネルを3つにしてください。p95はtimeseries、現在のリクエスト率はstatです。

p95はヒストグラムから求めます。leはバケットの境界なので、それだけを残して残りを合算する必要があります。

promq 'histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))'
promq 'sum(rate(http_requests_total[5m]))'

現在のリクエスト率にgaugeを使わないでください。ゲージは0と最大値のあいだのどのあたりかを見せる道具ですが、秒あたりのリクエスト数には最大値がありません。いまこの瞬間の数字1つはstatです。

逆に、p95をstatやgaugeにすると、「いつから遅くなったか」を見られません。分位数は、時間とともにどう動いたかがすべてです。

1つのダッシュボードで4つのハンドラーを見られるようにする

handlerという名前のqueryタイプのテンプレート変数を入れ、3つのパネルのクエリがすべてその変数で絞り込まれるようにしてください。

値を手で並べるcustom変数は、ハンドラーが1つ増えた日に古くなります。データから読み込んでください。

label_values(http_requests_total, handler)

そして変数は、宣言しただけでは何もしません。パネルのクエリが、次のように絞り込まれている必要があります。

sum(rate(http_requests_total{handler="$handler"}[5m]))

採点ツールは、変数を実際の値に置き換えてクエリを投げてみます。ラベル名や引用符が間違っていると空の結果が出て、その場で不合格になります。

アラートよりランブックを先に書く

/root/graf/runbook.mdにランブックを書いてください。## 무엇이 깨졌나、## 먼저 볼 것、## 되돌리는 법の3つの節が必要で(韓国語の見出しは順に「何が壊れたか」「最初に見るもの」「元に戻す方法」という意味です)、「最初に見るもの」には、実際に投げられるhttp_requests_totalのクエリを入れる必要があります。

順序がこうなっているのには理由があります。アラートを先に作ると「鳴ったらそのとき考えよう」になり、その文書は結局書かれません。ランブックを先に書けば、「これが鳴ったとき、人はいま何をするのか」に答えられないアラートが、そもそも作られません。

「最初に見るもの」は文章ではなくコマンドでなければなりません。「状態を確認する」は、午前3時には何の役にも立ちません。どのハンドラーが失敗しているかを数えるクエリを、そのまま書いておいてください。

アラートを作り、ランブックを指すようにする

/root/graf/provisioning/alerting/shop-api.ymlに、5xx割合のアラートルールを作ってください。しきい値の条件が必要で、forは5分以上、annotations.runbook_urlはステップ6のランブックを指す必要があります。ファイルを書いたら、Grafanaを再起動します。

forが、このステップの核心です。5xxはリクエストが1件失敗しただけでも一瞬跳ねますが、そのたびに人を起こすと、その人は次からアラートを無視します。アラートは、消えて死ぬのではなく、無視されて死にます。

アラートのクエリには、$handlerのようなダッシュボード変数を使えません。アラートは画面なしに評価されるため、変数を解決してくれるドロップダウンがありません。

プロビジョニングの設定は、起動時に読み込みます。ファイルだけ書いておいても、何も起こりません。

pkill -x grafana; sleep 3
# (1단계와 같은 환경변수로 다시 띄운다)
curl -s http://127.0.0.1:3000/api/v1/provisioning/alert-rules | jq '.[].title'

管理者アカウントで再読み込みさせる方法もあります(匿名アクセスでは403です)。

curl -XPOST -u admin:admin http://127.0.0.1:3000/api/admin/provisioning/alerting/reload

ダッシュボードをファイルとして固める

いま画面に表示されているダッシュボードをJSONでエクスポートして/root/graf/dashboards/shop-api.jsonに保存し、/root/graf/provisioning/dashboards/lab.ymlで、Grafanaがそのディレクトリを見るようにしてから、再起動してください。採点ツールは、meta.provisionedがtrueかどうかを調べます。

ダッシュボードJSONとプロバイダー(provider)ファイルは別物です。プロバイダーファイルには、ダッシュボードではなく、どのディレクトリを見るかという指示が入ります。

curl -s http://127.0.0.1:3000/api/dashboards/uid/shop-api \
  | jq '.dashboard' > /root/graf/dashboards/shop-api.json

pkill -x grafana; sleep 3
# (1단계와 같은 환경변수로 다시 띄운다)

curl -s http://127.0.0.1:3000/api/dashboards/uid/shop-api | jq '.meta.provisioned'

meta.provisionedがtrueなら、いま画面に表示されているものはファイルから来たものです。その時点から、APIで上書きしようとしても、GrafanaがCannot save provisioned dashboardで拒否します。画面とファイルがずれる道を塞いでいるのです。