exporterの書き方 - Prometheusドキュメント

このページはPrometheus公式ドキュメント和訳+αの一部です。

exporterの書き方

自分自身のコードにメトリクスを組み込む場合は、Prometheusクライアントライブラリを用いたコードへのメトリクスの組み込み方の一般的なルールに従うべきである。 他の監視システムやメトリクス組み込みシステムからメトリクスを取得する場合は、物事はそこまで白黒はっきりしていない。

このドキュメントには、exporterや独自コレクターを書く時に考慮すべきことが含まれている。 ここで述べられている理論は、直接メトリクス組み込みをする人にも興味深いだろう。

もしexporterを書いていて、このページのどこかが分からなければ、IRC(#prometheus on Freenode)またはメーリングリストで私たちに連絡して下さい。

Maintainability and purity

exporterを書く際に決めなければいけない主なことは、完璧なメトリクスを得るためにどれぐらいの労力をかけるつもりがあるのかということである。

もし目的のシステムにほぼ変化しないいくつかのメトリクスしかないなら、全てを完璧にするのは簡単な選択である。 この良い例が、HAProxy exporterである。

他方、新バージョンで頻繁に変わる数百ものメトリクスがあるシステムで完璧にしようとすれば、長期間にわたる多くの仕事の契約をした事になる。MySQL exporterがこちら側の極端な例である。

node exporterは、これらの中間にあり、モジュールごとに様々な複雑さがある。 例えば、mdadmコレクターは、ファイルを独自解析して、特にこのコレクターのために生成されたメトリクスを出力する。 meminfoコレクターに対してはカーネルバージョンによって結果が異なるので、正しいメトリクスを生成するために十分な変換をすることになる。

Configuration

もし粒度が細か過ぎるなら、特定のメトリクスを外せるようにしてあげなければいけないかもしれない。 例えば、HAProxy exporterは、サーバー毎の統計をフィルタリングできるようになっている。 同様に、デフォルトでは無効になっている高価なメトリクスがあるかもしれない。

他の監視システム、フレームワークプロトコルに取り組んでいるなら、 Prometheusに適したメトリクスを生成するように、追加設定やカスタマイズを提供しなければならないだろう。 一番良いケースでは、メトリクスの変換方法が自動的に決まるぐらいPrometheusに十分に似ているデータモデルを持っている。 CloudwatchSNMPcollectdがこのケースに当てはまる。 この場合、せいぜい、ユーザーがどのメトリクスを取り出したいかを選択できる機能が必要である。

他のケースでは、そのシステムから来るメトリクスは完全に非標準的であり、システムの使い方やその下にあるアプリケーションに依存している。 そのようなケースでは、メトリクスをどのように変換するかをユーザーが指定する必要がある。 この一番厄介な例がJMX exporterである。 Graphite exporterStatsD exporterもラベルを抽出するために設定が必要である。

設定なしにexporterが動くことの保証すること、および変換に設定が必要ならそのサンプル集を提供することが推奨される。

YAMLが標準のPrometheus設定フォーマットであり、全ての設定はデフォルトでYAMLを利用するべきである。

メトリクス

命名

メトリック名とラベル名ベストプラクティスに従うこと。

一般的に、メトリック名は、Prometheusには慣れていても特定のシステムには慣れていない人がメトリックの意味を推測しやすくなっているべきである。 http_requests_totalという名前は、物凄く有益という訳ではない(リクエストが入ってきた時に測られるのだろうか?何らかのフィルターの中で測られるのだろうか?それともユーザーのコードに到達した時に測れるのだろうか?)。 requests_totalはさらに悪い。どんな種類のリクエストなのか分からない。

メトリクスを直接組み込む場合、あるメトリックは一つのファイルだけに存在しているべきである。 したがって、exporterやcollectorの中でメトリックは一つのサブシステムにだけ適用されるべきであり、それに従って命名されるべきである。

メトリック名は、カスタムcollectorやexporterを書いている時以外は、決して機械的に生成してはいけない。

アプリケーションのためのメトリック名は、一般的に、exporter名で始めるべきである(例 haproxy_up)。

メトリクスには基本単位(秒やバイト)を使うべきである。単位を読みやすく変換するのはグラフ化ツールに任せるべきである。 どんな単位を使うことになったとしても、メトリック名に付ける単位は、使われている単位に合わせなければならない。 同様に、パーセントではなく比率を使うべきである。 その比率を成す二項目のカウンターを指定するとさらに良い。

メトリック名は、一緒に出力されるラベルを含んではいけない。例えば、by_typeは、集約されてラベルが亡くなった時に意味をなさなくなってしまう。

一つの例外は、同じデータを異なるラベルで複数のメトリクスを通して出力する時である。 こういう場合は、普通、メトリック名にラベルを含めるのがそれらのデータを区別するための最も合理的な方法となる。 これは、直接のメトリクス組み込みでは、一つのメトリックを全てのラベル値と共に出力すると個数が多くなり過ぎる場合にだけ起こるはずである。

Prometheusのメトリクスとラベル名は、snake_caseで書かれる。 camelCaseからsnake_caseに変換することが望ましいが、myTCPExampleisNaNのようなものに対しては自動で変換すると必ずしも良い結果にはならないので、そのままにしておくのが良い場合もある。

exposeされるメトリクスはコロンを含んではならない。コロンは、集約する時にユーザー定義のレコーディングルールが使うために予約されている。

メトリック名では[a-zA-Z0-9:_]のみが正しく、他のあらゆる文字はアンダースコアに書き換えるなければならない。

_sum_count_bucket_totalといったサフィックスは、サマリーやヒストグラム、カウンターで利用される。 これらを生成するのでなければ、これらのサフィックスは避けること。

カウンターには_totalを付けるのが慣例である。COUNTERタイプを使う場合は、それに従うべきである。

プリフィックスprocess_scrape_は、予約されている。 独自のプリフィックスをこれらに追加することは、matching semanticsに従っているのなら、問題ない。 例えば、Prometheusには、スクレイプにどれぐらいかかったかを表すscrape_duration_secondsがある。 また、特定のexporterがその処理にどれぐらいかかったかを表すexporterに関連するメトリック(例えば、jmx_scrape_duration_seconds)もあるのが良いプラクティスである。 PIDへのアクセスが出来るプロセスの統計情報に関して、GoとPythonはこの処理をするコレクターを提供している。 HAProxy exporterがこの良い例である。

成功したリクエスト数と失敗したリクエスト数がある場合、これをexposeする最も良い方法は、一つのメトリックをリクエスト総数に、もう一つのメトリックを失敗したリクエスト数にすることである。 これによってエラー率を計算するのが簡単になる。 failedやsuccessというラベルを持つ一つのメトリックを使わないこと。 同様に、キャッシュのヒットとミスに関しては、一つを総数、もう一つをヒット数にするのが良い。

監視を利用している人がメトリック名でコード検索やWeb検索をする可能性を考慮すること。 名前がうまく決められていてその名前に慣れている人達の範囲を超えて使われる可能性が低ければ(例えばSNMPとネットワークエンジニア)、 名前をそのままにしておくのが良いだろう。 この理論は全てのexporterに当てはまるわけではない。例えば、MySQL exporterのメトリクスは、DB管理者だけでなく色々な人に使われるだろう。 HELP文字列に元々の名前を含めておくと、元々の名前を使うのと同程度の恩恵が得られるだろう。

ラベル

ラベルに関する一般的なアドバイスを読むこと。

ラベル名としてtypeは避けること。 一般的過ぎるし、多くの場合意味がない。 regionzoneclusteravailability_zoneazdatacenterdcownercustomerstageserviceenvironmentenvのような、監視対象を表すラベルと衝突する可能性の高いラベルも、可能であれば避けること。 ただし、アプリケーションがリソースをそのような名前で呼んでいるなら、名前変更による混乱を起こさないようにするのが良い。

プリフィックスが共通しているというだけの理由で1つのメトリックにものを詰め込みたくなる誘惑に耐えること。 1つのメトリックとして意味を成すという確信がなければ、複数のメトリクスにしておく方が安全である。

leというラベルはヒストグラムにとって特別な意味があり、quantileというラベルはサマリーにとって特別な意味がある。 一般的にこれらのラベルは避けること。

Read/writeやsend/receiveは、ラベルではなく、別々のメトリクスにするのが良い。 これらは、一度に考慮するのは普通どちらか1つについてであって、そのように使った方が簡単である。

大まかなルールとして、1つのメトリックは、合計や平均した場合に意味を成すべきである。 そうすべきでない場合が1つあり、それはexporterに関連がある。 それはデータが本質的に表であるような場合であり、そうしなければユーザーがメトリクス名を正規表現で扱わなければならなくなるだろう。 マザーボード上の複数の電圧センサーについて考えてみると、それらの合計や平均は意味がない一方で、センサーごとのメトリックを持つのではなく、1つのメトリックにまとめるのが理にかなっている。 1つのメトリックの値は全て(ほぼ)必ず同じ単位であるべきである。例えば、ファンの速度が電圧と混ざっている場合を考えると、それらを自動的に分別する方法はない。

以下のようにしてはいけない。

my_metric{label=a} 1
my_metric{label=b} 6
my_metric{label=total} 7

以下のようにしてもいけない。

my_metric{label=a} 1
my_metric{label=b} 6
my_metric{} 7

前者は、このメトリックをsum()する人にとってうまくいかない。 後者は、sumがうまくいかないし、扱いが難しい。 いくつかのライブラリ(例えばGo)は、カスタムコレクターで後者のようなことをするのを積極的に止めようとする。 また、全てのクライアントライブラリは、直接のメトリクス組み込みで後者をすることを止めるべきである。 絶対に前者も後者もせずに、代わりにPrometheusの集約を利用すること。

自分の監視ツールがこのような出力をする場合、totalを落とすこと。 もし、何らかの理由(例えば、個別のカウントに入ってないものがtotalに入っている)で、それを持ち続けなければならないなら、違うメトリック名を使うこと。

メトリクスを組み込む際のラベルは、最小限にするべきである。 ユーザーがクエリを書く際に、余分なラベルそれぞれが、考慮しなければいけないことを1つずつ増やすことになる。 したがって、メトリクスを組み込む際に、消しても時系列の単一性を損なうことがないラベルは避けること。 メトリックの補足情報はinfoメトリックを介して追加することができる。 例えば、バージョン番号をどのように扱うかについて下記を参照すること。

ただし、あるメトリックの実質的に全てのユーザーが補足情報を求めていると予想される場合がある。 そのような場合、infoメトリックではなく、非ユニークなラベルを追加するのが正解である。 例えば、mysqld_exportermysqld_perf_schema_events_statements_totalのラベルdigestは、完全なクエリパターンのハッシュであり、ユニークさとして十分である。 しかし、人間が可読なラベルdigest_textがなければほとんど役に立たない。 これは、長いクエリに対してはクエリパターンの最初だけしか含んでおらず、ユニークではない。 したがって、結局は、人間のためのdigest_textと単一性のためのdigestの両方を持つことになる。

Target labels, not static scraped labels

もし、万が一、全てのメトリクスに同じラベルを適用したくなっても、やめること。

一般的に、これが起きる場合が2つある。

1つ目は、ソフトウェアのバージョン番号のような、メトリクスに付けておくと便利なラベルである。 この場合、代わりに、マシンロールのためのラベルはどう持つべきか?で説明されている方法を使うこと。

2つ目は、ラベルが実はターゲットラベルである時である。 リージョン、クラスタ名などのようなものは、アプリケーションではなく、インフラの構成から来るものである。 ラベルの分類としてどの場所に当てはまるかを決めるのは、アプリケーションではない。 それは、Prometheusサーバーを実行する人が設定することであり、同じアプリケーションを監視していても、人が異なれば、異なる名前を付けて良い。

したがって、これらのラベルは、利用している任意のサービスディスカバリーを経由して、Prometheusのスクレイプの設定に含まれる。 少なくとも一部のスクレイプする人にとっては、マシンロールは有益な情報である可能性が高いので、この考え方をマシンロールにも適用して良い。

自分のメトリクスをPrometheusの型に合うようにする必要がある。 これは、通常、カウンターとゲージである。 サマリーの_count_sumも比較的よくあり、たまに分位数も見かけるだろう。 ヒストグラムは稀であり、もしヒストグラムに出会ったら、出力形式が累積的な値を出力していることを思い出そう。

メトリクスの集合を自動的に処理するような場合は特に、メトリクスの型が明らかでないことがよくある。 一般的に、UNTYPEDが安全なデフォルトである。

カウンターは減少できないので、他のメトリクスを組み込むシステムから来るカウンター型がデクリメントされ得るのであれば、それはカウンターにしてはいけない。それはゲージである。 GAUGEがカウンターとして利用されるのは誤解を招くであろうから、ここで使うにはUNTYPEDがおそらく最善の型である。

ヘルプ文字列

メトリクスを変換している場合、元が何だったか、どんなルールがその変換で動いたのかをユーザーが辿れるようになっていると便利である。 collectorやexporterの名前と適用されたルールのID、元のメトリックの名前をヘルプ文字列に入れておくと、かなりユーザーが助かるだろう。

Prometheusでは、1つのメトリックに異なる複数のヘルプ文字列があるのは好ましくない。 多くのメトリクスから1つのメトリクスを作る場合、そのうちの1つを選んでヘルプ文字列の入れること。

この例として、SNMP exporterはOIDを利用し、JMX exporterはサンプルのmBean名を入れている。 HAProxy exporterには、手書きの文字列がある。 node exporterにも幅広い種類の例がある。

有益でない情報の削除

メトリクス組み込みの仕組みには、最小値、最大値、標準偏差に加えて、1m、5m、15mのレート、アプリケーションが起動してからの平均レート(Dropwizardでは、これらはmeanと呼ばれる)を出力するものがある。 。

これらは、有益ではないし、情報が散らかるので、全て削除されるべきである。 Prometheusは自分自身でレートを計算することができるし、普通は(出力された平均は指数関数的に陳腐化するので)より正確に計算できる。 minやmaxがどの時間にわたって計算されたのか分からないし、標準偏差は統計的に利用できないし、もし計算する必要があるならいつでも二乗和、_sum_countを出力することができる。

分位数にも同じような問題があり、それらを削除するのかサマリーに入れるのかを選ぶことができる。

ドット区切り文字列

多くの監視システムには、ラベルがない。 その代わり、my.class.path.mymetric.labelvalue1.labelvalue2.labelvalue3のようなことをする。

Graphite exporterStatsD exporterは、小さな設定言語を用いてそれらを変換する方法を共有している。 他のexporterも同じものを実装すべきである。 変換は、現在、Goでのみ実装されており、別のライブラリに切り出すことで恩恵が得られるだろう。

Collectors

自分のexporterのためにcollectorを実装している場合、通常の直接のメトリクス組み込みの手法を使って スクレイプのたびにメトリクスを更新するべきではない。

そうではなく、新しいメトリクスを毎回作成すること。 これは、Goでは、Update()メソッドの中でMustNewConstMetricを使うことでできる。 Pythonについてはhttps://github.com/prometheus/client_python#custom-collectorsを参照すること。 Javaについては、collectメソッドの中でList<MetricFamilySamples>を生成すること。 サンプルについては、StandardExports.java を参照すること。

こうすることには二重の理由がある。まず第一に、2つのスクレイプが同時に起こり得て、直接のメトリクス組み込みは実質的にファイルレベルでグローバルな変数を使っているので、レースコンディションが起きる。 第二に、ラベルの値が消滅しても出力はされ続ける。

exporter自体に直接メトリクスを組み込むのは問題がない。 転送されたバイト合計や全てのスクレイプにまたがってexporterによって実行された呼び出し回数は、その例である。 blackbox exporterSMNP exporterのような単一の監視対象に結びついていないexporterに対して、これらはいつも通りの/metricsの呼び出しでのみ出力するべきであり、特定の監視対象のスクレイプで出力すべきではない。

スクレイプ自体に関するメトリクス

スクレイプに関して、どれぐらい時間がかかったか、何個のレコードを処理したかのようなメトリクスを出力したい時がある。

これらは、1つのイベント(スクレイプ)に関するものなのでゲージとして出力すべきである。 その名前は、例えばjmx_scrape_duration_secondsのように、exporter名をプリフィックスにするべきである。 _exporterは、通常は除外され、exporterが単にcollectorとして使うのも理にかなっているなら絶対に除外される。

マシンとプロセスのメトリクス

例えばElasticsearchのような多くのシステムは、CPUやメモリ、ファイルシステム情報のようなマシンのメトリクスも出力する。 これらは、Prometheusエコシステムの中で、node exporterが提供しているので、削除されるべきである。

Javaの世界では、多くのメトリクス組み込みフレームワークが、CPUやGCのようなプロセスレベルとJVMレベルの情報を出力する。 JavaクライアントとJMX exporterは既に、DefaultExports.javaを通して好ましい形でこれらを含んでいるので、これらも削除するべきである。

他の言語やフレームワークでも同様である。

デプロイ

各exporterは、出来れば同じマシン上でちょうど1つのインスタンスのアプリケーションを監視すべきである。 つまり、運用しているHAProxyそれぞれに対してhaproxy_exporterプロセスを実行することになる。 Mesosワーカーがいる全てのマシンでMesos exporterを実行し、もしあるマシンにマスターもいるならマスターのためにさらに1つ実行する。

これはメトリクスを直接埋め込む場合にしていることであり、他の構成でも出来るだけそれに近くしようというのが、こうする理屈である。 つまり、全てのサービスディスカバリーは、exporterではなく、Prometheusの中で行われるということである。 これには、ユーザーがblackbox exporterで自分のサービスを検査できるような監視対象の情報をPrometheusが持つという恩恵もある。

これには2つの例外がある。

1つ目は、アプリケーションの横で監視を動かすのが根本的に意味がない場合である。 SNMPblackbox、IPMIのexporterがこの主な例である。 デバイスはその上でコードを実行できないブラックボックであることが多いので、IPMIやSNMPはこの例に当たる(ただし、node exporterをそのデバイスで動かせるなら、そうした方が良い)。 コードを実行する場所がないDNS名のようなものの監視をするのでblackbox exporterもこの例に当たる。 こういった場合でも、Prometheusがサービスディスカバリーをしてスクレイプする対象を渡すべきである。 サンプルは、blackbox exporterとSNMPを参照すること。

この種のexporterを書くことができるのは、現在、GoやPythonJavaのクライアントライブだけであることに注意。

2つ目の例外は、システムのランダムなインスタンスから情報を取得しており、どのインスタンスと通信していても構わない場合である。 MySQLレプリカの集合があり、そのデータに対してビジネスクエリを実行して出力したいと考えてみよう。 1つのレプリカと通信するためにいつも使っているロードバランスの方法を使うexporterにするのが最も合理的な方法である。

これは、マスター選出のあるシステムでは適用できない。 その場合、それぞれのインスタンスを個別に監視し、Prometheusで「マスターであること」を判断すべきである。 これは、必ずしもちょうど1つのマスターがある訳ではないからであり、Prometheusの足元でターゲットが何であるかを変更するとおかしなことが起きる。

スケジュール

メトリクスは、Prometheusがスクレイプした時にだけアプリケーションからpullされるべきである。 exporterは、自分のタイマーに基づいてスクレイプをするべきではない。 つまり、全てのスクレイプは同期的であるべきである。

したがって、出力するメトリクスにタイムスタンプをセットすべきではない。Prometheusがその面倒を見るべきである。 もし、タイムスタンプが必要と考えているなら、おそらく、代わりにPushgatewayが必要である。

もし、あるメトリクスの取得のコストが特に高い(例えば1分以上かかる)のであれば、それをキャッシュすることが容認される。 このことは、HELPで記載されるべきである。

Prometheusがスクレイプをタイムアウトさせるのは、デフォルトで10秒である。 自分のexporterがこれを超えそうなら、ユーザードキュメントでこのことを呼びかけるべきである。

プッシュ

アプリケーションや監視システムの中には、例えばStatsD、Graphite、collectdのように、メトリクスをプッシュするだけのものもある。

ここで考慮すべきことが2つある。

第一に、メトリクスをいつまで有効とするか? collectdおよびGraphiteと通信するものはどちらも定期的に出力をする。 それらが止まった時は、そのメトリクスの出力を止めたい。 collectdには有効期限が含まれているので、それを使う。 Graphiteはそうではないので、exporterのフラグになっている。

StatsDは、メトリクスではなくイベントを扱うものなので、少し違っている。 それぞれのアプリケーションの横で1つのexporterを起動し、アプリケーションが再起動するときに状態が綺麗になるようにexporterを再起動するのが最善の方法である。

第二に、この種のシステムでは、ユーザーが差分でも生のカウンターでも送信できる傾向にある。 生のカウンターがPrometheusの一般的なモデルなので、できる限り生のカウンターを使うべきである。

サービスレベルのメトリクス(例えばサービスレベルのバッチジョブ)では、状態を自分で管理するのではなく、exporterにPushgatewayへプッシュをさせ、イベントの後は終了させるべきである。 インスタンスレベルのバッチメトリクスでは、明らかなパターンはまだない。 選択肢としては、node exporterのtextfile collectorを乱用するか、メモリ内の状態に頼る(リブート後に状態を保持する必要がなければおそらく最善)か、textfile collectorに似た機能を実装するかのいずれかである。

失敗したスクレイプ

通信しているアプリケーションが反応しなかったり他の問題がある場合のスクレイプの失敗するときのために、現状で2つのパターンがある。

1つ目は、5xxエラーを返すことである。

2つ目は、スクレイプが機能しているかどうかをもとに0または1の値になる変数myexporter_up(例えばhaproxy_up)を持つことである。

スクレイプが失敗してもまだ有益なメトリクスが取得できる場合には、後者の方が良い。 例えば、HAProxy exporterはプロセスの情報を提供する。 後者は、upが通常の動作をするので多少使いやすい。 ただし、exporterがダウンしているのかアプリケーションがダウンしているか区別することはできない。

ランディングページ

http://yourexporter/を見ると簡単なHTMLでexporterの名前と/metricsページへのリンクがあるとユーザーにとって親切である。

ポート番号

ユーザーは、同じマシンにたくさんのexporterとPrometheusコンポーネントを持っているかもしれない。 したがって、それぞれにユニークなポート番号を持たせるのが簡単であるようにすること。

https://github.com/prometheus/prometheus/wiki/Default-port-allocationsがポート番号が記録されるところになっていて、誰でも編集できる。

exporterを開発しているときは、次のポート番号を、できれば公衆にアナウンスする前に、とって構わない。 まだリリースの準備ができていないなら、ユーザー名とWIPを書くので構わない。

これはPrometheusユーザーの暮らしを少し良くするための登録であり、特定のexporterを開発するという公約ではない。 内部的なアプリケーションのためのexporterには、デフォルトのポート割り当て範囲外のポートを利用することを推奨する。

アナウンス

自分のexporterを世界にアナウンスする準備が出来たなら、メーリングリストにメールして、利用可能なexporter一覧に追加するためにPRを送ること。

参考リンク

おすすめ書籍

入門 Prometheus ―インフラとアプリケーションのパフォーマンスモニタリング

入門 Prometheus ―インフラとアプリケーションのパフォーマンスモニタリング

入門 監視 ―モダンなモニタリングのためのデザインパターン

入門 監視 ―モダンなモニタリングのためのデザインパターン

SRE サイトリライアビリティエンジニアリング ―Googleの信頼性を支えるエンジニアリングチーム

SRE サイトリライアビリティエンジニアリング ―Googleの信頼性を支えるエンジニアリングチーム

和訳活動の支援

Prometheusドキュメント和訳が役に立った方は、以下QRコードからPayPayで活動を支援して頂けるとありがたいです。

PayPayによる支援用QRコード
上のQRコードからPayPayによる支援