七つの計測器、ログレコードの十二フィールド、そしてスキーマ URL
一言でいうと
メトリクスAPIの計測器(instrument)は、Counter・UpDownCounter・Gauge・Histogramと、その非同期(callback)の変形に分かれ、SDKが出力する合計(Sum)には、deltaまたはcumulativeというtemporalityが付きます。ログシグナルは、12個のフィールドを持つLogRecordデータモデルと、既存のロギングライブラリをそのモデルにつなぐブリッジAPIで構成されます。スキーマURLは、セマンティック規約が変わっても、コンシューマーがデータを解釈できるようにするバージョンの目印です。出典は、メトリクスAPI・メトリクスデータモデル・ログデータモデル・テレメトリスキーマのスペックです。
なぜ必要なのか
「リクエスト数」と「キューの長さ」と「室温」は、どれも数値ですが、性質が違います。リクエスト数は増えるだけで、キューの長さは増えたり減ったりし、温度は足し合わせて合計する意味がありません。計測器の種類が分かれている理由は、この性質をAPIの段階で明らかにしておくことで、SDKが適切なデフォルトの集計を選べるようにするためです。スペック概要はこれを「生の測定値を記録すれば、集計方法は最終ユーザーが選ぶ」と表現しています。
ログは逆方向の問題です。すでにすべての言語にロギングライブラリがあり、そのログを捨てて新しいAPIで書き直せとは言えません。そのため、ログシグナルは「既存のログをどう共通モデルに移し、トレースとつなぐか」から出発します。スキーマも同じ種類の問題です。セマンティック規約の属性名が変わると、その名前を期待していたダッシュボードとバックエンドが壊れます。
どう動くのか
計測器7種類
計測器は、名前・種類・単位(任意)・説明(任意)で定義されます。概念ドキュメントとAPIスペックの説明を合わせると、次のとおりです。
| 計測器 | 同期/非同期 | 性質 | 記録操作 |
|---|---|---|---|
| Counter | 同期 | 単調増加(増えるだけ) | Add |
| Asynchronous Counter | 非同期 | 単調増加、観測時点の累積値を報告 | コールバック |
| UpDownCounter | 同期 | 増加・減少の両方(キューの長さ) | Add |
| Asynchronous UpDownCounter | 非同期 | 足し合わせられる値(プロセスのヒープサイズ) | コールバック |
| Gauge | 同期 | 足し合わせられない値(騒音レベル)、変化したときに記録 | Record |
| Asynchronous Gauge | 非同期 | 足し合わせられない値(室温)、観測時点に報告 | コールバック |
| Histogram | 同期 | 分布に意味がある値(リクエストのレイテンシ) | Record |
同期計測器は、アプリケーションロジックの中で直接呼び出され、その測定値はContext(つまり現在のスパン)と関連付けられます。非同期計測器は、コールバックを登録しておくと、SDKが収集(collection)の時点にだけ呼び出します。スペックの例は、15秒ごとにセンサーの温度を読むことです。非同期の測定値はContextと関連付けられません。スペックは、ここでの同期・非同期がプログラミングのasyncパターンとは無関係だと付け加えています。UpDownCounterの説明には、「値が単調増加ならCounterを使うように」という指針があります。
各計測器にはデフォルトの集計があり、Viewで変更したり、特定の計測器を無視したり、報告する属性を選んだりできます。概念ドキュメントは、カーディナリティの限界も説明しています。メトリクスストリームあたりの固有の属性の組み合わせのデフォルトの上限は2000で、超えると測定値を捨てずに、otel.metric.overflow=trueという属性1つに折りたたみます。合計は合いますが、折りたたまれた測定値はほかの属性を失うため、その属性で絞り込むクエリは少なく数えます。
temporality: deltaとcumulative
データモデルでSumは、AggregationTemporality(deltaまたはcumulative)と、単調(monotonic)フラグ、そして開始・終了時刻を持つデータポイントで構成されます。deltaは、重ならない時間ウィンドウごとにその区間の値を報告し、cumulativeは、開始(通常はプロセスの開始)からの全体の合計を報告します。単調なdeltaの合計は負であってはならず、単調なcumulativeの合計は、以前の値より小さくなってはなりません。
スペックは、両者の間のトレードオフとして、プロセス再起動の検出、レート(rate)の計算、プッシュ/プル方式を挙げ、OTLPは両方をサポートし、必要ならDelta-to-CumulativeまたはCumulative-to-Deltaの変換を行えると書いています。ヒストグラムもtemporalityを持ち、minとmaxはdeltaのほうが有用で、delta→cumulativeの変換はできますが、その逆はできません。概念ドキュメントは、カーディナリティとの関係も指摘しています。deltaは周期ごとに状態を消すため、1周期の中の組み合わせだけを数えますが、cumulativeは状態を保持するため、一度上限に達するとプロセスが再起動するまで超過し続けます。Gaugeは特定の時刻のサンプル値なので、temporalityがありません。
ログレコードのフィールド
ログデータモデルは、「既存のログ形式を曖昧さなくこのモデルに移せる必要がある」という要件から出発し、よく使われるフィールドは名前付きの最上位フィールドに、残りはAttributesに置きます。最上位フィールドは12個です。
| フィールド | 意味 |
|---|---|
| Timestamp | イベントが発生した時刻(元の時計) |
| ObservedTimestamp | 収集システムが観察した時刻。SDKで作ったログではTimestampと同じ |
| TraceId / SpanId / TraceFlags | W3C Trace Contextの識別子とフラグ。リクエスト処理中のログに付く |
| SeverityText | 元のログレベルの文字列 |
| SeverityNumber | 正規化された深刻度の数値 |
| Body | 本文 |
| Resource | ログを出したエンティティ |
| InstrumentationScope | ログを出した範囲(ライブラリ・モジュール) |
| Attributes | 追加情報 |
| EventName | イベントの種類を表す名前 |
時刻が1つしかサポートされない場所へ移すときは、Timestampがあればそれを、なければObservedTimestampを使います。SeverityNumberは1–24の範囲で、1–4がTRACE、5–8がDEBUG、9–12がINFO、13–16がWARN、17–20がERROR、21–24がFATALで、0は未指定です。スペックの例のように、17は20より深刻度の低いエラーです。EventsはこのLogRecordの標準化された形式で、ログのセマンティック規約はEvent形式で定義されます。
ブリッジAPI
ログAPIは、最初の文で対象を明らかにしています。「ロギングライブラリの作者がログアペンダー(appender)を作れるように提供され、アペンダーはこのAPIで、既存のロギングライブラリとOpenTelemetryのログデータモデルの間をつなぐ」という内容です。構造は、LoggerProviderからLoggerを取得し(name、必要に応じてversion・schema_url・attributes)、LoggerがLogRecordをEmitするだけです。Emitは、Timestamp・ObservedTimestamp・Context・SeverityNumber・SeverityText・Body・Attributes・EventNameを受け取り、Contextを渡さなければ現在のContextを使います。これが、ログにTraceId・SpanIdが自動で付く仕組みです。Enabledは、LogRecordを作るコストが大きいときにだけ先に問い合わせる最適化で、呼び出しは必須ではありません。
ログシグナルの概要は、収集方式を2つに分けます。ファイルや標準出力に出たログをCollectorが読んでパースする方式(アプリケーションの変更はほとんどありませんが、パースが難しくなります)と、アペンダーを付けてOTLPで直接送る方式(構造化がうまくでき、ファイル・ローテーション・パースがなくなりますが、OTLPを受け取る宛先が必要です)です。どちらの場合も、アプリケーション開発者は、起動時にアペンダーとSDKを設定するだけで済みます。
スキーマURL
セマンティック規約は進化し、テレメトリのソースとコンシューマーは、それぞれ違う速度で変わります。スキーマスペックは、この3者を切り離すために、次のことを定めています。スキーマにはバージョンがあり(MAJOR.MINOR.PATCH)、バージョン間の変換(例: 属性名の変更)をスキーマファイルが明示し、各バージョンは固有のSchema URLで識別されます。URLの最後のパスがバージョンで、その前がスキーマファミリーです。スキーマファイルは発行されると不変なので、永続的にキャッシュしてもかまいません。ソース(計装ライブラリ)は、出力するテレメトリにスキーマURLを入れ(Tracer・Meter・Loggerを取得するときのschema_url引数がその位置です)、コンシューマーは、受け取ったスキーマバージョンを見て、必要なら自分が期待するバージョンに変換します。ドキュメントの例は、1.2.0のdeployment.environmentを、1.1.0を期待するバックエンドがenvironmentに変えて保存するというもので、バックエンドがスキーマを知らない場合は、Collectorのスキーマ変換プロセッサーが代わりに行います。OpenTelemetry自体のスキーマは、/schemas/<version>のパスで発行されます。
現場での姿
あるチームが「現在アクティブな接続数」をCounterで記録していたため、グラフが上がるだけでした。接続が閉じられるときに減る必要があるので、UpDownCounterが正しい選択で、実際には接続プールから値を読むコールバックのほうが自然なため、Asynchronous UpDownCounterに変更しました。
別のチームは、Prometheusに送るメトリクスとベンダーのバックエンドに送るメトリクスの値が違うと言っていましたが、片方はcumulative、もう片方はdeltaに設定されていました。同じCounterでも、temporalityが違うと、1つの点の数字が意味する区間が違います。
次のクイズで確認すること
クイズでは、APIとSDKを分離した理由、Batching processorのデフォルト値、同期計測器と非同期計測器の違いとContextとの関連付け、deltaとcumulativeの意味、LogRecordのTimestampとObservedTimestamp、ブリッジAPIの対象、スキーマURLの役割を問います。