プロファイルデータフォーマット

この記事は英語の原文を日本語に翻訳したものです。原文: https://opentelemetry.io/docs/specs/otel/profiles/data-format/

翻訳元: open-telemetry/opentelemetry-specification v1.60.0(コミット 29ae8c7

ステータス: アルファ(Alpha)

概要

OpenTelemetryのプロファイル用データフォーマットは、集約されたスタックトレースと関連メタデータのエンコードおよび配送のためのプロトコル仕様と、セマンティック規約から構成されます。

プロトコル仕様はprofiles.protoというprotobufファイルで定義されており、pprofフォーマットに基づいています。これはつまり、pprofをこのデータフォーマットへ一意にマッピングできることを意味します。対象のプロファイリングフォーマットが同等の機能を持つ範囲であれば、このデータフォーマットからの無損失な逆マッピングも可能です。

以下の図はメッセージ間の関係を示しています。実線の矢印は埋め込み関係を表し、破線の矢印はディクショナリテーブルへのインデックスによる参照を表します。

graph TD
    ProfilesData -->|"1-n"| ResourceProfiles
    ProfilesData -->|"1"| ProfilesDictionary
    ResourceProfiles -->|"1-n"| ScopeProfiles
    ScopeProfiles -->|"1-n"| Profile
    Profile -->|"1-n"| Sample

    Sample -. "n-1" .-> Stack
    Sample -. "n-n" .-> KeyValueAndUnit
    Sample -. "n-1" .-> Link

    Stack -. "n-n" .-> Location

    Location -->|"1-n"| Line
    Location -. "n-n" .-> KeyValueAndUnit
    Location -. "n-1" .-> Mapping

    Line -. "n-1" .-> Function

    Mapping -. "n-n" .-> KeyValueAndUnit

他シグナルとの違い

プロファイラーは大量のデータを生成し、ユーザーはプロファイリングが引き起こすオーバーヘッドに対して非常に敏感です。それを踏まえ、プロファイルのデータフォーマットは、pprofや他のOpenTelemetryのエコシステムとの互換性を保ちながらペイロードサイズを小さく、処理コストを低く保つために、他のOpenTelemetryシグナルとは異なる設計にしています。

メッセージの埋め込み

ほとんどのOpenTelemetryシグナルは、直接(「値による」)埋め込みを使います。トレース内のスパンはそのイベントとリンクを埋め込み、ログレコードはその属性をインラインで含みます。

プロファイルは「値による」埋め込みと「参照による」埋め込みの両方の方式を使います。

  1. 直接埋め込みは、外側の階層(ProfilesDataResourceProfilesScopeProfilesProfileSample)と(LocationLine)に使われます。
  2. インデックスによる参照は、それ以外のすべての関係で、共有されるディクショナリに対して使われます。サンプルは、スタック・属性・リンクを直接埋め込むのではなく、インデックスによって参照します。

ディクショナリ

プロファイルのデータフォーマットは、ProfilesDataメッセージ全体で共有されるデータを重複排除するために、トップレベルのディクショナリメッセージ(ProfilesDictionary)を使います。他のOpenTelemetryシグナルでは各レコードがほぼ自己完結的であるのに対し、プロファイルは重複排除によって利益を得られるほど繰り返しの多いデータを大量に含みます。共有ディクショナリを参照することで、プロデューサーは同じバイト列を繰り返し格納・転送することを避け、プロファイルペイロードの通信量を大幅に削減できます。

トップレベルのディクショナリは、重複排除されるデータの種類ごとに1つずつ、追加のディクショナリテーブルを埋め込みます。埋め込まれた各ディクショナリテーブルは、要素がインデックスで参照される配列としてエンコードされます。

属性

このデータフォーマットは、2種類の属性を使います。

  1. 標準のKeyValue属性: 他のシグナルで使われているものと同じKeyValueのペアです。これらはResourceメッセージとInstrumentationScopeメッセージに現れ、通常のOpenTelemetryの属性のセマンティクス(キーの一意性、単位なし)に従いますが、プロファイル固有の拡張として、キーと値の文字列参照フィールドを持ちます(次の節を参照)。

  2. KeyValueAndUnit属性: プロファイル固有の属性エンコード方式です。これらはProfilesDictionary.attribute_tableに格納され、ProfileSampleMappingLocationの各メッセージからインデックスで参照されます。キーと値に加えて、オプションのunitフィールドを持ち、セマンティック規約だけに頼るのではなく、"allocation_size": 128 By(単位はUCUM)のような属性の単位を明示的に表現できます。

KeyValueにおけるディクショナリの利用

ペイロードサイズを最小化するため、このデータフォーマットは標準のOpenTelemetryのKeyValueメッセージとAnyValueメッセージを、ProfilesDictionary.string_tableを指す文字列参照フィールドで拡張しています。

これは、Resource属性が、単一のProfilesDataメッセージ内の多数のプロファイルやサンプルにわたって同じ文字列値(service.namehost.nameなど)を頻繁に繰り返すために行われています。

メッセージの説明

Message ProfilesData

ProfilesDataはトップレベルのメッセージであり、永続ストレージに格納できるデータ、またはOTLPプロトコルを実装しないがOTLPプロファイルを転送する他のプロトコルによって埋め込まれるデータをカプセル化します。

詳細については、protobufの仕様を参照してください。

Message ProfilesDictionary

ProfilesDictionaryは、ProfilesDataメッセージ全体で共有されるすべてのディクショナリテーブルを含みます。

詳細については、protobufの仕様を参照してください。

Message ResourceProfiles

Resourceから得られるScopeProfilesの集合です。

詳細については、protobufの仕様を参照してください。

Message ScopeProfiles

InstrumentationScopeによって生成されたProfileメッセージの集合です。

詳細については、protobufの仕様を参照してください。

Message Profile

完全なプロファイルを表します。サンプル種別、サンプル、バイナリへのマッピング、スタック、ロケーション、関数、および関連メタデータを含みます。

詳細については、protobufの仕様を参照してください。

Message Sample

Sampleは、プログラムのコンテキスト(通常はスタックトレース)で観測された値を記録し、スレッドIDやより高レベルなリクエストコンテキストなどの補助情報が付加されることもあります。

詳細については、protobufの仕様を参照してください。

一意なトレースIDとスパンIDによって識別される、プロファイルのSampleからトレーススパンへのポインタです。

詳細については、protobufの仕様を参照してください。

Message Stack

Location(末端が先頭)のリストとしてエンコードされたスタックトレースです。

詳細については、protobufの仕様を参照してください。

Message Location

単一のフレームに関する関数情報と行テーブルのデバッグ情報を含みます。

詳細については、protobufの仕様を参照してください。

Message Line

関数に紐づけられた、ソースコード中の特定の行の詳細を示します。

詳細については、protobufの仕様を参照してください。

Message Mapping

アドレス範囲、ファイルオフセット、ビルドIDなどのメタデータを含む、メモリ上のバイナリのマッピングを表します。Mappingメッセージの必須属性については、Mappingsを参照してください。

詳細については、protobufの仕様を参照してください。

Message Function

人間が読める名前、システム名、ソースファイル、開始行番号を含む、関数を表します。

詳細については、protobufの仕様を参照してください。

Message ValueType

値の型と単位を表します。

詳細については、protobufの仕様を参照してください。

Message KeyValueAndUnit

キーにProfilesDictionary.string_tableを使う、属性のカスタムなディクショナリネイティブのエンコーディングであり、オプションの単位情報をエンコードできます。

詳細については、protobufの仕様を参照してください。

他シグナルとの関係

OpenTelemetryプロファイルは、以下の2つの次元にわたって他のシグナルとの双方向リンクをサポートします。

  • リソースコンテキストによる相関
  • 直接参照による相関

リソースコンテキストによる相関とは、単に同じサービスインスタンスなど、関連するログ・メトリクス・トレースを発生させたのと同じResourceにプロファイルデータをリンクすることです。

プロファイルと他のシグナルの間には、2種類の直接参照関係があります。

  • プロファイルから他シグナルへ
  • 他シグナルからプロファイルへ

プロファイルから他シグナルへ

Linkは、trace_idspan_idを介して、プロファイルのSampleをトレーススパンに接続します。ログやメトリクスなど他のシグナルも同じトレース・スパン識別子を使う場合があるため、プロファイルはこの共有されたトレースコンテキストを通じてそれらのシグナルと相関できます。

他シグナルからプロファイルへ

他のシグナルは、Profileメッセージ上のprofile_idフィールドを使ってプロファイルを参照できます。例えば、ログレコードは、そのログレコードが生成された時点で収集されたプロファイルを参照するためにprofile_id属性を持つことができます。profile_idフィールドは現在、発生源ではオプションですが、収集後(例えばOpenTelemetry Collectorの処理パイプライン内)に設定される場合があることに注意してください。

さらに、trace_idspan_idは、プロファイル中のSample(個別のサンプルではなくグループ単位)を参照するために使うことができます。これは、サンプルがLinkメッセージを使ってトレースとリンクされているためです。

ペイロード例

シンプルなCPUプロファイル

以下の例は、20Hz(実際のCPU実行時間50msごとに1サンプル)で動作するサンプリングプロファイラーによって収集されたオンCPUプロファイルを示しています。2つの一意なスタックトレースが観測されました。1つ(3回観測)は呼び出しスタックmain -> foo -> barを持ち、もう1つ(2回観測)は呼び出しスタックmain -> bazを持ちます。

わかりやすくするため、文字列とディクショナリのインデックスをインラインで示しています。

ProfilesData {
  dictionary: ProfilesDictionary {
    string_table: ["", "samples", "count", "cpu", "nanoseconds",
                   "main", "foo", "bar", "baz",
                   "main.go", "foo.go", "bar.go", "baz.go"]
    function_table: [
      Function {},                                          // index 0: null
      Function { name_strindex: 5, filename_strindex: 9 },  // index 1: main
      Function { name_strindex: 6, filename_strindex: 10 }, // index 2: foo
      Function { name_strindex: 7, filename_strindex: 11 }, // index 3: bar
      Function { name_strindex: 8, filename_strindex: 12 }, // index 4: baz
    ]
    location_table: [
      Location {},                                                // index 0: null
      Location { lines: [Line { function_index: 1, line: 10 }] }, // index 1: main
      Location { lines: [Line { function_index: 2, line: 20 }] }, // index 2: foo
      Location { lines: [Line { function_index: 3, line: 30 }] }, // index 3: bar
      Location { lines: [Line { function_index: 4, line: 40 }] }, // index 4: baz
    ]
    stack_table: [
      Stack {},                              // index 0: null
      Stack { location_indices: [3, 2, 1] }, // index 1: bar <- foo <- main
      Stack { location_indices: [4, 1] },    // index 2: baz <- main
    ]
    mapping_table:   [Mapping {}]
    link_table:      [Link {}]
    attribute_table: [KeyValueAndUnit {}]
  }
  resource_profiles: [ResourceProfiles {
    scope_profiles: [ScopeProfiles {
      profiles: [Profile {
        sample_type: ValueType { type_strindex: 1, unit_strindex: 2 } // "samples", "count"
        samples: [
          Sample { stack_index: 1, values: [3] },
          Sample { stack_index: 2, values: [2] },
        ]
        time_unix_nano: 1234567890000000000
        duration_nano:  1000000000
        period_type: ValueType { type_strindex: 3, unit_strindex: 4 } // "cpu", "nanoseconds"
        period: 50000000 // 50ms = 20Hz
      }]
    }]
  }]
}

リソース属性とスパンリンクを伴うCPUプロファイル

この例は、リソース属性(service.nameprocess.executable.name)と、あるサンプルに対するスパンリンクを持つプロファイルを示しており、トレースとの相関を示しています。

このリソース属性(プロファイル固有のKeyValueAndUnit属性ではなく、標準のKeyValue属性)は、ProfilesDictionaryへの文字列参照を使っています。

ProfilesData {
  dictionary: ProfilesDictionary {
    string_table: ["", "samples", "count", "cpu", "nanoseconds",
                   "handleRequest", "db.Query", "server.go", "db.go",
                   "service.name", "process.executable.name",
                   "my-service", "my-service.bin"]
    function_table: [
      Function {},                                         // index 0: null
      Function { name_strindex: 5, filename_strindex: 7 }, // index 1: handleRequest
      Function { name_strindex: 6, filename_strindex: 8 }, // index 2: db.Query
    ]
    location_table: [
      Location {},                                                 // index 0: null
      Location { lines: [Line { function_index: 1, line: 45 }] },  // index 1: handleRequest
      Location { lines: [Line { function_index: 2, line: 112 }] }, // index 2: db.Query
    ]
    stack_table: [
      Stack {},                           // index 0: null
      Stack { location_indices: [2, 1] }, // index 1: db.Query <- handleRequest
      Stack { location_indices: [1] },    // index 2: handleRequest
    ]
    link_table: [
      Link {},                                     // index 0: null
      Link {                                       // index 1
        trace_id: 1122aabbccddeeff0000000000000000
        span_id:  ff01020304050607
      },
    ]
    mapping_table:   [Mapping {}]
    attribute_table: [KeyValueAndUnit {}]
  }
  resource_profiles: [ResourceProfiles {
    resource: Resource {
      attributes: [
        { key_strindex: 9, value: { string_value_strindex: 11 } }, // "service.name", "my-service"
        { key_strindex: 10, value: { string_value_strindex: 12 } }, // "process.executable.name", "my-service.bin"
      ]
    }
    scope_profiles: [ScopeProfiles {
      profiles: [Profile {
        sample_type: ValueType { type_strindex: 1, unit_strindex: 2 } // "samples", "count"
        samples: [
          Sample { stack_index: 1, values: [5], link_index: 1 }, // Linked to trace span
          Sample { stack_index: 2, values: [3] },                // No span link
        ]
        time_unix_nano: 2000000000000000000
        duration_nano:  1000000000
        period_type: ValueType { type_strindex: 3, unit_strindex: 4 } // "cpu", "nanoseconds"
        period: 50000000 // 50ms = 20Hz
      }]
    }]
  }]
}

参考資料