トレース

アプリケーションを通過するリクエストの経路

トレース は、リクエストがアプリケーションに投げられたときに何が起こるかの全体像を教えてくれます。 あなたのアプリケーションが、単一のデータベースを持つモノリスであろうと、洗練されたメッシュサービスであろうと、トレースは、リクエストがアプリケーションの中でたどる完全な「経路」を理解するために不可欠です。

スパンで表現される以下の3つのJSONデータで、これを探ってみましょう。

hello スパンは次のとおりです。

{
  "name": "hello",
  "context": {
    "trace_id": "0x5b8aa5a2d2c872e8321cf37308d69df2",
    "span_id": "0x051581bf3cb55c13"
  },
  "parent_id": null,
  "start_time": "2022-04-29T18:52:58.114201Z",
  "end_time": "2022-04-29T18:52:58.114687Z",
  "attributes": {
    "http.route": "some_route1"
  },
  "events": [
    {
      "name": "Guten Tag!",
      "timestamp": "2022-04-29T18:52:58.114561Z",
      "attributes": {
        "event_attributes": 1
      }
    }
  ]
}

これはルートスパンであり、オペレーション全体の始まりと終わりを示します。 トレースを示す trace_id フィールドがありますが、parent_id がないことに注意してください。 これがルートスパンであることを示します。

hello-greetings スパンは次のとおりです。

{
  "name": "hello-greetings",
  "context": {
    "trace_id": "0x5b8aa5a2d2c872e8321cf37308d69df2",
    "span_id": "0x5fb397be34d26b51"
  },
  "parent_id": "0x051581bf3cb55c13",
  "start_time": "2022-04-29T18:52:58.114304Z",
  "end_time": "2022-04-29T22:52:58.114561Z",
  "attributes": {
    "http.route": "some_route2"
  },
  "events": [
    {
      "name": "hey there!",
      "timestamp": "2022-04-29T18:52:58.114561Z",
      "attributes": {
        "event_attributes": 1
      }
    },
    {
      "name": "bye now!",
      "timestamp": "2022-04-29T18:52:58.114585Z",
      "attributes": {
        "event_attributes": 1
      }
    }
  ]
}

このスパンは、挨拶(greetings)のような特定のタスクをカプセル化していて、その親は hello スパンです。 このスパンはルートスパンと同じ trace_id を共有していて、同じトレースの一部であることを示しています。 さらに、 hello スパンの span_id と一致する parent_id を持っています。

hello-salutations スパンは次のとおりです。

{
  "name": "hello-salutations",
  "context": {
    "trace_id": "0x5b8aa5a2d2c872e8321cf37308d69df2",
    "span_id": "0x93564f51e1abe1c2"
  },
  "parent_id": "0x051581bf3cb55c13",
  "start_time": "2022-04-29T18:52:58.114492Z",
  "end_time": "2022-04-29T18:52:58.114631Z",
  "attributes": {
    "http.route": "some_route3"
  },
  "events": [
    {
      "name": "hey there!",
      "timestamp": "2022-04-29T18:52:58.114561Z",
      "attributes": {
        "event_attributes": 1
      }
    }
  ]
}

このスパンはこのトレースにおける3つ目の操作を表し、前のスパンと同様にhelloスパンの子です。 また、hello-greetingsスパンの兄弟でもあります。

これらの3つのJSONブロックはすべて同じ trace_id を共有していて、parent_id フィールドは階層を表しています。 これは1つのトレースになります!

もうひとつ、各スパンが構造化されたログのように見えることにお気づきでしょう。 それはその通りだからです!トレースについて考える一つの方法は、トレースはコンテキスト、相関関係、階層構造などを持つ構造化されたログの集まりであるということです。 しかし、これらの「構造化されたログ」は、異なるプロセス、サービス、VM、データセンターなどから来る可能性があります。 これにより、トレースはあらゆるシステムのエンドツーエンドのビューを表現できます。

OpenTelemetryでのトレースがどのように機能するかを理解するために、コードの計装の一翼を担う一連のコンポーネントを見てみましょう。

トレーサープロバイダー

トレーサープロバイダー(TracerProvider と呼ばれることもあります)は Tracer のファクトリーです。 ほとんどのアプリケーションでは、トレーサープロバイダーは一度だけ初期化され、そのライフサイクルはアプリケーションのライフサイクルと一致します。 トレーサープロバイダーの初期化には、リソースとエクスポーターの初期化も含まれます。 これは通常、OpenTelemetry によるトレースの最初のステップです。 いくつかの言語SDKでは、グローバルなトレーサープロバイダーがすでに初期化されています。

トレーサー

トレーサーは、サービス内のリクエストなど、与えられた操作で何が起こっているかについての詳細な情報を含むスパンを作成します。 トレーサーはトレーサープロバイダーから作成されます。

トレースエクスポーター

トレースエクスポーターはトレースをコンシューマーに送信します。 このコンシューマーは、デバッグや開発時間用の標準出力、OpenTelemetryコレクター、あるいは任意のオープンソースやベンダーのバックエンドです。

コンテキスト伝搬

コンテキスト伝搬(プロパゲーション)は、分散トレースを可能にする中心となる概念です。 コンテキスト伝搬を使用すると、スパンがどこで生成されたかに関係なく、スパンを相互に関連付け、トレースとして組み立てられます。 このトピックについては、コンテキスト伝搬の概要を参照してください。

スパン

スパン は、作業や操作の単位を表します。 スパンはトレースの構成要素です。 OpenTelemetryでは、以下の情報を含みます。

次はスパンの例です。(訳注:JSON形式で表現しているだけで、必ずしもJSONではありません)

{
  "name": "/v1/sys/health",
  "context": {
    "trace_id": "7bba9f33312b3dbb8b2c2c62bb7abe2d",
    "span_id": "086e83747d0e381e"
  },
  "parent_id": "",
  "start_time": "2021-10-22 16:04:01.209458162 +0000 UTC",
  "end_time": "2021-10-22 16:04:01.209514132 +0000 UTC",
  "status_code": "STATUS_CODE_OK",
  "status_message": "",
  "attributes": {
    "net.transport": "IP.TCP",
    "net.peer.ip": "172.17.0.1",
    "net.peer.port": "51820",
    "net.host.ip": "10.177.2.152",
    "net.host.port": "26040",
    "http.method": "GET",
    "http.target": "/v1/sys/health",
    "http.server_name": "mortar-gateway",
    "http.route": "/v1/sys/health",
    "http.user_agent": "Consul Health Check",
    "http.scheme": "http",
    "http.host": "10.177.2.152:26040",
    "http.flavor": "1.1"
  },
  "events": [
    {
      "name": "",
      "message": "OK",
      "timestamp": "2021-10-22 16:04:01.209512872 +0000 UTC"
    }
  ]
}

スパンは、親スパンIDの存在によって暗示されるように、入れ子にできます。 これによって、スパンはアプリケーションで行われる作業をより正確に把握できます。

スパンコンテキスト

スパンコンテキストは、各スパンの不変オブジェクトであり、以下を含みます。

  • スパンが属するトレースを表すトレースID
  • スパンのスパンID
  • トレースフラグ。これはトレースに関する情報を含むバイナリエンコーディングです。
  • ベンダ固有のトレース情報を保持するキーと値のペアのリスト

スパンコンテキストは、分散コンテキストバゲッジと共にシリアライズされ、伝搬されるスパンの一部です。

スパンコンテキストにはトレースIDが含まれているため、スパンリンクを作成する際に使用されます。

属性

属性(アトリビュート)はキーと値のペアで、スパンに注釈を付けるためのメタデータを含んでいます。このメタデータは追跡している操作に関する情報を伝えるためのものです。

たとえば、eコマースシステムでユーザーのショッピングカートに商品を追加する操作をスパンが追跡する場合、ユーザーのID、カートに追加する商品のID、カートIDを捕捉できます。

スパンには、スパン作成中または作成後に属性を追加できます。 SDKでのサンプリングで属性を利用できるようにするには、スパン作成時に属性を追加することをおすすめします。 スパン作成後に値を追加する必要がある場合は、その値でスパンを更新してください。

属性には、各言語SDKが実装する以下のルールがあります。

  • キーは非NULL文字列値でなければならない
  • 値は、非NULL文字列、ブール値、浮動小数点値、整数、またはこれらの値の配列でなければならない

さらに、セマンティック属性があり、これは一般的な操作に通常存在するメタデータのための既知の命名規則です。 システム間で共通の種類のメタデータが標準化されるように、可能な限りセマンティック属性の命名を使用することは有用です。

スパンイベント

スパンイベントは、スパン上の構造化ログメッセージ(または注釈)と考えられます。通常、スパンの期間中、意味のある特異な時点を示すために使われます。

たとえば、ウェブブラウザでの2つのシナリオを考えてみましょう。

  1. ページ読み込みの追跡
  2. ページがインタラクティブになるタイミングを示す

スパンは、開始と終了がある操作なので、最初のシナリオに最も適しています。

スパンイベントは、意味のある特定の時点を表すため、2つ目のシナリオを追跡するのに最も適しています。

スパンイベントとスパン属性の使い分け

スパンイベントにも属性が含まれるため、属性のかわりにいつイベントを使用するかという質問には、必ずしも明白な答えがあるとは限りません。 決定するための参考として、特定のタイムスタンプに意味があるかどうかを考えてみてください。

たとえば、スパンで操作を追跡していて、操作が完了した時、操作からのデータをテレメトリーに追加したいと思うかもしれません。

  • 操作が完了したタイムスタンプに意味がある場合、または関連性がある場合は、データをスパンイベントに添付する。
  • タイムスタンプに意味がない場合は、スパン属性としてデータを添付する。

リンクは、あるスパンと別の1つ以上のスパンを関連付け、因果関係を示唆するために存在します。 たとえば、ある操作がトレースによって追跡される分散システムがあるとしましょう。

これらの操作のいくつかに対して、追加の操作がキューに入れられ実行されますが、その実行は非同期です。 この後続の操作もトレースで追跡できます。

後続の操作のトレースを最初のトレースに関連付けたいと思っても、後続の操作がいつ始まるかは予測できません。 この2つのトレースを関連付ける必要があるので、スパンリンクを使用します。

最初のトレースの最後のスパンを、2番目のトレースの最初のスパンにリンクできます。 これで、これらのスパンは互いに因果関係があることになります。

リンクは必須ではありませんが、トレーススパン同士を関連付ける良い方法として役立ちます。

スパンステータス

各スパンにはステータスがあります。可能な値は以下の3つです。

  • Unset
  • Error
  • Ok

デフォルト値は Unset です。 スパンのステータスが Unset である場合は、追跡した操作がエラーなしで正常に完了したということです。

スパンのステータスが Error である場合、そのスパンが追跡する操作で何らかのエラーが発生したことを意味します。 たとえば、リクエストを処理するサーバーでHTTP 500エラーが発生した場合などです。

スパンのステータスが Ok である場合、そのスパンはアプリケーションの開発者によって明示的にエラーなしとマークされたことを意味します。 これは直感的ではありませんが、スパンがエラーなく完了したことが分かっている場合、スパンのステータスを Ok とする必要はありません。 これは Unset でカバーされるからです。 Okは、ユーザーによって明示的に設定されたスパンのステータスの明確な「最終決定」を表すものです。 これは、開発者がスパンの解釈を「成功した」以外のものにはしないことを望む場合に役立ちます。

もう一度確認します。 Unset はエラーなしで完了したスパンを表します。 Ok は、開発者が明示的にスパンを成功とマークした場合を表します。 ほとんどの場合、スパンを明示的に Ok とマークする必要はありません。

スパンの種類(SpanKind)

スパンが作成されると、Client(クライアント)Server(サーバー)Internal(内部)Producer(プロデューサー)Consumer(コンシューマー) のいずれかとなります。 このスパンの種類は、トレースがどのように組み立てられるべきかのヒントをトレースバックエンドに提供します。 OpenTelemetryの仕様によると、サーバースパンの親はリモートクライアントスパンであることが多く、クライアントスパンの子は通常サーバースパンです。 同様に、コンシューマースパンの親は、常にプロデューサであり、プロデューサースパンの子は、常にコンシューマである。提供されない場合、スパンの種類は内部的なものとみなされます。

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

Client(クライアント)

クライアントスパンは、発信HTTPリクエストやデータベース呼び出しのような、同期的な発信リモート呼び出しを表します。 この文脈では、「同期」は async/await を指すのではなく、後の処理のためにキューに入れることが出来ない、ということを指すことに注意してください。

Server(サーバー)

サーバースパンは、HTTPリクエストやリモートプロシージャコールのような、 同期的に着信するリモートコールを表します。

Internal(内部)

内部スパンは、プロセス境界を越えない操作を表します。 関数呼び出しやNode.jsのExpressミドルウェアの計装などで、内部スパンを使用することがあります。

Producer(プロデューサー)

プロデューサースパンは、後で非同期に処理される可能性のあるジョブの作成を表します。 それは、ジョブキューに挿入されるようなリモートジョブかもしれないし、イベントリスナーによって処理されるローカルジョブかもしれません。

Consumer(コンシューマー)

コンシューマースパンは、プロデューサーが作成したジョブの処理を表し、プロデューサースパンがすでに終了したずっと後に開始されることがあります。

仕様

詳細はトレース仕様を参照してください。