開発に慣れている言語によっては、エラーとは何か、例外の構成要素とそれをどのように処理するかについて、一定の考えがあるかもしれません。 たとえば、Goには例外がありません。これは、プログラマーが通常のエラーを例外として分類するのを防ぐためです。 一方、JavaやPythonなどの言語では、例外をスローしたりキャッチしたりするためのサポートが組み込まれています。

エラーや例外とは何か、またそれらをどのように処理するかについてさまざまな言語で意見が一致していないところから始めます。これらの言語で書かれたマイクロサービス全体で標準化されたテレメトリーとエラー報告が必要な場合、何を使用しますか?以下などに対処するためのツールである、OpenTelemetryです。

  • バックエンドでエラーがどのように視覚化されるかは、予想した場所や外観と異なる場合がある
  • スパンの種類がエラー報告にどのように影響するか
  • スパンとログによって報告されるエラー

OpenTelemetryとは?

OpenTelemetry(略してOTel)は、クラウドネイティブ・コンピューティング・ ファウンデーション(CNCF)のプロジェクトです。これは、テレメトリーデータの計装、生成、収集、エクスポートのためのオープンソースのベンダーニュートラルなオブザーバビリティフレームワークです。

エラーと例外

OTelがエラーと例外をどのように処理するかを説明する前に、それらが何であるか、それぞれがどのように違うのかを明確にしましょう。これらの用語の定義にはさまざまなバリエーションがありますが、このブログ記事では以下の定義を使用します。

エラーとは、プログラムの実行を妨げる予期しない問題です。 例としては、セミコロンの欠落やインデントの誤りなどの構文エラーや、ロジックのエラーによって生じる実行時エラーなどがあります。

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

PythonやJavaScriptなどの一部の言語では、エラーと例外を同義語として扱います。PHPやJavaなどはそうではありません。 エラーと例外の区別を理解することは、アプリケーション障害の処理と回復について、より微妙な戦略を採用できるようになるため、効果的なエラー処理には非常に重要です。

OTelでのエラー処理

では、OTelは言語間のこうした概念的な違いにどのように対処するのでしょうか?ここで、specification(略して「spec」)が登場します。 この仕様は、プロジェクトのさまざまな部分に取り組む開発者にブループリントを提供し、すべての言語で実装を標準化します。

言語APIとSDKは仕様の実装であるため、仕様でカバーされていないものを実装することに対する一般的なルールがあります。 これは、プロジェクトへの貢献を整理するのに役立つ指針になります。実際には、例外がいくつかあります。たとえば、言語は、仕様への追加の一環として新しい機能のプロトタイプを作成する場合がありますが、対応する言語が追加される前に、その機能が (通常はアルファ版または実験版として)公開される場合があります。


もう1つの例外は、言語が仕様から逸脱することを決定した場合です。 一般的には推奨されませんが、言語に特有の強い理由により、異なる操作を行う必要がある場合もあります。 このように、この仕様では、各言語ができる限り慣用的に機能を実装できるようある程度の柔軟性が考慮されています。 たとえば、ほとんどの言語ではRecordExceptionが実装されていますが、Goでは同じことを行うRecordErrorが実装されています。

この仕様のコンプライアンスマトリックスはすべての言語にわたって表示できますが、最新の情報は個別の言語リポジトリを確認することで入手できます。これで、OTelでエラーを処理する方法を理解するための出発点ができました。まずは、次の方法でエラーを報告する方法から始めます。

  • スパン
  • ログ

スパンのエラー

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

メタデータによるスパンの強化

OTelを使用すると、キーと値のペアの形式でメタデータ(プロパティ)を使用してスパンを拡張できます。 ユーザーID、リクエスト、環境変数などの関連情報をスパンに添付することで、エラーを取り巻く状況をより深く洞察し、その根本原因を迅速に特定できます。 このメタデータを豊富に含むエラー処理アプローチにより、問題の診断と解決に必要な時間と労力が大幅に削減され、最終的にはアプリケーションの信頼性と保守性が向上します。

スパンにはスパンの種類フィールドもあります。これにより、開発者がエラーをトラブルシューティングするのに役立つ追加のメタデータが得られます。 OTelはいくつかのスパンの種類を定義しており、それぞれがエラー報告に対して独自の意味を持ちます。

  • クライアント:発信同期リモート呼び出しの場合、発信HTTPリクエストやDB呼び出しなど
  • サーバー:着信同期リモート呼び出しの場合、着信HTTPリクエストやリモート手順呼び出しなど
  • インターナル:プロセス境界を越えない操作の場合、関数呼び出しをインストゥルメント化するなど
  • プロデューサー:後で非同期に処理されるジョブを作成する場合、ジョブキューに挿入されるジョブなど
  • コンシューマー:プロデューサーによって作成されたジョブを処理する場合、プロデューサーのスパンが終了してからかなり後に開始される可能性がある

スパンの種類は、使用するインストゥルメンテーションライブラリによって自動的に決定されます。

スパンは、スパンステータスを使用してさらに拡張できます。 デフォルトでは、特に指定がない限り、スパンステータスはUnsetとしてマークされます。 結果のスパンにエラーが示されている場合はスパンステータスをErrorとしてマークし、結果のスパンにエラーがない場合はOkをマークできます。

スパンイベントによるスパンの強化

スパンイベントは、スパン内に埋め込まれた構造化されたログメッセージです。 スパンイベントは、スパンに関するわかりやすい情報を提供することで、スパンの強化に役立ちます。 スパンイベントは独自の属性を持つこともできます。 New Relicは、SpanEventと呼ばれる独自のデータ型としてスパンイベントを合成します。

スパンのステータスがErrorに設定されると、スパンイベントが自動的に作成され、スパンの結果のエラーメッセージとスタックトレースがそのスパンのイベントとしてキャプチャされます。 属性を追加することで、このスパンエラーをさらに強化できます。

先ほど、RecordExceptionというメソッドについて説明しました。 仕様によれば(当社独自のものを強調)、「言語が例外を使用する場合は、例外の記録を容易にするために、言語はRecordExceptionメソッドを提供する必要があります。…メソッドのシグネチャは言語ごとに決定され、必要に応じてオーバーロードできます。」

Goは例外の「従来の」概念をサポートしていないため、代わりにRecordErrorをサポートしています。これは本質的に同じことを慣用的に行います。 ただし、自動的には設定されないため、ステータスをErrorに設定する必要がある場合は、追加の呼び出しを行う必要があります。同様に、RecordExceptionを使用すると、スパンステータスをErrorに設定せずにスパンイベントを記録できます。つまり、これを使用してスパンに関する追加データを記録できます。

スパン例外の発生時にスパンステータスが自動的にErrorに設定されることから切り離すことで、OkまたはUnsetのステータスを持つ例外イベントを発生させることができるユースケースをサポートできます。 これにより、インストゥルメンテーションの作成者は最大限の柔軟性を得ることができます。

ログに示されるエラー

OTelでは、ログはサービスまたは他のコンポーネントによって発行される、構造化されたタイムスタンプ付きのメッセージです。 最近OTelにログが追加されたことで、エラーを報告する別の方法がさらに増えました。 従来、ログには、出力されるメッセージの種類を表すさまざまな重大度レベル(DEBUGINFOWARNINGERRORCRITICALなど)がありました。

OTelでは、トレースへのログの相関が可能で、トレースコンテキスト相関を介して、ログメッセージをトレース内のスパンに関連付けることができます。 したがって、ログレベルがERRORまたはCRITICALのログメッセージを検索すると、関連するトレースを取得することで、そのエラーの原因に関する詳細情報が得られます。


ログにエラーを記録するには、exception.typeまたはexception.messageのいずれかが必要ですが、exception.stacktraceが推奨されます。 ログ例外のセマンティック規約の詳細については、こちらをご覧ください。

エラーをキャプチャするにはログまたはスパン?

ここまでのことを踏まえると、エラーをキャプチャするにはスパンとログのどちらの信号を使用すべきか迷うことでしょう。答えは「状況次第」です。 おそらくあなたのチームでは主にトレースを使用しているか、主にログを使用しているでしょう。

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

ログではなくスパンイベントを使用する場合はどうでしょうか?繰り返しますが、これは状況次第です。スパンステータスがErrorに設定されると、例外メッセージ(およびキャプチャする必要があるその他のメタデータ)を含むスパンイベントが自動的に作成されるため、スパンイベントを使用すると便利な場合があります。

幸いなことに、 New Relicではログとトレースの両方をレンダリングし、データをより見つけやすくするためのクエリ言語を提供し、ログとトレースの相関関係をサポートしています。

New Relicでのエラーの可視化

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

弊社のエージェントのいずれかを使用してアプリケーションを監視しており、最近OTelに移行した場合は、APMエージェントでキャプチャされた同じエラーと比較して、OTelエラーが期待どおりに表現されない場合があることに気づくかもしれません。 これは、ベンダーがエラーをモデル化してきた方法とは異なる方法で、OTelがエラーをモデル化しているからです。

一例として、OTelのスパンの種類の概念は、OTelエラーの表示方法に影響を与える可能性があります。 インスタンスの場合、例外が1つあるトレースが内部スパン上にあり、ステータスがErrorに設定されている場合、トレースにはエラーのマークが付けられますが、アプリのエラー率にはカウントされない可能性があります。 これは、 New Relic 、エントリーポイントスパン(サーバースパン)とコンシューマースパンでのエラーのみをエラー率にカウントすべきであるという意見を持っているためです。 詳細はこちらをご覧ください。


New Relicでは、send_requestsというトレースグループをクリックすると、エラーのあるスパンが含まれるトレースを確認できます。

エラートレースの1つを選択すると、関係するすべてのスパンのトレースウォーターフォールを表示できます。 do_rollスパンにスパンイベントとして例外が含まれていることがわかります。 また、Attributesをクリックして、追加したカスタムアトリビュートなど、関連するメタデータを表示することもできます。

スパンイベントをクリックすると、例外に関する詳細と、スパンイベントに追加したカスタムアトリビュートが表示されます。 この場合、ダミー属性の例を確認できます。 手動でスパンイベントを記録しているため、例外に加えて2番目のスパンイベントもキャプチャされます。

選択したトレースのLogsをクリックすると、トレースから直接相関ログにアクセスできます。 ここでは、3つのログと、トレースのどの時点でそれらが生成されたかが表示されます。 チャートにはエラーログの数が表示されます。

まとめ

エラー処理は、ソフトウェア開発において困難ではありますが不可欠な側面であり、OTelはその複雑さを克服するための包括的なソリューションを提供します。 OTelの機能とNew Relicプラットフォーム機能を活用すると、アプリケーションの動作に関するより深いインサイトを得ることができ、問題のトラブルシューティングをより効果的に行うことができます。 今日の動的かつ要求の厳しい環境において、回復力、信頼性、パフォーマンスに優れたソフトウェアアプリケーションを構築および維持するための準備が整います。