observability-and-instrumentation について
本番システムの可観測性を設計する開発チーム向けのスキルです。オンコール時の疑問を起点にログ、メトリクス、トレース、アラートを選び、データと通知を検証します。
主な機能
- 01オンコールの疑問から計測目標を設定
- 02構造化ログと相関 ID
- 03RED・USE メトリクス設計
- 04OpenTelemetry による分散トレース
- 05症状ベースのアラートとランブック
- 06テレメトリーと通知の検証
活用シーン
- 01新しいサービスやバックグラウンドジョブの計測
- 02本番インシデントの診断改善
- 03リトライやサービス間呼び出しを含む変更のレビュー
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ addyosmani/ agent-skills --skill observability-and-instrumentationインストール後、Claude Code で/observability-and-instrumentationを入力して読み込まれたか確認します。
詳細
可観測性とインストルメンテーション
概要
観測できないコードは運用できません。可観測性とは、コードが出力するテレメトリーを使って、外部から「システムは何をしていて、なぜそうしているのか?」に答えられる能力です。インストルメンテーションはリリース後に追加するものではなく、テストと同じように機能とともに作り込みます。テレメトリーなしで機能をリリースすると、最初にユーザーから報告されたバグの調査は、クエリーではなく考古学のようになってしまいます。
使用する場面
- 本番環境で動作する機能を構築するとき
- 新しいサービス、エンドポイント、バックグラウンドジョブ、外部連携を追加するとき
- 本番環境のインシデントの診断に時間がかかりすぎたとき(「何が起きたのか判断できなかった」)
- アラートルールを設定またはレビューするとき
- I/O、リトライ、キュー、サービス間呼び出しを追加する PR をレビューするとき
次の場合は使用しないでください:
- 現在発生している障害を診断するとき —
debugging-and-error-recoveryスキルを使用してください(可観測性があれば、次回このスキルをすばやく活用できます) - 測定された遅さをプロファイリングして最適化するとき —
performance-optimizationスキルを使用してください - リリース当日の監視チェックリストやロールバックのトリガーを作成するとき —
shipping-and-launchスキルを参照してください。このスキルでは、それらにデータを提供するインストルメンテーションを扱います
プロセス
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 を生成(または受け入れ)し、すべてのログ行、スパン、アウトバウンド呼び出しに付与します。これがないと、入り交じったログから単一のリクエストを再構成できません。
// 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();
});
複数のエントリーポイントが同じログに書き込む場合は、エントリーポイント名を記録してください。 相関 ID は実行を特定しますが、どのコードパスが実行を開始したかは示しません。スケジューラー、リプレイ用エンドポイント、手動の CLI 実行のいずれから到達した場合も、同じジョブは1つのログ出力先に見分けのつかない行を生成します。そのため、ログ行の帰属先を特定するには、スケジューラーの履歴、プロセステーブル、デプロイログを照合するなど、消去法に頼ることになります。そして、その照合は外部記録が残っている間しか成り立ちません。実行の開始時点で、相関 ID の隣にエントリーポイントを記録し、両方を同じ方法で伝播させてください。
// One helper for every entry point: the run's own logger carries both fields.
// `entryPoint`, not `source` — ECS reserves `source.*` for network fields.
export const runLog = (entryPoint: 'scheduler' | 'replay_endpoint' | 'cli', runId: string) =>
logger.child({ entryPoint, requestId: runId });
// scheduler tick -> runLog('scheduler', crypto.randomUUID())
// POST /jobs/:id/replay -> runLog('replay_endpoint', req.id)
// CLI invocation -> runLog('cli', process.env.RUN_ID ?? crypto.randomUUID())
両方のフィールドを、相関 ID と同じ境界(キューメタデータ、HTTP ヘッダーなど)を越えて伝播させる必要があります。そうしないと、ワーカーがエントリーポイントを再導出して推測することになります。エントリーポイントと単に相関するフィールドは、帰属の証拠ではなく手掛かりにすぎません。ジョブを呼び出せるものなら何でも、そのフィールドを再現できます。
シークレット、トークン、パスワード、個人を完全に特定できる情報(PII)を決してログに記録しないでください。 これは security-and-hardening スキルに定められた厳守ルールです。テレメトリーパイプラインは、典型的なデータ漏えい経路です。フィールドは許可リストで管理し、リクエストボディ全体を記録しないでください。
4. メトリクス
リクエスト駆動型のサービスでは、すべてのエンドポイントと外部依存関係に RED を計測します。Rate(リクエスト数/秒)、Errors(失敗率)、Duration(平均ではなくレイテンシーヒストグラム)です。リソース(キュー、プール、ホスト)には USE を使います。Utilization(使用率)、Saturation(飽和度)、Errors(エラー)です。
トレースと同様に、ベンダー中立の方法は OpenTelemetry のメトリクス API です(ステップ 5 と同じ SDK とコンテキストを使用します)。以下の例では、Prometheus の prom-client を使用しています。これは一般的なバックエンドの選択肢の一つであり、唯一の選択肢ではありません。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 段階目を設けるとノイズになり、すべてを無視するよう人々を慣らしてしまいます。
ランブックの作成
上記のルール 2 により、すべてのアラートにランブックへのリンクが必要です。ランブックの役割は、読者に考えさせずに、何が起きているのか、最初に何を確認するのか、それで解決しない場合に誰に連絡するのかという 3 つの質問に答えることです。docs/runbooks/ に、アラート名を使ったファイル名で保存してください。
最小限のランブック(3 行):
# Runbook: High Error Rate on /api/tasks
**Means:** DB connection pool likely exhausted, or a bad deploy.
**First check:** `SELECT count(*) FROM pg_stat_activity WHERE backend_type = 'client backend';`
— if count > pool limit, see Step 2. (Swap in the equivalent for your database.)
**Escalate to:** #db-oncall or engineering on-call rotation.
3 行を超えて追記する場合: 最初の確認だけでは判断できない場合にのみ、手順を追加してください。最もよくある 3 つの原因を網羅する 5 段階のランブックのほうが、あらゆる例外ケースを網羅しているものの流し読みされる 20 段階の文書より優れています。
ランブックを最新の状態に保ってください。 ランブックを使ったインシデントを解決するたびに、そのランブックを更新してください。古いランブックは誤った安心感を生みます。手順に誤りや不足があった場合は、インシデントを解決済みにする前に修正してください。
7. テレメトリー自体を検証する
計装もコードであり、間違っている可能性があります。作業を完了とする前に、各経路を実行して実際の出力を確認してください。
- ステージングでエラーを強制的に発生させる →
requestIdでログを検索し、フィールドが構造化されていること([object Object]ではないこと)を確認します - テストトラフィックを送信する → 期待されるラベルと妥当な値でメトリクス系列が表示されることを確認します
- トレース UI で 1 つのリクエストをサービス間で追跡する → スパンが途切れていないことを確認します
- 新しいアラートをそれぞれ 1 回発報させる(一時的にしきい値を下げます) → 適切なチャネルに届き、ランブックへのリンクが機能することを確認します
よくある合理化
| 合理化 | 実態 |
|---|---|
| 「動いてからログを追加すればいい」 | 「後で」は「最初のインシデントの後」になりがちです。何も見えないと気づくには、最もコストのかかるタイミングです。構築と並行して計装してください。 |
| 「ログが多いほど可観測性が高まる」 | 構造化されていないノイズは、インシデント対応を速めるどころか遅らせます。検索可能なイベント3件のほうが、散文のログ行300件より役立ちます。 |
「今のところは console.log で十分」 | 構造化されていない出力は、フィルタリング、関連付け、アラート設定ができません。構造化ロガーを導入する追加コストは、一度だけの5分です。 |
| 「何か壊れたらダッシュボードを見ればいい」 | 明確な問いを定義せずに作られたダッシュボードでは、答え以外は何でも見つかります。オンコール時に生じる問いから始めてください。 |
| 「重要なものすべてにアラートを設定して、後で調整すればいい」 | ノイズの多いページャーは、アラートを無視するよう人々を慣らしてしまいます。調整は行われず、本当に必要な通知が見逃されます。 |
| 「デバッグしやすくなるので、ユーザー ID をメトリクスのラベルにしよう」 | その結果、メトリクスのバックエンドがダウンする可能性もあります。カーディナリティの高い検索には、ログとトレースを使ってください。 |
| 「サービスが2つしかないのにトレーシングは大げさだ」 | サービスが2つあれば、すでにログでは答えられないサービス間のレイテンシに関する問いが生じます。自動計装ならコストはごくわずかです。 |
危険信号
- リトライ、キュー、外部呼び出しを含む機能の PR に、新しいテレメトリがまったくない
- 構造化フィールドではなく、文字列補間で作られたログ行
- 相関 ID/リクエスト ID がなく、各ログ行が孤立している
- スケジューラー、Webhook、手動実行からのログが、どの実行元がログ行を生成したのかを示すフィールドなしに、ひとつのログストリームに流れている
- ユーザー ID、生の URL、エラーメッセージのテキストをラベルにしたメトリクス(カーディナリティの爆発)
- パーセンタイルのない平均値でレイテンシを計測している
- 毎日発火し、対応なしで確認済みになるアラート
- ユーザーに影響するエラー率を監視せず、原因(CPU、メモリ)のアラートで担当者にページャー通知を送っている
- ログにシークレット、トークン、完全なリクエストボディが含まれている
- 本番機能が正常であることの唯一の根拠が「自分のマシンでは動く」
検証
機能の計装後、次の点を確認してください。
- この機能についてオンコール時に生じる問いが文書化されており、各シグナルがいずれかの問いに対応している
- すべてのログ出力が構造化(JSON)されており、安定したイベント名と各行の相関 ID を備えている
- 複数のエントリーポイントから書き込まれるすべてのログシンクにエントリーポイントフィールドがあり、実行開始時に設定されて相関 ID とともに伝播されている(下流で推測されていない)
- どのログ行にもシークレット、トークン、未編集の PII が含まれていない(実際の出力を抜き取り確認する)
- すべての新しいエンドポイントと外部依存関係について、上限の定まったラベルセットを持つ RED メトリクスが存在する
- レイテンシがヒストグラムであり、p95/p99 をクエリできる
- トレーシング UI で、スパンの途切れなく単一のリクエストをエンドツーエンドで追跡できる
- 新しいアラートがすべて症状ベースで、ランブックへのリンクがあり、少なくとも一度はテスト発火されている
- ステージング環境で意図的に発生させた障害を、ソースを読まずにテレメトリだけで特定できた
このリストを、ローンチ前の計装ゲートを含めてひと目で確認できるバージョンについては、../../references/observability-checklist.md を参照してください。