Dress Code Advent Calendar 2025 の 13 日目の記事です。
遅刻です。

この記事では、O11y / SREの一般論や文化論はあんまりしません。
「実務で障害調査が進まない理由」と「どう計装設計に落とすか」 を、デバイス棚卸しレポートのドメインイベントを題材に、具体的な設計・実装パターンとして書きます。
なお、内容は試行錯誤中のものとなっています。
「計装を始める上で最初に整理するべきもの」と「実装パターン」など具体的な話で盛り上がりたい
本記事で扱うシステムは、以下のような構造を持っています:
この二層構造が、後述する「計装の分離」の前提になります。
障害調査でよくある苦しみは、こういうやつです。
ここまで具体的な話になると、SRE本やO11y資料の中に書いてある文化や考え方などを
さらに実務の領域に昇華していかないといけません。
次はこういった実務レベルの話で、どう計装しているのか、で盛り上がりたいなーと思って今回の記事を書いてます。
たとえば、以下のような流れで行われるデバイス棚卸しの業務で「従業員からのレポートが提出されていない」という問い合わせが来たとします。
この状況で、以下のどれが原因かを切り分けるのに時間がかかります:
HTTPステータスからは、業務フローのどこで止まっているかがわからない。
これを放置すると、「HTTP 200なのに顧客は困っている」状況を観測できず、ユーザー影響ベースの調査ができません。
各エンドポイントのHTTPステータス/レイテンシを測っても、業務が複数エンドポイントに跨ると、以下が見えづらくなります:
ドメインイベントは「ビジネスドメイン上で発生した重要な出来事を表すメッセージ」 で、システム内の状態変化(= 集約の状態変化)を表現します。
このイベント列を追えると、理想として:
| 観点 | HTTPステータスのみ | ドメインイベントトレース |
|---|---|---|
| 業務の成否 | ❌ 見えない | ✅ イベント属性で確認可能 |
| どこで止まったか | ❌ 推測が必要 | ✅ イベント列で追跡可能 |
| 状態遷移の適切さ | ❌ DB直接確認 | ✅ スパン属性で確認可能 |
| インシデント調査 | ❌ ログ横断が必要 | ✅ トレースID1つで追跡 |
技術エラーと業務エラーのギャップを埋めるには、ドメインイベントを一つ目の観測点にするのが良さそうという仮説で組み立ててみます。
ここからは、デバイス棚卸しを題材に具体的な計装設計を見ていきます。
デバイス棚卸し業務に関する業務の成功/失敗を定義してみます。
対象デバイスが正常に動いているかor故障しているか、
所有者は想定通りの人、場所のもとにあるか、がわかることが業務の成功と言えます。
今回のケースでは、成功の逆として考えると簡単ですね。
対象デバイスがちゃんと動いているのかわからない、
どこにあるのかわからない、がデバイス棚卸し業務の失敗と言えます。
棚卸しレポートの例だと、こういうイベントが候補になりそうです。
| 業務ステップ | ドメインイベント名 | "成否"の定義に使う属性 |
|---|---|---|
| 対象デバイス抽出 | device_inventory_package_started | 対象件数、抽出条件、対象期間 |
| 依頼(アサイン) | device_inventory_self_report_requested | 依頼先、期限、依頼対象数 |
| 提出 | device_inventory_self_report_submitted | 報告者、報告結果、バリデーション結果 |
| レビュー | device_inventory_report_reviewed | レビュワー、承認/差戻し、理由コード |
| 完了 | device_inventory_package_completed | 最終成功率、処理時間 |
「HTTP成功/失敗」ではなく、業務としてどういうイベントが起きたのか、を表すことを重視しています。
実際にどう実装するか、3つのパターンに分けて紹介します。
また今回はDatadogのdd-traceを使って計装コードを書いていきます。
まず、現在のトレースのスパンに属性を付与する基本関数です。
// ルートスパンに属性を付与する汎用関数exportfunctionputAttributesToRootSpan( attributes: Record<string, SpanAttributeValue>):void{const currentSpan=getActiveSpan();if(!currentSpan)return;const rootSpan=getRootSpan(currentSpan);putAttributesToSpan(attributes, rootSpan);}functiongetRootSpan(span: tracer.Span): tracer.Span{try{const context=(spanasany).context?.();const rootSpan= context?._trace?.started?.[0];return rootSpan|| span;}catch{return span;}}// 使用例:業務処理の中で呼び出すasyncfunctionsubmitSelfReport(input: SubmitReportInput){putAttributesToRootSpan({'operation.package_instance_id': input.packageId,'operation.step_instance_id': input.stepId,'domain.device_id': input.deviceId,'domain.report_result': input.reportResult,});// ... 業務処理}ポイント:ログではなくスパン属性に出力することで、トレース画面から直接検索・フィルタ可能になる。
イベント種別ごとに関数化することで、計装の一貫性を保ちます。
// ドメインイベントの型定義interfaceDeviceInventoryBusinessContext{ packageInstanceId:string; stepInstanceId?:string; targetDeviceId?:string; operatorPersonId?:string; organizationId?:string;}// セルフレポート提出イベントfunctionputSelfReportSubmittedEvent(params:{ context: DeviceInventoryBusinessContext; reportResult:'OWNED'|'LOST'|'NOT_KNOWN'; deviceCondition?:'GOOD'|'BAD'; reporterPersonId:string; hasIssue:boolean;}):void{putAttributesToRootSpan({// イベントメタデータ'domain_event.type':'device_inventory_self_report_submitted','domain_event.category':'device_inventory','domain_event.timestamp':newDate().toISOString(),// ビジネスコンテキスト(どの業務パッケージか)'business_context.package_instance_id': params.context.packageInstanceId,'business_context.step_instance_id': params.context.stepInstanceId,'business_context.target_device_id': params.context.targetDeviceId,// イベント固有のデータ(業務的な成否)'event_data.report_result': params.reportResult,'event_data.device_condition': params.deviceCondition,'event_data.reporter_person_id': params.reporterPersonId,'event_data.has_issue': params.hasIssue,// 検索しやすいフラグ'event_data.is_owned': params.reportResult==='OWNED','event_data.is_lost': params.reportResult==='LOST',});}ポイント:ビジネスコンテキスト(どの業務パッケージか)とイベントデータ(何が起きたか)を分離する。
先述しましたが、今回のシステムではドメインの処理と、プラットフォーム側で行なっている業務の状態管理や進行の制御で責務が分かれています。
業務内で問題が起きたときに、プラットフォーム側かドメイン側かを切り分けるため、属性を明確に分類します。
// プラットフォーム側の計装functionputPlatformStartedEvent(params:{ packageInstanceId:string; packageType:string;// 'DeviceInventory' | 'Preboarding' | ... targetType:string;// 'Device' | 'Person' | ... targetId:string; operatorPersonId:string; stepCount:number;}):void{putAttributesToRootSpan({// operation.* = プラットフォーム共通の属性'operation.instance.id': params.packageInstanceId,'operation.type': params.packageType,'operation.target.id': params.targetId,// 棚卸し対象デバイス'operation.operator.person_id': params.operatorPersonId,// 棚卸しレポート実施者});}// ドメイン(デバイス棚卸し)側の計装functionputDeviceInventoryReportReviewedEvent(params:{ packageInstanceId:string; reviewedDeviceIds:string[]; reviewerPersonId:string; ownedDeviceCount:number; lostDeviceCount:number; issueDeviceCount:number; reviewResult:'APPROVED'|'REJECTED'|'REQUIRES_ACTION';}):void{const totalCount= params.reviewedDeviceIds.length;putAttributesToRootSpan({// domain.* = 業務固有の属性'domain.inventory.reviewed_device_count': totalCount,'domain.inventory.owned_device_count': params.ownedDeviceCount,'domain.inventory.lost_device_count': params.lostDeviceCount,'domain.inventory.issue_device_count': params.issueDeviceCount,'domain.inventory.review_result': params.reviewResult,'domain.inventory.reviewer_person_id': params.reviewerPersonId,// 計算済みメトリクス(ダッシュボード用)'domain.inventory.owned_rate': totalCount>0? params.ownedDeviceCount/ totalCount:0,'domain.inventory.issue_rate': totalCount>0? params.issueDeviceCount/ totalCount:0,});}| プレフィックス | 用途 | 例 |
|---|---|---|
operation.* | プラットフォーム共通の進捗追跡 | operation.type,operation.status |
domain.* | 業務固有の状態・結果 | domain.inventory.review_result |
business_context.* | 1業務を識別できるような横断のコンテキスト | business_context.instance_id |
domain_event.* | イベントのメタデータ | domain_event.type,domain_event.timestamp |
event_data.* | イベント固有のペイロード | 棚卸しのレポート内容など |
この分離は「キレイだから」ではなく、障害調査の初手を速くするため。
operation.* で"プラットフォーム上の進捗"を追い、domain.* で"業務としての成否/状態"を見る。混ぜるとどっちも弱くなります。
計装は「後で使う」前提だと迷走しがちなので、先に調査クエリ(見たい切り口)を決めます。
business_context.package_instance_id:<package_id>このクエリで、1つの棚卸しパッケージに関連するすべてのスパンを取得できます。
その上でoperation.step.status やdomain_event.type でフィルタして「どこで止まったか」を見ます。
domain.inventory.review_result:REJECTED OR domain.inventory.review_result:REQUIRES_ACTIONHTTPステータスではなく、業務的な判断(差戻し、要対応)で検索できます。
http.status_code:200 AND event_data.has_issue:trueこれが拾えること自体が、HTTPステータスだけでは得られない価値です。
domain_event.type:device_inventory_self_report_submitted AND event_data.report_result:LOST業務上重要な「紛失報告」だけを抽出できます。
domain_event.type:device_inventory_report_reviewed AND domain.inventory.owned_rate:<0.8所有確認率が80%未満のパッケージを検出し、問題のある棚卸しを特定します。
| やりたいこと | 必要なタグ | クエリ例 |
|---|---|---|
| 特定パッケージの追跡 | business_context.package_instance_id | 上記1 |
| 業務失敗の検出 | domain.*.review_result,event_data.has_issue | 上記2, 3 |
| 業務イベント種別での検索 | domain_event.type | 上記4 |
| メトリクスベースのアラート | domain.*.rate,domain.*.count | 上記5 |
先にこれらのクエリを決めてから、タグ設計を固める。
「全部やる」はだいたい破綻します。なので最初に優先順位を決めます。
| 調査頻度:低 | 調査頻度:高 | |
|---|---|---|
| 複雑度:高 | 優先 | 最優先で計装(デバイス棚卸しなど) |
| 複雑度:低 | 後回し(マスタ参照など) | シンプルな計装でOK(CRUD操作など) |
| 軸 | 説明 |
|---|---|
| 複雑度 | 内部実装の詳細知識が必要、データの関連が複雑 |
| 調査頻度 | 障害調査で見る頻度、問い合わせの多さ |
複雑度が高く、調査頻度も高い箇所を優先的に計装する。
業務的な問い合わせの起点になりやすく、非同期や状態遷移も絡みがちなものは計装対象として良さそうです。
棚卸しレポートを例に、実務で回すならこの順が安定します。
domain.* に入れるべき属性を決めるoperation.* に入れる進捗追跡用のキーを決める今回のお話のように、各システムで扱っている業務をどのように計装しているのか
どんなことで困っているのか、みたいな内容でO11y界隈盛り上がっていけたら嬉しいです!!
こちらの内容はObservability Conference Tokyo 2025で登壇した内容です。
https://speakerdeck.com/gamonges_dresscode/entapuraizubpmpuratutohuomuniokeruo11y
この記事は、社内で実践中のObservability改善の取り組みを元に書いています。
内容は試行錯誤中のため、フィードバックや議論を歓迎します。

PHPer→インフラエンジニア→SRE→ソフトウェアエンジニアになろうとしてる途中