「明らかにシステム障害が発生したのに…なぜ私のダッシュボードではすべて正常と表示されるのだろう?」🧐
開発者なら一度は経験したことがあるであろう、ヒヤリとする瞬間です。苦労して構築したObservabilityシステムが、肝心な瞬間に沈黙してしまう状況。その原因は、OpenTelemetryの例外処理を「適切に」行っていなかったためかもしれません。
今日は、パフォーマンスと問題解決の有用性、両方のメリットを享受できるOpenTelemetry例外処理の「ゴールデンルール」をご紹介します。

🎯 要点:完璧な例外処理のための3つの黄金律
早速ですが、OpenTelemetry例外処理の基本は、この3つの組み合わせです。
- span.record_exception(): 例外の詳細情報(スタックトレースなど)を記録する。
- span.set_status(ERROR): 現在のスパン(Span)の状態を「失敗」と明確に表示する。
- raise (Re-throw): 例外を再スローし、アプリケーションの本来のフローを妨げない。
この3つを併用することで、初めて完璧な例外追跡が可能になります。一つずつ詳しく見ていきましょう。
📜 ステップ1:詳細な手がかりを残す – record_exception()
例外が発生したときに最初に行うべきことは、「何が起こったのか」を詳細に記録することです。 span.record_exception(e)はまさにこの役割を果たします。
- 機能: スパンに「exception」という特別なイベントを追加します。このイベントには、次のような貴重な情報が含まれています。
- 例外タイプ: ZeroDivisionError, NullPointerException など
- 例外メッセージ: エラーが発生した理由
- スタックトレース(Stack Trace): エラーが発生するまでの関数呼び出しパス全体 🗺️
- なぜこれが良いのか? (パフォーマンス 🚀): もしこのすべての情報をスパンの属性(Attribute)に保存したらどうなるでしょうか?スタックトレースのように容量の大きいテキストは、バックエンドシステム(例:Elasticsearch)のインデックス作成に大きな負担をかけます。 record_exceptionはこれを「イベント」として処理するため、検索および集計パフォーマンスの低下を防ぎます。
🚨 ステップ2:「問題発生!」を確実に知らせる – set_status(ERROR)
例外情報を記録したら、このスパンが「失敗」したことを全員に知らせる必要があります。
span.set_status(Status(StatusCode.ERROR, “エラー要約メッセージ”))が、その拡声器の役割を果たします。
- 機能: 該当スパンを視覚的に「失敗」状態にします。GrafanaやJaegerのようなObservability UIで赤色で表示され、数千のトレースの中からでも一目で問題を発見できるようになります。
- もしこれを使わないと?: record_exceptionだけを使用すると、例外記録は残りますが、トレースリストでは「成功」と表示される可能性があります。まるで病気なのに病気だと伝えないようなものです。結果的に問題把握のためのゴールデンタイムを逃してしまいます。
🌪️ ステップ3:私の役割は終わり、元の流れに戻る – 例外の再スロー (raise)
最も重要で、最もよくある間違いです。例外を記録した後には、必ず再スローする必要があります。
- 機能: catch ブロックで処理した例外を上位の呼び出し元に再度伝達します。
- なぜ必ず行う必要があるのか?: OpenTelemetryはアプリケーションの「観察者」であり、「操作者」ではありません。もし例外を catch してそのまま終了させてしまうと(swallow)、アプリケーションはエラーがないと認識してしまいます。
- 恐ろしい結果 😨:
- DBトランザクションがロールバックされずにコミットされる可能性があります。
- APIサーバーがユーザーに500エラーではなく200 OK応答を送信する可能性があります。
- グローバル例外処理ロジックが動作せず、後続処理が漏れる可能性があります。
OpenTelemetryがアプリケーションの正常なエラー処理フローを妨げてはなりません。
🖥️ 全体像:コードで理解する
それでは、これら3つの原則がPythonコードでどのように調和して機能するかを確認してみましょう。
from opentelemetry import trace
from opentelemetry.trace.status import Status, StatusCode
tracer = trace.get_tracer(__name__)
def process_data(data):
# 現在のSpanコンテキストで新しいSpanを開始
with tracer.start_as_current_span("process_data") as span:
try:
# 🕵️♂️ デバッグに役立つコンテキスト情報(属性)を事前に追加しておくのが良いでしょう。
span.set_attribute("data.size", len(data))
# 💣 例外が発生する可能性のある危険な操作を実行
result = 100 / len(data) # len(data)が0の場合、ZeroDivisionErrorが発生
span.set_attribute("result", result)
# 成功時にはデフォルト値がOKなので、省略しても問題ありません。
# span.set_status(Status(StatusCode.OK))
return result
except Exception as e:
# ❗ 3段階の黄金ルールを適用 ❗
# 1️⃣ ステップ:Spanの状態をERRORに設定
error_message = f"Failed to process data: {e}"
span.set_status(Status(StatusCode.ERROR, error_message))
# 2️⃣ ステップ:例外詳細情報(スタックトレースを含む)をイベントとして記録
span.record_exception(e)
# 3️⃣ ステップ:アプリケーションの正常なエラー処理のために例外を再スロー
raise
# 関数使用例
try:
# 意図的にエラーを発生させる状況
process_data("")
except ZeroDivisionError:
# raiseのおかげで、アプリケーションの最上位レベルでも例外を認識し処理できます。
print("Application handled the ZeroDivisionError correctly. ✅")
❌ これらの方法はなぜダメなのか? (アンチパターン)
- record_exceptionのみを使用する場合: スタックトレースは記録されますが、UIでは成功(緑色)と表示され、問題を見落としやすくなります。 😴
- set_status(ERROR)のみを使用する場合: スパンが失敗したことはわかりますが、「なぜ」失敗したのかスタックトレースがないため、根本原因を見つけるのが困難です。 ❓
- 例外を raiseしない場合 (最悪!): トレースは正常に見えるかもしれませんが、実際にはアプリケーションのデータ整合性が損なわれたり、ユーザーに誤った応答を送信したりするなど、深刻なバグを引き起こす可能性があります。 💣
✨ まとめ:完璧な例外処理のためのチェックリスト
最後に、OpenTelemetryで例外を処理する際には、この表を覚えておいてください。
| 項目 | 推奨戦略 | なぜ行うべきか? (理由) |
| 📜 例外記録 | span.record_exception(e) | パフォーマンス+デバッグ: スタックトレースをイベントとして記録し、バックエンドの負荷を軽減し、原因分析に必要なすべての情報を提供します。 |
| 🚨 状態表示 | span.set_status(Status(StatusCode.ERROR, …)) | 可視性: UIで失敗したトレースを即座に識別し、問題解決の速度を向上させます。 |
| 🌪️ フロー制御 | 例外を再スロー (raise) | 安定性: アプリケーション固有のエラー処理ロジック(ロールバック、エラー応答など)が正常に動作することを保証します。 |
| 🕵️♂️ コンテキスト | エラー発生前に span.set_attribute()で関連情報を追加 | デバッグ: エラー発生時の user_id, request_id などの周辺状況を把握し、デバッグに決定的な手がかりを提供します。 |
コメントを残す