# エラーはどこだ？OpenTelemetry がエラーを記録する仕組み

LLMS index: [llms.txt](/llms.txt)

---

![エラーと例外について学ぼうとしている困惑したペンギン。Bing Copilot を通じて Dalle3 を使用して AI で生成された画像](penguin-chalkboard.jpg)

普段使っている開発言語によって、エラーとは何か、また例外とは何でありどのように処理すべきかについて、一定の考え方を持っているかもしれません。
たとえば、Go には例外がありません。
これは、プログラマーが通常のエラーを例外的なものとして過度にラベル付けすることを防ぐためです。
一方、Java や Python のような言語は、例外のスローとキャッチに対する組み込みのサポートを提供しています。

エラーや例外が何であるか、どのように処理するかについて言語間で見解が異なる場合、それらの言語で書かれたマイクロサービス全体で標準化されたテレメトリーとエラーレポートが必要なときには何を使えばよいのでしょうか。
OpenTelemetry がそのツールです。
この記事では以下の内容やそれ以上のことを扱います。

- バックエンドでエラーがどう可視化されるかは、予想とは異なる場所や見え方になるかもしれません。
- スパンの種類がエラーレポートにどう影響するか。
- スパンとログによるエラーレポートの違い。

## エラーと例外 {#errors-versus-exceptions}

OTel がエラーと例外をどのように扱うかに入る前に、それぞれが何であり、どのように異なるかを明確にしましょう。
これらの用語の定義にはさまざまなバリエーションがありますが、この記事では以下の定義を使います。
これは OTel の公式な用語では**ありません**。
一般的な業界の定義です。

**エラー**とは、プログラムの実行を妨げる予期しない問題です。
たとえば、セミコロンの欠落やインデントの誤りなどの構文エラーや、ロジックのエラーに起因するランタイムエラーが挙げられます。

**例外**とは、プログラムの正常なフローを中断するランタイムエラーの一種です。
たとえば、ゼロ除算や無効なメモリアドレスへのアクセスが挙げられます。

Python や JavaScript のような言語では、エラーと例外を同義語として扱います。
一方、PHP や Java のような言語ではそうではありません。
エラーと例外の区別を理解することは、効果的なエラーハンドリングにとって不可欠です。
この理解により、アプリケーションにおける障害からの対処と回復に対して、よりきめ細かな戦略を採用できるようになります。

## OTel でのエラーハンドリング {#handling-errors-in-otel}

では、OTel はこうした言語間の概念の違いにどう対処しているのでしょうか。
ここで[仕様](/docs/specs/otel/)（略して「spec」）が登場します。
仕様は、プロジェクトのさまざまな部分に取り組む開発者にとっての設計図を提供し、すべての言語にわたって実装を標準化します。

言語の API や SDK は仕様の実装であるため、仕様でカバーされていないものを実装することに対しては一般的な規則があります。
これは、プロジェクトへのコントリビューションを整理するための指針を提供します。
実際には、いくつかの例外があります。
たとえば、ある言語が仕様に追加する一環として新機能のプロトタイプを作成することがありますが、対応する言語仕様が追加される前にその機能が公開される（通常はアルファまたは実験的として）場合があります。

もう一つの例外は、ある言語が仕様から逸脱することを決定する場合です。
一般的には推奨されませんが、異なることをする強い言語固有の理由がある場合もあります。
このように、仕様は各言語ができるだけ慣用的に機能を実装するための柔軟性をある程度認めています。
たとえば、ほとんどの言語は `RecordException` を実装しています（たとえば、[Python](https://opentelemetry-python.readthedocs.io/en/latest/_modules/opentelemetry/sdk/trace.html#Span.record_exception)）。
一方、Go は [`RecordError`](https://github.com/open-telemetry/opentelemetry-go/blob/61f0e4855c3ab6997d6e1a1e9d3aebc636040240/sdk/trace/span.go?from_branch=main) を実装しており、同じことを行います。

すべての言語にわたる仕様の[準拠マトリクス](https://github.com/open-telemetry/opentelemetry-specification/blob/af2606810c19c31850738d1fd105b4eca7aa8c98/spec-compliance-matrix.md?from_branch=main)を確認できますが、最新の情報は個々の言語のリポジトリを確認するのが最も良い方法です。
それでは、OTel でのエラーハンドリングの方法を、エラーのレポートの仕方から見ていきましょう。

- スパン
- ログ

### スパンにおけるエラー {#errors-in-spans}

OTel では、スパンは分散トレースの構成要素であり、分散システム内の個々の作業単位を表します。
スパンは、コンテキストを通じて互いに、またトレースに関連付けられます。
簡単に言えば、コンテキストはデータの集まりを統合されたトレースに変える接着剤です。
コンテキスト伝搬により、複数のシステムにまたがって情報を渡すことができ、それらを結び付けることができます。
トレースは、メタデータとスパンイベントを通じてアプリケーションについてさまざまなことを教えてくれます。

![トレース内のスパンを示すグラフィック](OTel-spans.png)

### メタデータによるスパンの拡充 {#enhancing-spans-with-metadata}

OTel では、キーバリューペアの形式でメタデータ（[属性](/docs/concepts/signals/traces/#attributes)）を付与してスパンを拡充できます。
ユーザー ID、リクエストパラメーター、環境変数などの関連情報をスパンに付与することで、エラーを取り巻く状況についてより深い洞察を得て、その根本原因を迅速に特定できます。
このメタデータが豊富なエラーハンドリングのアプローチにより、問題の診断と解決に必要な時間と労力が大幅に削減され、アプリケーションの信頼性と保守性が向上します。

スパンには[スパンの種類](/docs/concepts/signals/traces/#span-kind)フィールドもあり、開発者がエラーのトラブルシューティングに役立つ追加のメタデータを提供します。
OTel はいくつかのスパンの種類を定義しており、それぞれがエラーレポートに対して固有の意味を持ちます。

- **client**: 送信される同期リモートコール（たとえば、送信 HTTP リクエストや DB コール）
- **server**: 受信される同期リモートコール（たとえば、受信 HTTP リクエストやリモートプロシージャコール）
- **internal**: プロセス境界を越えない操作（たとえば、関数呼び出しの計装）
- **producer**: 非同期に後で処理される可能性のあるジョブの作成（たとえば、ジョブキューに挿入されたジョブ）
- **consumer**: プロデューサーが作成したジョブの処理。
  プロデューサースパンが終了してからかなり後に開始される場合がある

スパンの種類は、使用される計装ライブラリによって自動的に決定されます。

スパンは[スパンステータス](/docs/concepts/signals/traces/#span-status)でさらに拡充できます。
デフォルトでは、特に指定しない限り、スパンステータスは `Unset` とマークされます。
結果のスパンがエラーを表す場合はスパンステータスを `Error` とマークでき、エラーがない場合は `Ok` とマークできます。

### スパンイベントによるスパンの拡充 {#enhancing-spans-with-span-events}

[スパンイベント](/docs/concepts/signals/traces/#span-events)は、スパン内に埋め込まれた構造化ログメッセージです。
スパンイベントは、スパンに関する説明的な情報を提供することで、スパンを拡充するのに役立ちます。
[スパンイベントは独自の属性を持つこともできます](/docs/languages/ruby/instrumentation/#add-span-events)。

先ほど、`RecordException` というメソッドに触れました。
[仕様](/docs/specs/otel/trace/api/#record-exception)によると（強調は筆者ら）、「例外の記録を容易にするために、**その言語が例外を使用する場合**、言語は RecordException メソッドを提供すべきです（SHOULD）。
メソッドのシグネチャは各言語によって決定され、必要に応じてオーバーロードできます。」

Go は「従来の」例外の概念をサポートしていないため、かわりに [`RecordError`](https://github.com/open-telemetry/opentelemetry-go/blob/61f0e4855c3ab6997d6e1a1e9d3aebc636040240/sdk/trace/span.go?from_branch=main#L445-L467) をサポートしており、慣用的に同じことを行います。
ステータスを `Error` に設定すべき場合は、追加の呼び出しを行う必要があります。
自動的に設定されることはないためです。
同様に、`RecordException` はスパンのステータスを `Error` に設定せずにスパンイベントを記録するために使用でき、スパンに関する追加データを記録するために使用できます。

スパン例外が発生したときにスパンステータスが自動的に `Error` に設定されることを分離することで、ステータスが `Ok` または `Unset` の例外イベントを持つユースケースをサポートできます。
これにより、計装の作成者に最大限の柔軟性が与えられます。

### ログにおけるエラー {#errors-in-logs}

OTel では、ログはサービスまたはその他のコンポーネントによって発行される構造化されたタイムスタンプ付きのメッセージです。
OTel にログが最近追加されたことで、エラーを報告するさらに別の方法が提供されています。
ログは従来、`DEBUG`、`INFO`、`WARNING`、`ERROR`、`CRITICAL` など、発行されるメッセージの種類を表すさまざまな重大度レベルを持っていました。

OTel では、ログとトレースの相関が可能であり、ログメッセージをトレース内のスパンにトレースコンテキスト相関を介して関連付けることができます。
したがって、`ERROR` や `CRITICAL` のログレベルを持つログメッセージを探すことで、相関するトレースを参照してそのエラーに至った経緯についてさらなる情報を得ることができます。

ログにエラーを記録するには、`exception.type` または `exception.message` のいずれかが必要であり、`exception.stacktrace` は推奨されます。
詳細については、[ログにおける例外のセマンティック規約](/docs/specs/semconv/exceptions/exceptions-logs/)を参照してください。

## エラーを捕捉するにはログかスパンか {#logs-or-spans-to-capture-errors}

ここまでの内容を踏まえて、エラーを捕捉するにはスパンとログのどちらのシグナルを使うべきか疑問に思うかもしれません。
答えは「場合による」です。
チームが主にトレースを使用しているかもしれませんし、主にログを使用しているかもしれません。

スパンはエラーの捕捉に適しています。
操作がエラーになった場合、スパンをエラーとしてマークすると目立つようになり、見つけやすくなるためです。
一方、トレースのフィルタリングやテイルサンプリングを行っておらず、システムが毎分数千のスパンを生成している場合、頻繁には発生していないがそれでも対処が必要なエラーを見逃す可能性があります。

スパンイベントとログの使い分けはどうでしょうか。
これも場合によります。
スパンステータスが `Error` に設定されると、例外メッセージ（およびキャプチャしたいその他のメタデータ）を含むスパンイベントが自動的に作成されるため、スパンイベントの使用が便利な場合があります。

もう一つの考慮事項はオブザーバビリティバックエンドです。
バックエンドはログとトレースの両方をレンダリングしますか。
ログ、スパン、スパンイベントはどの程度クエリや発見が容易ですか。
ログとトレースの相関はサポートされていますか。

## さまざまなバックエンドでのエラーの可視化 {#visualizing-errors-in-different-backends}

OTel はシステムから発行される生のテレメトリーデータを提供しますが、データの可視化や解釈は提供しません。
これはオブザーバビリティバックエンドによって行われます。
OTel はベンダー中立であるため、同じ発行された情報を、アプリケーションを再計装することなく、異なるバックエンドで可視化および解釈できます。

### Jaeger {#jaeger}

[Jaeger](https://www.jaegertracing.io/) で OTel のエラーがどのように見えるかを見てみましょう。
エラーデータは[このリポジトリ](https://github.com/avillela/otel-errors-talk)のコードで生成されました。
以下は、サービス [py-otel-server](https://github.com/avillela/otel-errors-talk/blob/6eced0ad6585da19595b1c702fb9232851a9aa59/src/python/server.py?from_branch=main) のトレースビューです。
以下に示すように、エラーのスパンは赤い点として表示されます。

![Jaeger UI でのトレース一覧](jaeger-high-level-view.png)

エラースパンにドリルダウンしてズームインすると、`Logs` をクリックできます。
これは Jaeger でスパンイベントが表現される方法であり、キャプチャされた情報を確認できます。

![Jaeger でのエラースパンの属性とその他のメタデータ](jaeger-error.png)

スパンは明確にエラーとしてマークされており、例外がキャプチャされたスパンイベントが含まれています。
Jaeger はスパンイベントをログとして表現しますが、スパン外のログは可視化しません。

### プロプライエタリバックエンド {#proprietary-backends}

アプリケーションの監視にプロプライエタリエージェントを使用しており、最近 OTel に移行した場合、OTel のエラーがオブザーバビリティバックエンドで期待通りに表現されないことに気づくかもしれません。
プロプライエタリエージェントでキャプチャした同じエラーと比較した場合です。
これは、OTel がベンダーのエラーモデリングとは異なる方法でエラーをモデリングしていることが原因です。

大まかな例として、ベンダーにはアプリケーションにおける論理的な作業単位を構成するものについて、独自の概念がある場合があります。
`transaction` という用語に馴染みがあるかもしれませんが、これはベンダーによって少し異なる意味を持ちます。
OTel では、これはトレースによって表されます。
ベンダーが OTLP をファーストクラスのデータ型として対応するためにプラットフォームを調整するにつれて、データ可視化の体験に違いが出ていることにお気づきでしょう。

より具体的な例として、OTel のスパンの種類の概念が、バックエンドでの OTel エラーの表現方法に影響を与える場合があります。
たとえば、1つの例外を持つトレースがあり、それがステータスを `Error` に設定した内部スパンにある場合、トレースにはエラーがマークされていることが表示されますが、アプリケーション全体のエラーレートにはカウントされない可能性があります。
これは、ベンダーがエントリーポイントスパン（サーバースパン）とコンシューマースパンのエラーのみをエラーレートにカウントすべきであるという見解を持っている場合があるためです。

バックエンドがトレースとログの相関をサポートしている場合、ログから関連するトレースに移動でき、その逆も可能です。
さらに、Jaeger はスパンイベントをログとして可視化しますが、一部のベンダーはスパンイベントをログデータ型ではなく独自のデータ型として合成する場合があり、データのクエリ方法に影響します。

## まとめ {#conclusion}

この記事では、マイクロサービスアーキテクチャ内のさまざまなプログラミング言語にまたがるエラーと例外のハンドリングの課題を探り、標準化されたテレメトリーとエラーレポートのためのソリューションとして OTel を紹介しました。
OTel の仕様は、さまざまな言語にわたるエラーハンドリングを標準化するための設計図として機能し、実装のガイドラインを提供しつつ、ある程度の柔軟性も認めています。

言語の SDK の `RecordException` またはその同等メソッドを使用してスパンにエラーを記録でき、カスタム属性を追加してスパンイベントをさらに充実させることができます。
また、`exception.type` または `exception.message` を追加してログにエラーを記録でき、`exception.stacktrace` を追加してスタックトレースをキャプチャして何が起きたかについてさらなる情報を得ることができます。

そのデータがオブザーバビリティバックエンドに入った後、以前ベンダーのプロプライエタリ監視エージェントを使用していた場合、OTel で計装されたエラーの可視化方法と、エージェントで計装されたエラーの可視化方法に違いがあることに気づくかもしれません。
これは主に、OTel がベンダーの従来のモデリング方法とは異なる方法でエラーをモデリングしているためです。

OTel のログとスパンを通じたエラー記録機能とメタデータによる拡充機能を活用することで、アプリケーションの動作についてより深い洞察を得て、問題をより効果的にトラブルシューティングできます。
今日のダイナミックで要求の厳しい環境において、回復力があり、信頼性が高く、高パフォーマンスなソフトウェアアプリケーションを構築し維持するための備えがより整うでしょう。
詳細については、[OpenTelemetry におけるエラーハンドリング](/docs/specs/otel/error-handling/)を参照してください。

_この記事のバージョンは、もともと New Relic ブログに[投稿されました](https://newrelic.com/blog/how-to-relic/dude-wheres-my-error)。_
