トレース計装のベストプラクティス
OpenTelemetry を使ってトレーシングを導入する際、適切な計装設計が重要です。どこにSpanを作成し、どんな情報を記録するかによって、トレーシングシステムの価値が大きく変わります。この記事では、計装品質を高めるための実践的な指針を解説します。
計装対象の選定基準
Spanをどこに作成するかは、システムの理解度を左右します。闇雲に計装すると、ノイズが増えてデバッグが困難になります。一方、計装が不足すると、問題発生時に原因特定ができません。バランスの取れた計装を実現するには、優先度を意識することが大切です。
優先度の高い計装対象
外部サービス呼び出しは最優先です。データベースクエリ、REST API呼び出し、キャッシュアクセスなど、システム外部への依存を全て可視化しましょう。これらは遅延やエラーの大きな要因となるため、各呼び出しを個別のSpanで追跡することが重要です。
ビジネスロジックの重要処理も計装の対象です。注文処理、支払い確認、ユーザー認証など、ビジネスの核となる操作を追跡することで、システム全体の動作を理解できます。ただし、1行程度の単純な処理までSpanを作る必要はありません。特に複数のステップで構成される処理に焦点を当てます。
エラーハンドリングの関所も見落としやすいポイントです。リトライロジック、フォールバック処理、例外キャッチ箇所など、エラー時の動作を記録することで、障害時の原因分析が飛躍的に改善します。
非同期処理やバックグラウンドジョブは計装が後回しになりやすいですが、本番環境での問題の多くがこうした非同期処理に関係しています。ジョブキューへの投入、ワーカーでの処理開始と終了を追跡することは、システムの信頼性向上につながります。
次の図は、計装優先度を可視化したものです:
Span の設計原則
Spanを作成する際は、単に「何が起こったか」を記録するのではなく、「どう見えるべきか」を意識することが大切です。運用チーム、開発チーム、SRE など異なる立場の人々が同じトレースを見ることになるからです。
操作名の命名規約
Span名は、その操作の意図を一目で理解できる名前にしましょう。OpenTelemetry のセマンティック規約では、「db.query」「http.client.request」「messaging.publish」など、標準化された命名パターンが定義されています。これらに従うことで、トレーシングツールが自動的に適切にSpanを分類・分析できるようになります。
独自の命名を避けるべき理由は、標準規約に従うことで、ツール側の自動分析機能が働くからです。例えば、データベース関連のSpanには「db.」で始まる名前を付けることで、トレーシングバックエンドがデータベースパフォーマンスの自動分析を行えます。
属性の選択:セマンティック規約への準拠
Span に付与する属性(Attributes)の選択も重要です。自由な属性を追加できるからこそ、どの情報を記録するかの判断が求められます。セマンティック規約では、各操作タイプに対して「どの属性を記録すべきか」が定義されています。
例えば、HTTP クライアント呼び出しの場合、以下の属性が推奨されます:
| 属性名 | 説明 | 例 |
|---|---|---|
http.request.method | HTTPリクエストメソッド | GET, POST, PUT |
url.full | 完全なリクエストURL | https://api.example.com/users |
server.address | 接続先サーバーのホスト名またはIP | api.example.com |
server.port | 接続先サーバーのポート番号 | 443 |
http.response.status_code | HTTPレスポンスステータスコード | 200, 404, 500 |
データベース操作の場合は異なる属性セットが推奨されます。セマンティック規約に従うことで、バックエンド側が自動的に正しく分析でき、複数のチームで一貫した観測が実現します。
時間粒度の目安
細すぎるSpanの作成は避けましょう。ループ内で毎回Spanを作成したり、1行の処理にSpanを付けたりすると、大量のSpanデータが生成され、ストレージコストが増加し、分析も困難になります。
実務的には、10ミリ秒程度以上の処理時間を持つ操作をSpanの対象とするのが目安です。データベースクエリやAPI呼び出しはこの基準を満たすので優先度が高いのです。一方、メモリ内の単純な計算処理は通常この基準を満たさないため、Spanで追跡する必要がありません。
属性数とイベント数の考慮
各Spanに付与する属性や記録するイベントは、適度に抑えましょう。属性数が多すぎると、データサイズが増大し、フィルタリングやクエリが遅くなります。セマンティック規約で「必須」とされた属性を優先し、本当に必要な情報のみに絞ることがポイントです。
エラー・イベント・ステータスの記録
計装の品質は、正常系の情報をどれだけ揃えるかではなく、問題発生時にどれだけ情報を取得できるかで決まります。エラーハンドリングの質が、トレーシングの価値を左右する最重要要素です。
span.status の設定
OpenTelemetry では、Spanの結果を3つのステータスで表現します:
- Unset(デフォルト): 操作が正常に完了した状態
- Ok: 明示的に成功と判定された状態
- Error: エラーが発生した状態
多くの場合、デフォルトの Unset で十分ですが、エラーが発生した場合は必ず Status.ERROR を設定します。これにより、バックエンド側がエラーが発生したSpanを自動的に検出し、アラート生成やエラーレート分析に活用できます。
例外情報の記録方法
例外が発生した場合は、単にステータスを ERROR に設定するだけでなく、例外情報も記録します。言語によって方法は異なります。Python では span.record_exception(ex)、Java では SDK が自動的に例外情報をキャプチャするなど、言語別の標準的なパターンがあります。
例外情報を記録することで、スタックトレースや例外メッセージがトレースに含まれ、問題の根本原因特定が容易になります。
イベント(span.add_event)の活用
Spanの外側で複数の重要な出来事が起こる場合、イベント機能を使用します。例えば、リトライ処理では「1回目のリトライ失敗」「2回目のリトライ失敗」など、各試行のタイムスタンプ付き記録がイベントとして有用です。
イベント機能により、Span全体のライフサイクル内で「いつ何が起こったか」を詳細に把握できます。
ログレベルとの使い分け
トレーシング(Span記録)とログの関係を理解することも重要です。通常のログは、バッチ処理や定期的な冗長な出力に適しています。一方、トレーシングは個別のリクエストの流れを追跡する用途に向いています。
重要なエラーや例外は、ログとトレース両方に記録することで、複数の観点からの分析が可能になります。
過計装・過少計装のバランス
過計装によるパフォーマンス低下の危険
すべての関数にSpanを作成したり、ループ内で毎回Spanを作成したりすると、計装自体がオーバーヘッドになります。Spanの作成・管理・エクスポートにはメモリとCPUを消費するため、計装が多すぎるとアプリケーション本体のパフォーマンスが低下する可能性があります。
本番環境では、計装のオーバーヘッドが全体のレスポンスタイムに与える影響を考慮すべきです。
過少計装によるデバッグ困難
一方、計装が不足すると、問題発生時に原因特定ができません。本番環境で突然パフォーマンス低下が起こった際、トレースに十分な情報がないと、「どのサービスが遅いのか」「どのクエリが原因なのか」を特定できなくなります。
実務的な判断基準
適切なバランスを取るには、以下の指針を参考にします:
- 外部依存(DB、API): 全て計装対象とする
- ビジネス処理: 複数ステップで構成される重要な処理に限定
- 内部処理: 10ミリ秒未満の短時間処理は対象外
- エラーハンドリング: リトライやフォールバック処理は全て計装
- 本番環境での確認: 定期的にオーバーヘッドを測定し、必要に応じて計装レベルを調整
新規開発時は若干多めに計装し、本番運用で経験を積みながら調整するのが現実的です。
言語別の実装パターン
Python:with 文による自動リソース管理
Python では、コンテキストマネージャーを使用してSpan のライフサイクルを自動管理します:
with tracer.start_as_current_span("operation_name") as span:
span.set_attribute("key", "value")
# 処理を実行
# このブロック内で例外が発生した場合、
# コンテキストマネージャが自動でステータスを ERROR に設定します
some_function_that_might_raise_error()コンテキストマネージャを使用することで、with ブロック内で例外が発生した場合、自動的に Span のステータスが StatusCode.ERROR に設定され、スコープを抜ける際に Span が終了します。明示的な例外ハンドリングは不要です。
Java:try-with-resources での自動クローズ
Java では同様に try-with-resources パターンが標準です:
try (Scope scope = span.makeCurrent()) {
// 処理を実行
// ...
span.setStatus(StatusCode.OK);
} catch (Throwable t) {
span.setStatus(StatusCode.ERROR, "処理中にエラーが発生しました");
span.recordException(t);
throw t; // 上位の呼び出し元に例外を伝播させる
} finally {
span.end(); // Span を確実に終了させる
}try-with-resources で管理される Scope オブジェクトはSpanのコンテキストを管理します。例外が発生した場合は、Throwable で全ての例外を捕捉し、ステータスと例外情報を記録した後、throw で上位に伝播させることが重要です。finally ブロックで確実に span.end() が実行されます。
Node.js:async/await と Context API
Node.js では、async context を適切に保持することが重要です。自動計装ライブラリが context を維持するため、手動計装時も context 対応を意識します:
const span = tracer.startSpan("operation_name");
try {
// 処理を実行
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR });
span.recordException(error);
throw error; // 上位に例外を伝播させる
} finally {
span.end();
}Go:defer による確実な終了
Go では defer パターンを活用して Span の終了を確保します:
ctx, span := tracer.Start(context.Background(), "operation_name")
defer span.End()
span.SetAttributes(
attribute.String("key", "value"),
)
if err != nil {
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
}defer により、エラーが発生した場合でも確実に Span が終了します。
Span 設計チェックリスト
計装を実装する前に、以下のチェックリストで設計の妥当性を確認しましょう:
- [ ] 計装対象は優先度マトリックスで上位に位置する操作か
- [ ] Span 名は標準規約(db.、http. など)に従っているか
- [ ] 必須属性はセマンティック規約に基づいて選定されているか
- [ ] 操作時間は 10ms 以上の目安を満たしているか(短時間処理除外)
- [ ] エラー発生時にステータスと例外情報を記録する処理がある
- [ ] 例外情報の記録方法は言語別の標準パターンに従っているか
- [ ] 属性数は合理的な範囲に抑えられているか(10個程度が目安)
- [ ] ループ内や頻繁に呼ばれる処理の計装は避けられているか
これらのポイントを意識することで、保守性が高く、ノイズの少ないトレーシングシステムが実現できます。
関連記事
セマンティック規約について、より詳細なリファレンスと実装パターンは、以下の記事で解説しています:
- セマンティック規約リファレンス - HTTP、DB、RPC等のドメイン別属性リファレンスと実装パターン
まとめ
トレース計装は、単なる「何かを記録する」のではなく、「どう理解しやすいか」「問題発生時に役立つか」を考えて設計する必要があります。優先度の高い対象から段階的に計装を進め、標準規約に従いながら、継続的に改善していくアプローチが実務的です。計装品質を高めることで、初期トラブルシューティングの効率が劇的に改善します。