Movatterモバイル変換


[0]ホーム

URL:


DRESS CODE TECH BLOGDRESS CODE TECH BLOG
DRESS CODE TECH BLOGPublicationへの投稿
🤙

O11y実践の話で盛り上がりたい

に公開

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

TL;DR

はじめに:この記事で伝えたいこと

この記事では、O11y / SREの一般論や文化論はあんまりしません。

「実務で障害調査が進まない理由」と「どう計装設計に落とすか」 を、デバイス棚卸しレポートのドメインイベントを題材に、具体的な設計・実装パターンとして書きます。

なお、内容は試行錯誤中のものとなっています。

この記事のゴール

「計装を始める上で最初に整理するべきもの」と「実装パターン」など具体的な話で盛り上がりたい

前提:プラットフォームとドメインの二層構造

本記事で扱うシステムは、以下のような構造を持っています:

  • プラットフォーム層:ワークフロー実行、タスク管理、状態遷移を司る共通基盤
  • ドメイン層:デバイス棚卸し、入社手続きなど、業務固有のロジック

この二層構造が、後述する「計装の分離」の前提になります。

なぜ、これで盛り上がりたいのか

障害調査でよくある苦しみは、こういうやつです。

  • 監視はしている(レイテンシ、エラー率、ログ、トレース)
  • でも障害調査になると「結局、業務フローのどこで止まった?」が追えない
  • 内部のプラットフォームの理解が必要になって詰まる
  • 「これはプラットフォーム側の問題?それとも業務(ドメイン)側の問題?」が切り分けられない

ここまで具体的な話になると、SRE本やO11y資料の中に書いてある文化や考え方などを
さらに実務の領域に昇華していかないといけません。

次はこういった実務レベルの話で、どう計装しているのか、で盛り上がりたいなーと思って今回の記事を書いてます。

具体例:デバイス棚卸しでの困りごと

たとえば、以下のような流れで行われるデバイス棚卸しの業務で「従業員からのレポートが提出されていない」という問い合わせが来たとします。

  1. APIのログを見る → すべて HTTP 200
  2. DBを見る → レコードは存在する
  3. でも顧客は「棚卸しが完了していない」と言っている

この状況で、以下のどれが原因かを切り分けるのに時間がかかります:

  • セルフレポートの依頼が送られていない?
  • 依頼は送られたが、レポートが提出されていない?
  • レポートは提出されたが、レビューが完了していない?
  • レビューは完了したが、パッケージ全体のステータスが更新されていない?

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つの計装レイヤー

実際にどう実装するか、3つのパターンに分けて紹介します。
また今回はDatadogのdd-traceを使って計装コードを書いていきます。

パターン1:基本的なスパン属性の付与

まず、現在のトレースのスパンに属性を付与する基本関数です。

// ルートスパンに属性を付与する汎用関数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,});// ... 業務処理}

ポイント:ログではなくスパン属性に出力することで、トレース画面から直接検索・フィルタ可能になる。

パターン2:ドメインイベント関数の設計

イベント種別ごとに関数化することで、計装の一貫性を保ちます。

// ドメインイベントの型定義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',});}

ポイント:ビジネスコンテキスト(どの業務パッケージか)とイベントデータ(何が起きたか)を分離する。

パターン3:プラットフォーム属性とドメイン属性の分離

先述しましたが、今回のシステムではドメインの処理と、プラットフォーム側で行なっている業務の状態管理や進行の制御で責務が分かれています。

業務内で問題が起きたときに、プラットフォーム側かドメイン側かを切り分けるため、属性を明確に分類します。

// プラットフォーム側の計装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.* で"業務としての成否/状態"を見る。混ぜるとどっちも弱くなります。

クエリ駆動で計装を設計する

計装は「後で使う」前提だと迷走しがちなので、先に調査クエリ(見たい切り口)を決めます。

調査クエリ例:棚卸しレポート

1. ある業務パッケージの進捗を追う

business_context.package_instance_id:<package_id>

このクエリで、1つの棚卸しパッケージに関連するすべてのスパンを取得できます。
その上でoperation.step.statusdomain_event.type でフィルタして「どこで止まったか」を見ます。

2. 業務としての失敗を抽出する

domain.inventory.review_result:REJECTED OR domain.inventory.review_result:REQUIRES_ACTION

HTTPステータスではなく、業務的な判断(差戻し、要対応)で検索できます。

3. 「HTTP 200だけど業務失敗」のケースを拾う

http.status_code:200 AND event_data.has_issue:true

これが拾えること自体が、HTTPステータスだけでは得られない価値です。

4. 紛失デバイスの一覧を取得する

domain_event.type:device_inventory_self_report_submitted AND event_data.report_result:LOST

業務上重要な「紛失報告」だけを抽出できます。

5. 完了率の低いパッケージを特定する

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操作など)
説明
複雑度内部実装の詳細知識が必要、データの関連が複雑
調査頻度障害調査で見る頻度、問い合わせの多さ

複雑度が高く、調査頻度も高い箇所を優先的に計装する。

業務的な問い合わせの起点になりやすく、非同期や状態遷移も絡みがちなものは計装対象として良さそうです。

実践のためのチェックリスト

棚卸しレポートを例に、実務で回すならこの順が安定します。

Step 1: 業務フローの整理

  • 対象業務を1つ決める(例:デバイス棚卸し)
  • 業務フローを箇条書きにする(ステップ列挙)
  • 各ステップにドメインイベント名を付ける

Step 2: イベント辞書の作成

  • 「業務としての成功/失敗」を定義する
  • domain.* に入れるべき属性を決める
  • operation.* に入れる進捗追跡用のキーを決める

Step 3: クエリ駆動の設計

  • 「こう調べたい」というクエリを先に書く
  • クエリに必要なタグを洗い出す
  • タグ設計を固める

Step 4: 優先順位づけ

  • 複雑度 × 調査頻度で評価する
  • 最初にやる範囲を絞る(全部やらない)

Step 5: 実装と検証

  • ドメインイベント関数を実装する
  • 実際のトレースで検索できるか確認する
  • 障害調査で使ってみてフィードバックを得る

Step 6: 継続的な改善

  • 障害調査のたびにタグ/イベント辞書を更新する
  • 試行錯誤を前提に回す

おわりに

この記事で伝えたかったこと

  • エンドポイント監視は必要。でもそれだけだと、業務フローの信頼性は説明できない
  • 技術エラーと業務エラーのギャップを埋めるには、ドメインイベントを"観測の第一級"にするのが効く
  • そのためには、優先順位づけと、プラットフォーム/ドメインの計装分離がセットで必要

今回のお話のように、各システムで扱っている業務をどのように計装しているのか
どんなことで困っているのか、みたいな内容でO11y界隈盛り上がっていけたら嬉しいです!!


こちらの内容はObservability Conference Tokyo 2025で登壇した内容です。
https://speakerdeck.com/gamonges_dresscode/entapuraizubpmpuratutohuomuniokeruo11y

!

この記事は、社内で実践中のObservability改善の取り組みを元に書いています。
内容は試行錯誤中のため、フィードバックや議論を歓迎します。

DRESS CODE TECH BLOG により固定

DRESS CODEで情シス・人事労務・採用・総務といったWorkforce Management領域の課題を解決!

■ Dress Code株式会社、2024年9月設立、2025年4月正式創業
■ 企業経営・業務の摩擦問題を解消する「DRESS CODE」を開発中
■ 日本・インドネシア・ベトナム・タイ・シンガポールといったアジアを中心にグローバル展開中
■ プレシード及びシードラウンドにて14.1億円の資金調達完了

ご興味がある方はかわうそまたはぷーじまでご連絡ください。

gamonges

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

DRESS CODE TECH BLOG

DRESS CODEのProduct & Technologyチームによるテックブログです!プロダクトマネジメントからモデリング、アーキテクチャ、フロントエンド、バックエンド、SRE、セキュリティなどなど様々なテーマで情報を発信しています!

Discussion

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

目次
  1. TL;DR
  2. はじめに:この記事で伝えたいこと
    1. この記事のゴール
    2. 前提:プラットフォームとドメインの二層構造
  3. なぜ、これで盛り上がりたいのか
    1. 具体例:デバイス棚卸しでの困りごと
  4. なぜドメインイベントをトレースするのか
    1. エンドポイント指標の限界
    2. ドメインイベントとは
    3. ドメインイベントをトレースすると見えるようになること
  5. 題材:デバイス棚卸しの業務フロー
    1. 業務の成功/失敗を定義する
    2. 業務を"イベントの辞書"にする
  6. 実装パターン:3つの計装レイヤー
    1. パターン1:基本的なスパン属性の付与
    2. パターン2:ドメインイベント関数の設計
    3. パターン3:プラットフォーム属性とドメイン属性の分離
    4. タグ命名規約のまとめ
  7. クエリ駆動で計装を設計する
    1. 調査クエリ例:棚卸しレポート
    2. クエリ設計のポイント
  8. 計装の優先順位づけ
    1. 判断軸:複雑度 × 調査頻度
  9. 実践のためのチェックリスト
    1. Step 1: 業務フローの整理
    2. Step 2: イベント辞書の作成
    3. Step 3: クエリ駆動の設計
    4. Step 4: 優先順位づけ
    5. Step 5: 実装と検証
    6. Step 6: 継続的な改善
  10. おわりに
    1. この記事で伝えたかったこと

[8]ページ先頭

©2009-2025 Movatter.jp