observability-and-instrumentation について
Observability and Instrumentationは、本番機能の開発と並行してテレメトリーを設計するためのソフトウェア開発スキルです。構造化ログ、メトリクス、分散トレース、実行可能なアラート、検証を扱い、運用上の問いへの回答と機密データ・メトリクスのカーディナリティ抑制を重視します。
主な機能
- 01テレメトリーをオンコールの問いに対応付け
- 02構造化ログと相関IDの指針
- 03RED・USEメトリクスの実践方法
- 04OpenTelemetryによる分散トレース
- 05実行可能なアラートとランブックの基準
- 06テストシナリオによるテレメトリー検証
活用シーン
- 01本番機能へのオブザーバビリティ追加
- 02新サービスや外部連携の計装
- 03プルリクエストでのテレメトリーとアラートの確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ afonsoft/ skills --skill observability-and-instrumentationインストール後、Claude Code で/observability-and-instrumentationを入力して読み込まれたか確認します。
同梱ファイルreferences/observability-checklist.md
詳細
オブザーバビリティと計装
概要
観測できないコードは運用できません。オブザーバビリティとは、コードが出力するテレメトリを使い、外部から「システムは何をしていて、その理由は何か?」に答えられる能力です。計装はリリース後に追加するものではなく、テストと同じように機能と並行して実装します。テレメトリなしで機能をリリースすると、ユーザーから最初に報告されたバグは、クエリで調べるのではなく、考古学のような調査になってしまいます。
使用するタイミング
- 本番環境で実行される機能を構築するとき
- 新しいサービス、エンドポイント、バックグラウンドジョブ、外部連携を追加するとき
- 本番環境のインシデントの診断に時間がかかりすぎたとき(「何が起きたのか分からなかった」)
- アラートルールを設定またはレビューするとき
- I/O、リトライ、キュー、サービス間呼び出しを追加する PR をレビューするとき
対象外:
-
今まさに発生している障害の診断 — デバッグスキルを使ってください(オブザーバビリティがあれば、次回のデバッグが速くなります)
-
計測済みの遅さのプロファイリングと最適化 — パフォーマンス最適化スキルを使ってください
-
リリース当日の監視チェックリストとロールバックの条件 — リリース/ローンチスキルを使ってください。このスキルでは、それらに必要な計装を扱います
-
ユーザーがこのスキルを英語で依頼または言及した場合(例:「/observability-and-instrumentation を使って」「observability-and-instrumentation を実行して」)。
-
ユーザーがこのスキルをポルトガル語で依頼または言及した場合(例:「use /observability-and-instrumentation」、「execute observability-and-instrumentation」)。
プロセス
1. 計装の前に「正常に動作している状態」を定義する
問いのないテレメトリはノイズです。計装を追加する前に、オンコールエンジニアがこの機能について尋ねるであろう質問を2〜4個書き出します。
FEATURE: checkout payment retry
QUESTIONS ON-CALL WILL ASK:
1. What fraction of payments succeed on first attempt vs after retry?
2. When a payment fails permanently, why? (provider error? timeout? validation?)
3. Is the payment provider slower than usual?
→ Every signal below must help answer one of these.
質問を挙げられないなら、計装を始める準備ができていません。すべてをログに記録しても、何も分からないでしょう。
2. 質問ごとに適切なシグナルを選ぶ
| シグナル | 分かること | コスト特性 | 例 |
|---|---|---|---|
| 構造化ログ | 「この特定のケースで何が起きたか?」 | イベント単位。トラフィックに応じて増加 | プロバイダーのエラーコードを含む payment_failed |
| メトリクス | 「全体として、どのくらいの頻度で/速さで起きるか?」 | 時系列ごとに一定。クエリコストが低い | プロバイダー呼び出しの p99 レイテンシ |
| トレース | 「サービス間でどこに時間がかかったか?」 | リクエスト単位。通常はサンプリングされる | 処理区間ごとに内訳を示した、遅いチェックアウト1件 |
経験則:メトリクスは何かがおかしいことを、トレースはどこがおかしいかを、ログはなぜおかしいかを教えてくれます。
3. 構造化ログ
文章ではなくイベントをログに記録します。すべてのログ行を、安定したイベント名と機械可読なフィールドを含む JSON オブジェクトにします。
// BAD: string interpolation — unqueryable, inconsistent
logger.info(`Payment ${id} failed for user ${userId} after ${n} retries`);
// GOOD: stable event name + structured fields
logger.warn({
event: 'payment_failed',
paymentId: id,
provider: 'stripe',
errorCode: err.code,
attempt: n,
}, 'payment failed');
ログレベル — 一貫して使います:
| レベル | 意味 | オンコール時の対応 |
|---|---|---|
error | 不変条件が破られており、対応が必要な場合があります | 調査する |
warn | 劣化しているものの対処済み(リトライ成功、フォールバック使用) | 傾向を監視する |
info | 重要なビジネスイベント(注文確定、ジョブ完了) | なし |
debug | 診断用の詳細情報 | 本番環境ではデフォルトで無効 |
相関 ID は必須です。 システム境界でリクエスト ID を生成(または受け入れ)し、すべてのログ行、スパン、送信先への呼び出しに付与します。相関 ID がなければ、インターリーブしたログから単一のリクエストを再構成できません。
// Express: child logger per request, ID propagated downstream
app.use((req, res, next) => {
req.id = req.headers['x-request-id'] ?? crypto.randomUUID();
req.log = logger.child({ requestId: req.id });
res.setHeader('x-request-id', req.id);
next();
});
シークレット、トークン、パスワード、完全な PII は絶対にログに記録しないでください。 テレメトリパイプラインは、典型的なデータ漏えい経路です。フィールドは許可リストで管理し、リクエストボディ全体を記録しないでください。セキュリティチェックリストの全文は ../code-review-and-quality/references/security-checklist.md を参照してください。
4. メトリクス
リクエスト駆動型サービスでは、すべてのエンドポイントと外部依存先に RED を設定します。Rate(リクエスト数/秒)、Errors(エラー率)、Duration(平均値ではなく、レイテンシーのヒストグラム)です。リソース(キュー、プール、ホスト)には USE を使用します。Utilization(使用率)、Saturation(飽和度)、Errors(エラー)です。
トレースと同様に、ベンダー中立の方法は OpenTelemetry のメトリクス API(ステップ 5 と同じ SDK とコンテキスト)です。以下の例では Prometheus の prom-client を使用しています。これは一般的なバックエンドの選択肢の 1 つであり、唯一の選択肢ではありません。RED/USE とカーディナリティに関するルールは、どちらを選んでも同じです。
import { Histogram } from 'prom-client';
const httpDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration',
labelNames: ['method', 'route', 'status_class'], // '2xx', not '200'
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5],
});
カーディナリティは障害の原因になります。 ラベルの組み合わせが異なるたびに、別の時系列データが作成されます。ラベルには、小さく固定された値の集合(ルートテンプレート、ステータスクラス、プロバイダー名)を使用してください。ユーザー ID、生の URL、エラーメッセージなど、値の数に上限がないものはラベルに使用しないでください。これらはログやトレースに記録するものです。
OK as label: route="/api/tasks/:id" status_class="5xx" provider="stripe"
NEVER a label: user_id, email, request_id, full URL, error message text
平均値は決して使わず、常にパーセンタイルを追跡してください。平均値では、ひどい体験をしている 1% のユーザーが見えなくなります。ヒストグラムを使用し、p50/p95/p99 を確認してください。
5. 分散トレーシング
OpenTelemetry を使用してください。ベンダー中立の標準であり、自動計装によって HTTP、gRPC、一般的な DB クライアントを、コードをほとんど追加せずにカバーできます。
// tracing.ts — must be imported before anything else
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
serviceName: 'checkout-service',
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
手動でスパンを追加するのは、意味のある内部処理単位(例:applyDiscounts、`chargeProvider)の周辺に限り、オンコール担当者がフィルタリングに使う属性を付与します。すべての非同期境界(HTTP ヘッダー、キューメッセージのメタデータなど)を越えてコンテキストを伝播してください。そうしないと、トレースは境界で途切れます。デフォルトでは低いレートでヘッドベースサンプリングを行い、バックエンドがテールサンプリングに対応している場合は、エラーを 100% 保持してください。
6. アラート
原因ではなく、ユーザーが感じる症状に対してアラートを設定します。
SYMPTOM (page-worthy): CAUSE (dashboard, not a page):
error rate > 1% for 5 min CPU at 85%
p99 latency > 2s one pod restarted
queue age > 10 min disk at 70%
原因ベースのアラートは、何も問題がないときに発報し、予測していなかった障害を見逃します。症状ベースのアラートは、原因に関係なく、ユーザーに影響が及んだときに正確に発報します。
作成するすべてのアラートには、次のルールを適用してください。
- 対応可能であること。 対応が「無視する。自動で回復する」であれば、そのアラートは削除してください。
- ランブックへのリンクがあること。3 行だけでも構いません。意味、最初に実行するクエリ、エスカレーション先を記載します。
- しきい値と継続時間があること。推測ではなく、SLO または過去のデータに基づいて設定します。
- 重大度は page(ユーザーに影響、今すぐ対応)と ticket(劣化、今週中に対応)の 2 つだけを使用します。3 段階目はノイズとなり、あらゆる通知を無視する習慣を招きます。
7. テレメトリー自体を検証する
計装もコードの一部であり、間違っている可能性があります。作業を完了とする前に、対象の処理を実行し、実際の出力を確認してください。
- ステージング環境でエラーを強制的に発生させる → ログから
requestIdで見つけ、フィールドが構造化されていること([object Object]ではないこと)を確認する - テストトラフィックを送信する → 期待したラベルと妥当な値でメトリクスの時系列データが作成されることを確認する
- トレーシング UI で 1 つのリクエストをサービス間で追跡する → スパンが途切れていないことを確認する
- 新しいアラートをそれぞれ 1 回発報させる(しきい値を一時的に下げる) → 正しい通知先に届き、ランブックのリンクが機能することを確認する
よくある合理化
| 合理化 | 実態 |
|---|---|
| 「動いてからログを追加すればいい」 | 「後で」は「最初のインシデントの後」に変わります。何も見えないと気づくには、最も代償の大きいタイミングです。構築しながら計装しましょう。 |
| 「ログが多いほど可観測性が高い」 | 構造化されていないノイズは、インシデント対応を速めるどころか遅らせます。検索可能なイベント3件は、散文形式のログ300行に勝ります。 |
「今は console.log で問題ない」 | 構造化されていない出力は、フィルタリング、関連付け、アラート通知ができません。構造化ロガーを導入する手間は、最初の5分だけです。 |
| 「問題が起きたらダッシュボードを見ればいい」 | 問いが定義されていないまま作られたダッシュボードには、答え以外のあらゆる情報が表示されます。オンコール時の疑問から始めましょう。 |
| 「重要なものはすべてアラートにして、後で調整すればいい」 | 騒がしいページャーは、担当者に通知を無視するよう教えてしまいます。調整は結局行われず、実際のページャーを見逃すことになります。 |
| 「ユーザーIDをメトリクスのラベルにすればデバッグしやすい」 | その一方で、メトリクスのバックエンドがダウンします。カーディナリティの高い検索は、ログとトレースに任せましょう。 |
| 「サービスが2つしかないのにトレーシングは大げさだ」 | サービスが2つあれば、ログでは答えられないサービス間のレイテンシに関する疑問がすでに生じます。自動計装を使えば、コストはごくわずかです。 |
危険信号
- リトライ、キュー、外部呼び出しを伴う機能のPRなのに、新しいテレメトリーが一切ない
- 構造化フィールドではなく、文字列補間で組み立てられたログ行
- 相関ID/リクエストIDがなく、各ログ行が孤立している
- ユーザーID、生のURL、エラーメッセージのテキストがラベル付けされたメトリクス(カーディナリティの爆弾)
- レイテンシをパーセンタイルなしで平均値として追跡している
- 毎日発火し、対応されないまま確認済みにされるアラート
- ユーザーに影響するエラー率を監視せず、原因(CPU、メモリ)に対するアラートで担当者を呼び出している
- シークレット、トークン、リクエスト本文全体がログに記録されている
- 本番環境の機能が正常であることの唯一の根拠が「自分のマシンでは動く」
検証
機能に計装を追加した後、次の項目を確認してください。
- この機能に関するオンコール時の疑問が文書化され、それぞれに対応するシグナルがある
- すべてのログ出力が構造化されている(JSON)。イベント名が安定しており、すべての行に相関IDがある
- いずれのログ行にもシークレット、トークン、マスキングされていないPIIが含まれていない(実際の出力を抜き取り確認する)
- すべての新しいエンドポイントと外部依存関係にREDメトリクスがあり、ラベルの種類が上限内に収まっている
- レイテンシがヒストグラムであり、p95/p99をクエリできる
- トレーシングUIで、スパンの欠落なく単一のリクエストをエンドツーエンドで追跡できる
- 新しいアラートはすべて症状ベースで、ランブックへのリンクがあり、一度テスト発火させている
- ステージング環境で発生させた障害を、ソースコードを読まずにテレメトリーだけで特定できた
このリストをリリース前の計装ゲートも含めてひと目で確認できる形式については、references/observability-checklist.md を参照してください。