OTEP-0256: エンティティデータモデル(第1部)
これはエンティティを表現するためのデータモデルの提案です。 このデータモデルの目的は、エンティティとは何か、エンティティのオブザーバビリティシステムがどのようなデータを記録・転送・保存・解釈する必要があるかについて、共通の理解を持つことです。
動機
このデータモデルは、OpenTelemetryにエンティティを追加するための基盤を確立するものです。 このデータモデルの大部分は、entities SIGの設立にあたって承認された最初の提案から借用しています。
このOTEPは、エンティティデータモデルを導入するにあたってのステップ1です。 後続のOTEPでは、リソース情報とエンティティの紐付けを含む、さらなるデータモデルの定義を追加していく予定です。
設計原則
OpenTelemetryの他の部分との一貫性が重要です。 私たちは、他のOpenTelemetryデータモデルと見た目や感触が似ているソリューションを強く支持します。
意味のある(特に人間が読める)IDは、ランダムに生成されたIDよりも価値があります。 状態の変化(エンティティの再起動など)を経ても存続する長寿命のIDは、短命で一時的なIDよりも価値があります。 ナビゲーションの必要性を参照してください。
エンティティのグローバルな識別に必要な情報のすべてが、一度に、一箇所で利用可能であるという前提を置くことはできません。 この知識は複数の参加者にまたがって分散している場合があり、グローバルに一意な識別子を形成するためには、それらを組み合わせる必要があります。
セマンティック規約は、テレメトリーにできる限り秩序をもたらさなければなりませんが、厳格すぎて実世界のユースケースを妨げるものであってはなりません。
データモデル
私たちは、エンティティという新しい概念を提案します。
エンティティとは、生成されたテレメトリー(トレース、メトリクス、ログ)に関連付けられる関心対象のオブジェクトを表します。
たとえば、OpenTelemetry SDKを使って生成されたテレメトリーは、通常Serviceエンティティに関連付けられます。 同様に、OpenTelemetryはホストのためのシステムメトリクスを定義していますが、この場合メトリクスを関連付けたいエンティティはHostです。
エンティティは、生成されたテレメトリーに間接的に関連付けられることもあります。 たとえば、テレメトリーを生成するサービスは、そのサービスが実行されているプロセスとも関係があるため、Serviceエンティティは Processエンティティに関連していると言えます。 通常、そのプロセスはホスト上でも実行されているため、Processエンティティは Hostエンティティに関連していると言えます。
注記: エンティティがトレース・メトリクス・ログとどのように関連付けられるか、またエンティティ間の関係がどのように指定されるかについては、後続のOTEPで定義します。 今後の作業を参照してください。
以下のデータモデルは、エンティティデータがどのような物理的なフォーマットやエンコーディングで記録されるかに関わらない、エンティティの論理モデルを定義します。
| フィールド | 型 | 説明 |
| Type | string | エンティティの型を定義します。エンティティの存続期間中に変化してはなりません(MUST not)。 たとえば "service" や "host" などです。このフィールドは必須であり、有効なエンティティにおいて空であってはなりません(MUST not)。 |
| Id | map<string, attribute> | エンティティを識別する属性です。 エンティティの存続期間中に変化してはなりません(MUST not)。Idは少なくとも1つの属性を含まなければなりません。 OpenTelemetryの共通の属性定義に従います。属性に関するOpenTelemetryのセマンティック規約に従うべきです(SHOULD)。 |
| Attributes | map<string, any> | エンティティの記述的な(識別に用いられない)属性です。 エンティティの存続期間中に変化してもかまいません(MAY)。空であってもかまいません(MAY)。これらの属性はエンティティの識別情報の一部ではありません。 OpenTelemetry仕様におけるany 値の定義に従います。スカラー値、バイト配列、値の配列やマップを取ることができます。配列やマップについては任意の深さのネストが許可されます。 属性に関するOpenTelemetryのセマンティック規約に従うべきです(SHOULD)。 |
必要最小限のID
一般に、テレメトリーの生成者はエンティティの複数の属性をすぐに利用できる状態にあり、そこからIDを組み立てます。
利用可能な属性のうち、エンティティIDにはそのエンティティを一意に識別するために十分な最小限の属性の集合を含めるべきです。
たとえば、ホスト上のProcessは(process.pid、process.start_time)という属性によって一意に識別できます。
これに加えて、たとえば process.executable.name 属性をIDに追加することは不要であり、必要最小限のIDというルールに違反します。
エンティティの例
このセクションは非規範的なものであり、データモデルを示す目的でのみ存在します。
以下は、エンティティの例、それらが通常持つ識別属性、およびそのエンティティに関連付けられる可能性のある非識別属性の例です。
| エンティティ | エンティティの型 | 識別属性 | 非識別属性 |
| Service | "service" | service.name(必須) service.instance.id service.namespace | service.version |
| Host | "host" | host.id | host.name host.type host.image.id host.image.name |
| K8s Pod | "k8s.pod" | k8s.pod.uid(必須) k8s.cluster.name | 任意のPodラベル |
| K8s Pod Container | "container" | k8s.pod.uid(必須) k8s.cluster.name container.name | 任意のコンテナラベル |
ID フィールドの構成の細部を示すさらなる例については、エンティティの識別セクションを参照してください。
エンティティイベント
エンティティに関する情報は、EntityStateとEntityDeleteという2種類のエンティティイベントを使って生成・伝達できます。
EntityStateイベント
EntityStateイベントは、ある特定の時点における エンティティの 状態 に関する情報を格納します。 EntityStateイベントのデータモデルは、いくつかの追加フィールドを除けば、エンティティのデータモデルと同じです。
| フィールド | 型 | 説明 |
| Timestamp | nanoseconds | このイベントが記述するエンティティの状態が、いつから成立しているかを示す時刻です。 この時刻はオリジンクロックによって計測されます。このフィールドは必須です。 |
| Interval | milliseconds | レポート周期、すなわちエンティティが変化していない場合でも、そのエンティティに関する情報がEntityStateイベントを通じてどれくらいの頻度で報告されるかを定義します。 このエンティティに対して次に予期されるEntityEventは、(Timestamp + Interval)の時刻に予期されます。 受信者はこの値を使って、EntityDeleteイベントが観測されなかった場合でも、もはや報告されなくなったエンティティが消滅したと推定できます。 任意項目であり、欠落している場合はintervalは不明です。 |
| Type | データモデルを参照してください。 | |
| Id | データモデルを参照してください | |
| Attributes | データモデルを参照してください |
私たちは、エンティティの記述的な属性のうち1つ以上が変化したとき、そのエンティティが変異(変化)すると言います。 新しい記述的属性が追加されたり、既存の記述的属性が削除されたり、既存の記述的属性の値が変更されたりすることがあります。 これらの変化はすべて、時間の経過に伴うエンティティの正当な変異を表します。 これらの変異が発生しても、エンティティの識別情報は変化しません。
エンティティの状態が変化したときには、送信元が新しいタイムスタンプと他のすべてのフィールドの完全な値のリストを持つ、新しいEntityStateイベントを発行することが期待されます。
エンティティイベントの生成者は、エンティティが変化していない場合でも、定期的にイベントを発行すべきです(SHOULD)。 この場合、Type、ID、Attributeフィールドは同じままですが、新しいTimestampがイベントに記録されます。 このようなイベントを生成することで、システムはイベントの損失に対して耐性を持つことができます。 たとえ一部のイベントが失われたとしても、最終的にはエンティティの正しい状態が最終的な宛先に配信される可能性が高くなります。 EntityStateイベントを定期的に送信することは、生存確認の指標としても機能します(EntityDeleteイベントの代わりにどのように使えるかは後述します)。
EntityDeleteイベント
EntityDeleteイベントは、特定のエンティティが消滅したことを示します。
| フィールド | 型 | 説明 |
| Timestamp | nanoseconds | エンティティが削除された時刻です。この時刻はオリジンクロックによって計測されます。このフィールドは必須です。 |
| Type | データモデルを参照してください | |
| Id | データモデルを参照してください |
エンティティが消滅したときに、EntityDeleteが必ず送信されるとは限らないことに注意してください。 エンティティシグナルの受信者は、EntityStateイベントの報告がもはや見られなくなったエンティティを期限切れとして扱うことで、この状況に対処できるように準備しておく必要があります(つまり、EntityStateイベントの有無を生存確認の指標として扱います)。
この期限切れの仕組みは、以前に報告されたEntityStateイベントの Interval フィールドに基づいています。
受信者はこの値を使って次のEntityStateイベントがいつ到着するかを見積もることができ、(多少の許容誤差を加えた上で)そのイベントがタイムリーに到着しなかった場合、EntityDeleteイベントが観測されなかったとしても、そのエンティティが消滅したとみなすことができます。
エンティティの識別
データモデルはエンティティIDフィールドの構造を定義します。 このセクションでは、IDフィールドがどのように計算されるかを説明します。
LID、GID、IDCONTEXT
すべてのエンティティは、ローカルID(LID)とグローバルID(GID)を持ちます。
LIDは特定の識別コンテキスト内で一意ですが、必ずしもグローバルに一意であるとは限りません。 たとえば、Processエンティティの LIDはそのPID番号とプロセスの開始時刻です。 (PID, StartTime)のペアは、そのプロセスが実行されているホストのコンテキスト内でのみ一意です(この場合、ホストが識別コンテキストとなります)。
エンティティのGIDは、特定のテレメトリーストア内のエンティティの集合全体において、同じGID値を持つエンティティが2つと存在しないという意味で、グローバルに一意です。
エンティティEのGIDは次のように定義されます。
GID(E) = UNION( LID(E), GID(IDCONTEXT(E)) )
ここで IDCONTEXT(E) は、エンティティEのLIDが一意である識別コンテキストです。
IDCONTEXT(E) の値はそれ自体がエンティティであるため、そのGID値も同様に計算できます。
言い換えれば、エンティティのGIDは、そのLIDと、その識別コンテキストのGIDの和集合です。 注記: GID(E)はキーと値の属性のマップです。
IDCONTEXT(E) の値を決定し、上記の式に従ってGIDを計算する処理は、しばしばエンリッチメント処理の責任範囲となりますが、GIDが追加のエンリッチメントを必要とせずにテレメトリーの発生源(たとえばOTel SDK)によって一度に生成されることもあります。
セマンティック規約
OpenTelemetryのセマンティック規約は、Service、Process、Hostなどのよく知られたエンティティについて、エンティティの定義を含むよう拡張されます。
よく知られたエンティティ型については、LID(E)はエンティティ型ごとにOTelセマンティック規約で定義されます。 LIDの値はキーと値の属性のマップです。 たとえば、“process” 型のエンティティについては、セマンティック規約は次の2つの属性からなるLIDを定義します。
{
"process.pid": $pid,
"process.start_time": $starttime
}
カスタムのエンティティ型(OTelセマンティック規約で定義されていないもの)については、エンドユーザーが同様の方法で自身のカスタムセマンティック規約を定義する責任を負います。
エンティティ情報の生成者は、自身が情報を生成しているそれぞれのエンティティの識別コンテキストを決定する責任を負います。
意味のあるIDCONTEXTの定義が1つしかないような特定のケースでは、IDCONTEXTがセマンティック規約の中で定義されることがあります。 たとえば、Kubernetesのノードは常にKubernetesクラスターという識別コンテキストの中に存在します。 “k8s.node” と “k8s.cluster” のセマンティック規約は、“k8s.node” 型のエンティティのIDCONTEXTが常に “k8s.cluster” 型のエンティティであると規定できます。
重要: セマンティック規約は、GIDの完全な構成を規定することを期待されていません(そして通常そうしません)。 セマンティック規約はLIDを規定すべきであり、IDCONTEXTを規定してもかまいませんが、GIDの構成は一般に静的には知り得ません。
たとえば、ホストのLIDは host.id 属性であるべきです。
クラウド上で稼働しているホストのIDCONTEXTは “cloud.account” であるべきで、“cloud.account” エンティティのLIDは(cloud.provider、cloud.account.id)です。
しかし、すべてのホストがクラウド上で稼働しているわけではないため、セマンティック規約はホストのGIDが(host.id、cloud.provider、cloud.account.id)であると規定することはできません。
単一のデータセンター内でオンプレミスで稼働しているホストは、単に(host.id)だけをGIDとして持つかもしれませんし、顧客が複数のオンプレミスデータセンターを持っている場合には、data.center.id をその識別子として使い、(host.id、data.center.id)をホストのGIDとして使うかもしれません。
例
このセクションは補足的なガイドラインであり、論理データモデルの一部ではありません。
ホスト上のProcess
“process” エンティティに関する情報を生成する、ローカルで稼働しているホストエージェント(たとえばOTel Collector)は、プロセスが特定のホスト上で実行されているという知識を持っているため、“host” がそのエージェントが観測するプロセスの識別コンテキストとなります。 Process のLIDは次のようになります。
{
"process.pid": 12345,
"process.start_time": 1714491491
}
そしてCollectorは “host” をIDCONTEXTとして使い、ホストのLIDをこれに追加します。
{
// Process LID, unique per host.
"process.pid": 12345,
"process.start_time": 1714491491,
// Host LID
"host.id": "fdbf79e8af94cb7f9e8df36789187052"
}
データセンターが1つしか存在せず、ホストIDがグローバルに一意であると仮定すると、上記のIDはグローバルに一意であり、Processのグローバル一意なGIDとなります。 この仮定が私たちの状況において成り立たない場合には、GIDがグローバルに一意になるまで追加のIDCONTEXTを適用し続けることになります。 たとえば、下記のクラウドアカウント内のHostの例を参照してください。
Kubernetes内のProcess
(Kubernetes内で実行されている)OTel Collectorは、プロセス エンティティに関する情報を生成する際、プロセスが特定のPod内の特定のコンテナで実行されているという知識を持っているため、コンテナがそのプロセスの識別コンテキストであり、Podがそのコンテナの識別コンテキストとなります。 先ほどと同じProcessのLIDから始めるとします。
{
"process.pid": 12345,
"process.start_time": 1714491491
}
すると、CollectorはこれにコンテナとpodのIDCONTEXTを追加し、次のようになります。
{
// Process LID, unique per container.
"process.pid": 12345,
"process.start_time": 1714491491,
// Container LID, unique per pod.
"k8s.container.name": "redis",
// Pod LID has 2 attributes.
"k8s.pod.uid": "0c4cbbf8-d4b4-4e84-bc8b-b95f0d537fc7",
"k8s.cluster.name": "dev"
}
上記ではGIDを組み立てるために3つの異なるLIDを使用したことに注意してください。 それぞれのLIDに含まれる属性は、OTelセマンティック規約で定義されています。
この例では、Podがルートとなる IDCONTEXTであるためこれを有効なGIDと仮定しています。 なぜなら、Podの LIDにはグローバルに一意であると期待されるクラスター名が含まれているからです。 クラスター名がグローバルに一意であるというこの仮定が誤っている場合には、クラスター名が一意になるような別の包含的IDCONTEXTを適用する必要が生じます。
また、(k8s.pod.uid、k8s.cluster.name)のペアを使用したことにも注目してください。
別の方法として、Kubernetesクラスターを私たちが関心を持つ別のエンティティであると考えることもできます。
これは、Podの IDCONTEXTがクラスターであることを意味します。
Process のGIDに対する最終的な結果はまったく同じになりますが、異なる方法でそこにたどり着くことになります。
{
// Process LID, unique per container.
"process.pid": 12345,
"process.start_time": 1714491491,
// Container LID, unique per pod.
"k8s.container.name": "redis",
// Pod LID, unique per cluster.
"k8s.pod.uid": "0c4cbbf8-d4b4-4e84-bc8b-b95f0d537fc7",
// Cluster LID, also globally unique since cluster is root entity.
"k8s.cluster.name": "dev"
}
クラウドアカウント内のHost
クラウドアカウント(たとえばAWS)上で稼働しているHostは、単一のクラウドアカウント内で一意なホストインスタンスIDを使うLIDを持ちます。 たとえば次のようになります。
{
// Host LID, unique per cloud account.
"host.id": "fdbf79e8af94cb7f9e8df36789187052"
}
“AWS” ディテクターを有効にした resourcedetection プロセッサーを使うOTel Collectorは、次のようにクラウドアカウントのIDCONTEXTを追加します。
{
// Host LID, unique per cloud account.
"host.id": "fdbf79e8af94cb7f9e8df36789187052"
// Cloud account LID has 2 attributes:
"cloud.provider": "aws",
"cloud.account.id": "1234567890"
}
プロトタイプ
このデータモデルを示すための一連のプロトタイプが実装されています。
先行技術
実験的なエンティティデータモデルは、このドキュメントに記載されているように、OpenTelemetry Collectorに実装されました。 Collectorの設計では、エンティティイベントの運び手としてLogRecordが使われており、その論理構造はこのOTEPが提案するものとほぼ同一です。
この設計はCollector内にも実装があります。
エンティティイベントを追加した完了済みのIssueと、Collectorの k8scluster レシーバーに対してエンティティイベントの発行を実装したPRを参照してください。
代替案
異なるID構造
エンティティIDフィールドに異なる構造を使うための代替案がここと ここで提案されました。
私たちは、以下の理由から、これらの提案を却下し、このOTEPで提案するIDフィールドを採用しました。
キーと値の属性のマップは、Resourceの属性、Scopeの属性、Metricのデータポイントの属性など、OpenTelemetryの他の場所で広く使われているため、OTelの他の部分と概念的に一貫性があります。
私たちはすでに、この属性の定義とうまく連携する多くの仕組みを持っています。 たとえば、OTTL言語には属性を扱うための構文がありますし、Collectorの pdata APIやSDKの属性値型もそうです。 もし異なるデータ構造を採用すると、これらのコードはすべてそのままでは動作しなくなり、別の方法で再実装する必要が生じます。
エンティティイベントなし
エンティティシグナルによって、エンティティの状態を記録できます。 エンティティの状態が変化するにつれて、新しい状態を記述するイベントが発行されます。 この提案では、エンティティの状態は(type, id, attributes)のタプルですが、将来的にはエンティティ間の関係(すなわち、Processが Hostで実行されているという事実)を記録するために、エンティティシグナルにさらなる情報を追加したいと考えています。
エンティティイベントのデータをResourceに統合する
エンティティシグナルという概念を排除し、エンティティの状態全体をResourceに格納するとすると、エンティティの状態が変化するたびに、そのエンティティの状態を表すResourceを含むResourceLogs/ResourceSpans/ResourceMetricsのいずれかのメッセージを発行しなければなりません。
しかし、報告すべきログやスパン、メトリクスのデータポイントが存在しない場合はどうすればよいのでしょうか。 空のログやスパン、メトリクスのデータポイントを含むResourceLogs/ResourceSpans/ResourceMetricsというOTLPメッセージを発行するのでしょうか。 ResourceLogs、ResourceSpans、ResourceMetricsのうち、どれを発行するのでしょうか。
将来、エンティティ間の関係を記録するサポートを追加したい場合はどうすればよいのでしょうか。 そのすべての情報をResourceに追加して、Resourceのサイズを肥大化させるのでしょうか。
EntityDeleteイベントはどのように報告すればよいのでしょうか。
これらの問いのすべてに、良い答えはありません。 エンティティ情報を、それが本来自然には収まらないResourceに無理やり押し込もうとすると、醜く非効率なソリューションになる可能性が高いでしょう。
階層的なIDフィールド
私たちには、LIDとIDCONTEXTから構成されたIDに関する情報を保持する、つまり特定のIDを生成した構成過程に関する情報を失わせてフラット化するのではなく、識別コンテキストの階層をIDのデータ構造の中に記録するという代替案もありました。
これには次のような理由があります。
フラットなID構造の方が単純です。
階層的なID構造を必要とする既知のユースケースはありません。 「エンティティ間の親子関係を記録する」というユースケースは、別途の関係データ構造を通じて明示的に扱われることになります(今後の作業を参照)。
未解決の課題
属性のデータ型
このデータモデルでは、より複雑なデータ型を許容する拡張された any属性値をAttributesフィールドに使うことを要求しています。 これは、より制限された形状を持つIDフィールドで使われるデータ型とは異なります。
この不一致に私たちは満足すべきでしょうか。
対応するTODO項目はこちらです。
エンティティ型のクラス
インフラストラクチャエンティティ(Pod、Host、Processなど)と、Serviceのような非インフラストラクチャエンティティ(論理エンティティ)とを区別する必要があるのでしょうか。 この区別は重要なのでしょうか。
対応するTODO項目はこちらです。
複数の観測者
同一のエンティティが、複数の異なる観測者によって同時に観測されることがあります。 たとえば、Hostに関する情報は、そのホスト上で稼働しているエージェントによって報告されるかもしれません。 それと同時に、そのホストに関するさらなる情報がクラウドプロバイダーのAPIを通じて得られるかもしれません。
異なる観測者によって得られる情報は互いに補完的であり、必ずしも同じデータにアクセスできるとは限りません。 バックエンドでこの情報を組み合わせ、ユーザーがすべて利用できるようにすることは、非常に有用です。
しかし、本ドキュメントで先に定義したEntityStateイベントを、複数の観測者が同時に使用することはできません。 なぜなら、あるイベントの情報は、同じエンティティについて以前受信した情報を上書きしてしまうからです。
複数の観測者が同じエンティティに関する情報の一部分を同時に報告できるようにする1つの方法は、EntityStateイベントに “ObserverId” フィールドを追加して観測者を示すことです。 EntityStateイベントは、次のようになります。
| フィールド | 型 |
|---|---|
| Timestamp | nanoseconds |
| Interval | milliseconds |
| Type | |
| ID | |
| Attributes | |
| ObserverId | string or bytes |
ObserverIdフィールドは任意項目とすることができます。 異なるObserverId値を含むEntityStateイベントの属性は、バックエンドでマージされます。 同じObserverId値を含むEntityStateイベントの属性は、その観測者による以前のEntityStateイベントの報告に含まれていた属性を上書きします。
対応するTODO項目はこちらです。
Typeはエンティティの識別情報の一部か
Typeフィールドは、Idフィールドとともにエンティティの識別情報の一部なのでしょうか。
たとえば、Hostと、そのHost上で稼働しているOTel Collectorがあると仮定します。
Hostの Idには host.id という1つの属性が含まれ、エンティティのTypeは “host” になります。
Collectorも技術的には host.id という1つの属性によって識別可能であり、エンティティのTypeは “otel.collector” になります。
これが成り立つのは、Typeフィールドをエンティティの識別情報の一部とみなす場合に限られます。
Typeフィールドが識別情報の一部でない場合、上記の例ではCollectorを記述するエンティティのIdに何か別の属性(たとえば、受け入れられた場合の agent.type 属性)を含める必要があります。
対応するTODO項目はこちらです。
複数のIDからの選択
場合によっては、同一のエンティティが2通り以上の方法で識別されることがあります。
たとえば、Podはその k8s.pod.uid によって識別できますが、k8s.namespace.name、k8s.pod.name という属性のペアによっても識別できます。
有効な識別子が複数存在する場合に、それらの中からどのように選択すべきかについての推奨事項を提供する必要があります。
対応するTODO項目はこちらです。
今後の作業
このOTEPは、エンティティデータモデルを定義するステップ1です。 以下のトピックを扱う、その他のOTEPが続く予定です。
既存のResourceの概念が、既存のシグナルをエンティティにリンクさせるためにどのように変更されるか。
エンティティ間の関係がどのようにモデル化されるか。
エンティティデータのワイヤー上での表現と、エンティティの伝送プロトコル。
OpenTelemetryのスキーマファイルにおいて、エンティティのセマンティック規約の変更を記述する変換の追加。
未解決の課題に対応するための追加のOTEPも、おそらく提出することになるでしょう。