クライアントライブラリの記述
このドキュメントは、Prometheus クライアントライブラリが提供すべき機能と API について説明しています。これは、ライブラリ間の一貫性を確保し、簡単なユースケースを容易にし、ユーザーを間違った方向に導く可能性のある機能の提供を避けることを目的としています。
執筆時点では 10 言語が既にサポート されており、クライアントの書き方について十分な知識が得られました。これらのガイドラインは、新しいクライアントライブラリの作成者が優れたライブラリを制作するのに役立つことを目指しています。
規約
MUST/MUST NOT/SHOULD/SHOULD NOT/MAY は、https://www.ietf.org/rfc/rfc2119.txt で与えられる意味を持ちます。
さらに ENCOURAGED は、機能が望ましいが、存在しなくても問題ないことを意味します。つまり、「あれば嬉しい」機能です。
考慮すべき点
-
各言語の機能を活用してください。
-
一般的なユースケースは容易であるべきです。
-
正しいやり方が簡単なやり方であるべきです。
-
より複雑なユースケースも可能であるべきです。
一般的なユースケースは(順に)次のとおりです。
-
ラベルのないカウンターをライブラリ/アプリケーション全体に広く散布する。
-
サマリー/ヒストグラムのコードブロック/関数のタイミング。
-
現在の状態を追跡するためのゲージ(およびその制限)。
-
バッチジョブの監視。
全体構造
クライアントは内部的にコールバックベースで記述される必要があります。クライアントは一般的にここに記載されている構造に従うべきです。
主要なクラスは Collector です。これは、ゼロ個以上のメトリックとそのサンプルを返すメソッド(通常は 'collect' と呼ばれる)を持ちます。コレクターは CollectorRegistry に登録されます。データは、CollectorRegistry をクラス/メソッド/関数「bridge」に渡すことで公開され、Prometheus がサポートする形式でメトリックが返されます。CollectorRegistry がスクレイプされるたびに、各コレクターの collect メソッドにコールバックする必要があります。
ほとんどのユーザーが対話するインターフェースは、Counter、Gauge、Summary、Histogram Collector です。これらは単一のメトリックを表し、ユーザーが自身のコードをインストルメントするほとんどのユースケースをカバーするはずです。
プロキシ(他の監視/インストルメンテーションシステムからの)のような、より高度なユースケースではカスタム Collector の記述が必要です。また、CollectorRegistry を受け取り、異なる監視/インストルメンテーションシステムが理解できる形式でデータを生成する「bridge」を記述することで、ユーザーは 1 つのインストルメンテーションシステムについてのみ考えることができます。
CollectorRegistry は `register()`/`unregister()` 関数を提供すべきであり、Collector は複数の CollectorRegistry に登録されることが許容されるべきです。
クライアントライブラリはスレッドセーフである必要があります。
C のような非 OO 言語では、クライアントライブラリは、実践可能な限り、この構造の精神に従うべきです。
命名
クライアントライブラリは、作業している言語の命名規則を考慮しつつ、このドキュメントで言及されている関数/メソッド/クラス名に従うべきです。例えば、Python では `set_to_current_time()` は良いメソッド名ですが、Go では `SetToCurrentTime()`、Java では `setToCurrentTime()` が慣例です。技術的な理由(例:関数オーバーロードを許可しない)で名前が異なる場合、ドキュメント/ヘルプ文字列はユーザーを他の名前の方に誘導すべきです。
ライブラリは、ここで与えられた名前と同じまたは類似した名前で、異なる意味を持つ関数/メソッド/クラスを提供してはなりません。
メトリック
Counter、Gauge、Summary、Histogram の メトリックタイプ がユーザーの主なインターフェースです。
Counter と Gauge はクライアントライブラリの一部である必要があります。Summary または Histogram の少なくとも一方は提供される必要があります。
これらは主にファイル静的変数として使用されるべきです。つまり、インストルメントするコードと同じファイルに定義されたグローバル変数です。クライアントライブラリはこれを可能にすべきです。一般的なユースケースは、オブジェクトの 1 つのインスタンスのコンテキストではなく、コード全体をインストルメントすることです。ユーザーはメトリックをコード全体に渡すことを気にする必要はなく、クライアントライブラリがそれを代行すべきです(そうしない場合、ユーザーはライブラリのラッパーを書いて「簡単」にしようとしますが、それはうまくいかないことがよくあります)。
デフォルトの CollectorRegistry が存在する必要があります。標準メトリックは、ユーザーによる特別な作業なしに、デフォルトで暗黙的にそれに登録される必要があります。バッチジョブや単体テストでの使用のために、メトリックがデフォルトの CollectorRegistry に登録されないようにする方法が必要です。カスタムコレクターもこれに従うべきです。
メトリックの作成方法は言語によって異なります。Java、Go のような言語ではビルダーアプローチが最適ですが、Python のような言語では、関数引数がそれを 1 回の呼び出しで実行するのに十分です。
例えば、Java Simpleclient の場合、次のようになります。
class YourClass {
static final Counter requests = Counter.build()
.name("requests_total")
.help("Requests.").register();
}
`build()` を呼び出すと、リクエストはデフォルトの CollectorRegistry に登録されます。`register()` ではなく `build()` を呼び出すことで、メトリックは登録されません(単体テストに便利です)。また、`register()` に CollectorRegistry を渡すこともできます(バッチジョブに便利です)。
Counter
Counter は単調増加するカウンターです。値が減少することは許されませんが、0 にリセットされることはあります(サーバーの再起動など)。
カウンターには以下のメソッドが必要です。
- `inc()`: カウンターを 1 増やします。
- `inc(double v)`: 与えられた量だけカウンターを増やします。v >= 0 であることを確認する必要があります。
カウンターには以下が推奨されます。
特定のコードブロックでスロー/発生した例外をカウントする方法。オプションで特定の例外タイプのみをカウントできます。Python では `count_exceptions` です。
カウンターは 0 から開始する必要があります。
Gauge
Gauge は増減する値を示します。
ゲージには以下のメソッドが必要です。
- `inc()`: ゲージを 1 増やします。
- `inc(double v)`: 与えられた量だけゲージを増やします。
- `dec()`: ゲージを 1 減らします。
- `dec(double v)`: 与えられた量だけゲージを減らします。
- `set(double v)`: ゲージを指定された値に設定します。
ゲージは 0 から開始する必要があります。特定のゲージが異なる数値から開始する方法を提供することは許可されます。
ゲージには以下のメソッドが推奨されます。
- `set_to_current_time()`: ゲージを現在のユニックスエポック秒に設定します。
ゲージには以下が推奨されます。
特定のコードブロック/関数で進行中のリクエストを追跡する方法。Python では `track_inprogress` です。
コードブロックの時間を計測し、ゲージをその秒単位の期間に設定する方法。これはバッチジョブに便利です。Java では `startTimer`/`setDuration`、Python では `@decorator`/context manager の `time()` です。これは Summary/Histogram のパターンと一致すべきです(`observe()` の代わりに `set()`)。
概要
A summary は、リクエストの期間などのイベントの観測値を時間スライディングウィンドウでサンプリングし、その分布、頻度、合計のインスタントな洞察を提供します。
サマリーでは、サマリーのパーセンタイルを指定するために内部的に使用されるため、"quantile" をラベル名として設定することはできません。サマリーはパーセンタイルをエクスポートとして提供することが推奨されますが、これらは集約できず、遅くなる傾向があります。サマリーではパーセンタイルがない場合も許可される必要があります。なぜなら、`_count`/`_sum` だけでも非常に有用であり、これがデフォルトであるべきだからです。
サマリーには以下のメソッドが必要です。
- `observe(double v)`: 与えられた量を観測します。
サマリーには以下のメソッドが推奨されます。
ユーザーのためにコードの時間を秒単位で計測する方法。Python では `@decorator`/context manager の `time()` です。Java では `startTimer`/`observeDuration` です。秒以外の単位は提供してはなりません(ユーザーが何か他のことをしたい場合は、手動で行うことができます)。これは Gauge/Histogram と同じパターンに従うべきです。
Summary の `_count`/`_sum` は 0 から開始する必要があります。
Histogram
Histograms は、リクエストレイテンシなどのイベントの集計可能な分布を可能にします。これは基本的にバケットごとのカウンターです。
ヒストグラムでは、`le` をユーザー設定ラベルとして許可してはなりません。なぜなら `le` はバケットを指定するために内部的に使用されるからです。
ヒストグラムは、バケットを手動で選択する方法を提供する必要があります。`linear(start, width, count)` および `exponential(start, factor, count)` 形式でバケットを設定する方法が提供されるべきです。Count には `+Inf` バケットを含める必要があります。
ヒストグラムは、他のクライアントライブラリと同じデフォルトバケットを持つべきです。バケットはメトリック作成後に変更することはできません。
ヒストグラムには以下のメソッドが必要です。
- `observe(double v)`: 与えられた量を観測します。
ヒストグラムには以下のメソッドが推奨されます。
ユーザーのためにコードの時間を秒単位で計測する方法。Python では `@decorator`/context manager の `time()` です。Java では `startTimer`/`observeDuration` です。秒以外の単位は提供してはなりません(ユーザーが何か他のことをしたい場合は、手動で行うことができます)。これは Gauge/Summary と同じパターンに従うべきです。
Histogram の `_count`/`_sum` およびバケットは 0 から開始する必要があります。
その他のメトリックに関する考慮事項
上記以外で、特定の言語にとって意味のある追加機能をメトリックに提供することは推奨されます。
もし簡略化できる一般的なユースケースがあれば、望ましくない動作(非効率的なメトリック/ラベルレイアウト、クライアントでの計算実行など)を助長しない限り、それを行ってください。
ラベル
ラベルは Prometheus の 最も強力な側面 の 1 つですが、簡単に乱用 されます。したがって、クライアントライブラリはラベルをユーザーに提供する方法に非常に注意する必要があります。
クライアントライブラリは、Gauge/Counter/Summary/Histogram またはライブラリによって提供される他のコレクターに対して、同じメトリックで異なるラベル名を持つことをユーザーに許可してはなりません。
カスタムコレクターからのメトリックは、ほぼ常に一貫したラベル名を持つべきです。まだ稀ですが、そうでない有効なユースケースもあるため、クライアントライブラリはこれを検証しないべきです。
ラベルは強力ですが、ほとんどのメトリックにはラベルがありません。したがって、API はラベルを許可するべきですが、それを支配しないようにすべきです。
クライアントライブラリは、Gauge/Counter/Summary/Histogram 作成時にラベル名のリストをオプションで指定できる必要があります。クライアントライブラリは任意の数のラベル名をサポートすべきです。クライアントライブラリは、ラベル名が 文書化された要件 を満たすことを検証する必要があります。
メトリックのラベル付けされた次元へのアクセスを提供する一般的な方法は、ラベル値のリストまたはラベル名からラベル値へのマップを受け取り、「Child」を返す `labels()` メソッドを介したものです。通常の `.inc()`/`.dec()`/`.observe()` などのメソッドは、その後 Child に対して呼び出すことができます。
`labels()` によって返される Child は、ユーザーによってキャッシュ可能であるべきです。これにより、再度検索する必要がなくなります。これはレイテンシが重要なコードで重要です。
ラベル付きメトリックは、`labels()` と同じシグネチャを持つ `remove()` メソッド(これにより、メトリックから Child を削除し、もはやエクスポートしなくなります)、およびすべての Child をメトリックから削除する `clear()` メソッドをサポートすべきです。これらは Child のキャッシュを無効にします。
特定の Child をデフォルト値で初期化する方法があるべきです。通常は `labels()` を呼び出すだけです。ラベルのないメトリックは、メトリックの欠落に関する問題 を避けるために、常に初期化される必要があります。
メトリック名
メトリック名は 仕様 に従う必要があります。ラベル名と同様に、Gauge/Counter/Summary/Histogram の使用、およびライブラリで提供される他のコレクターでは、これが満たされている必要があります。
多くのクライアントライブラリは、`namespace_subsystem_name` の 3 つの部分で名前を設定できます。そのうち `name` のみが必須です。
動的/生成されたメトリック名またはメトリック名のサブパートは、カスタムコレクターが他のインストルメンテーション/監視システムからプロキシする場合を除き、推奨されません。生成/動的なメトリック名は、代わりにラベルを使用すべきであることの兆候です。
メトリックの説明とヘルプ
Gauge/Counter/Summary/Histogram は、メトリックの説明/ヘルプを提供することを必須とします。
クライアントライブラリで提供されるカスタムコレクターは、そのメトリックに説明/ヘルプを持っている必要があります。
必須引数にすることが推奨されますが、特定の長さであるかどうかのチェックは行いません。ドキュメントを書きたくない人がいる場合、それを説得することはできません。ライブラリで提供されるコレクター(そしてエコシステム全体で可能な限り)は、例を示すために、優れたメトリック説明を持つべきです。
エクスポージョン
クライアントは、エクスポージョンフォーマット ドキュメントで概説されているテキストベースのエクスポージョンフォーマットを実装する必要があります。
エクスポートされたメトリックの再現可能な順序は、(特に人間が読める形式で)実装に重大なリソースコストがかからない場合は推奨されます。
標準およびランタイムコレクター
クライアントライブラリは、以下に文書化されている標準エクスポートの可能な限り多くのものを、提供すべきです。
これらはカスタムコレクターとして実装され、デフォルトの CollectorRegistry にデフォルトで登録されるべきです。非常にニッチなユースケースで邪魔になる場合があるため、これらを無効にする方法があるべきです。
プロセスメトリクス
これらのメトリックには `process_` というプレフィックスがあります。使用している言語やランタイムで必要な値を取得することが困難、または不可能な場合、クライアントライブラリは、誤った、不正確な、または特殊な値(`NaN` など)をエクスポートするよりも、対応するメトリックを省略することを優先すべきです。すべてのメモリ値はバイト、すべての時間はユニックスエポック/秒です。
| メトリック名 | ヘルプ文字列 | 単位 |
|---|---|---|
process_cpu_seconds_total | ユーザーとシステムの両方の CPU 総時間(秒)。 | 秒 |
process_open_fds | 開いているファイルディスクリプタの数。 | ファイルディスクリプタ |
process_max_fds | 開くことができるファイルディスクリプタの最大数。 | ファイルディスクリプタ |
process_virtual_memory_bytes | 仮想メモリサイズ(バイト)。 | バイト |
process_virtual_memory_max_bytes | 利用可能な仮想メモリの最大量(バイト)。 | バイト |
process_resident_memory_bytes | 常駐メモリサイズ(バイト)。 | バイト |
process_heap_bytes | プロセスヒープサイズ(バイト)。 | バイト |
process_start_time_seconds | プロセスの開始時刻(ユニックスエポックからの秒数)。 | 秒 |
process_threads | プロセスの OS スレッド数。 | スレッド |
ランタイムメトリクス
さらに、クライアントライブラリは、言語のランタイム(例:ガベージコレクション統計)にとって意味のあるメトリックを提供することが推奨されます。その際、`go_`、`hotspot_` などの適切なプレフィックスを使用します。
単体テスト
クライアントライブラリは、コアインストルメンテーションライブラリとエクスポージョンをカバーする単体テストを持つべきです。
クライアントライブラリは、ユーザーがインストルメンテーションコードの使用を単体テストするための簡単な方法を提供することが推奨されます。例えば、Python の `CollectorRegistry.get_sample_value` です。
パッケージングと依存関係
理想的には、クライアントライブラリは、インストルメンテーションを追加するためにどのアプリケーションにも含めることができ、アプリケーションを壊さないはずです。
したがって、クライアントライブラリに依存関係を追加する際には注意が必要です。例えば、ライブラリのバージョン x.y を必要とする Prometheus クライアントを使用するライブラリを、アプリケーションの他の場所で使用されている x.z と同時に追加した場合、アプリケーションに悪影響を与えるでしょうか?
この問題が発生する可能性がある場合は、コアインストルメンテーションを、特定の形式でのメトリックの bridge/exposition から分離することが提案されます。例えば、Java simpleclient の `simpleclient` モジュールには依存関係がなく、`simpleclient_servlet` には HTTP 部分が含まれています。
パフォーマンスに関する考慮事項
クライアントライブラリはスレッドセーフである必要があるため、何らかの形式の並行制御が必要であり、マルチコアマシンやアプリケーションでのパフォーマンスを考慮する必要があります。
経験上、最もパフォーマンスが低いのはミューテックスです。
プロセッサの原子命令は中間に位置し、一般的に許容範囲内です。
異なる CPU が同じ RAM ビットをミューテートすることを避けるアプローチが最も効果的です。Java の simpleclient の DoubleAdder などです。ただし、メモリコストがかかります。
上記のように、`labels()` の結果はキャッシュ可能であるべきです。ラベル付きメトリックをバックする同時マップは、比較的遅くなる傾向があります。ラベルのないメトリックを特別扱いして `labels()` のようなルックアップを避けることで、大幅な改善が見込めます。
メトリックは、インクリメント/デクリメント/セットなどの操作中にブロックすることを避けるべきです。スクレイプが進行中にアプリケーション全体が停止するのは望ましくありません。
主要なインストルメンテーション操作(ラベルを含む)のベンチマークを提供することは推奨されます。
エクスポージョンの実行時には、リソース消費、特に RAM を考慮する必要があります。結果をストリーミングすることでメモリフットプリントを削減することを検討し、同時スクレイプの数に制限を設けることも可能です。