オブザーバビリティ設計
実査機能を構成する全コンポーネント(新基盤 Next.js・minedia-www 新エンジン部・文字起こしワーカー・AI Agent・LiveKit Cloud)のログ・メトリクス・トレース・エラーの計測方針。インフラ(Cloudflare / Google Cloud)が未確定でも成立するよう、OpenTelemetry を計装の共通言語とする。
用語: 本ページでは「管理API」= 調査側サーバー → 基盤のサーバー間 REST(Room 作成・トークン発行など。REST API の同期プレーン)、「ルーム内API」= 参加者ブラウザ ⇔ 基盤のルーム内ランタイム(入室・チャットなど)を指す。
設計原則
| 原則 | 名前 | 内容 |
|---|---|---|
| P1 | ベンダー中立(OTel 一本) | 全コンポーネントを OTel SDK / OTLP で計装し、送信先は環境変数だけで差し替え可能にする。インフラ選定(Cloudflare / Google Cloud)の未決に監視設計を人質に取られない。唯一の意図的な例外がエラートラッキング(Sentry)— エラーの自動グルーピング・issue 管理は OTel が標準化しない領域のため。 |
| P2 | テレメトリ属性も「契約」 | サービス間の相関に使う属性名(room.uuid など)は API 契約と同格に管理する。命名が揺れた時点で横断検索は成立しない。 |
| P3 | 低トラフィック × ライブイベント特性 | 実査は「時間枠が固定された少数の高価値セッション」。リクエスト比率ベースの監視は母数不足で機能しないため、イベント駆動アラート(録画失敗=即時通知)を主軸にする。 |
| P4 | 相関ファースト | 障害調査の起点は常に「この Room / この参加者に何が起きたか」。全シグナルを room.uuid で横断検索できることを最優先する。 |
| P5 | PII・秘匿情報ゼロ | JWT・API key secret・表示名・発話/チャット本文はいかなるシグナルにも載せない。載せてよいのは不透明 ID(uuid / external_ref / jti)のみ。 |
計測対象マップ
| # | 境界 | 計測手段 | 相関キー |
|---|---|---|---|
| ① | minedia-www → core REST(管理API) | OTel 自動計装(HTTP client/server)+ W3C traceparent 伝播 | trace_id(同一トレース) |
| ② | core → minedia-www Webhook(W1〜W6) | 配信 attempt ごとに span。配信リクエストに発生元トレースを同梱し、受信側は span link で接続 | event.id + traceparent link |
| ③ | LiveKit → core webhook | LiveKit は traceparent を送らないため、受信 span に livekit.room_sid を付与して属性相関 | room_sid → room.uuid |
| ④ | 参加者ブラウザ → core(ルーム内API) | サーバー側 span(入室ゲート・チャット送信・認可)。ブラウザ側 RUM は将来判断 | room.uuid / participant.uuid / jti |
| ⑤ | core → 文字起こしワーカー(BullMQ) | job payload に traceparent を同梱。ワーカーは新トレースを開始し span link で接続 | recording.uuid / transcription.uuid |
| ⑥ | AI Agent(LiveKit Managed Hosting) | 自前コレクタを置けない前提で OTLP エンドポイントへ直送(環境変数のみで設定) | room.uuid |
| ⑦ | LiveKit メディア品質 | SaaS のため計装不可。Room ごとの provider_inspector_url(LiveKit ダッシュボード)+接続テストのサマリで代替 | room_sid |
相関設計
トレース伝播ルール
| 経路 | ルール | 理由 |
|---|---|---|
| 同期 REST(①④) | W3C Trace Context(traceparent)で親子接続 | 標準。自動計装で完結する |
| Webhook(②) | 受信側は新トレースを開始し span link で発生元に接続 | 配信リトライは最大 12 時間後まで走る(Webhook 仕様)。1 トレースに収めると duration が壊れる |
| 非同期ジョブ(⑤) | 同上(span link) | STT は分〜時間単位。enqueue → 完了を 1 トレースにしない |
| LiveKit webhook(③) | 伝播不可。属性相関(livekit.room_sid)のみ | 外部 SaaS |
標準属性(テレメトリ契約)
全コンポーネント・全シグナル共通。OTel の属性命名規約(dot 区切り小文字)に従う。
| 属性 | 型 | 付与対象 |
|---|---|---|
tenant.id | int | 全て |
room.uuid | string | Room に紐づく全操作(API 公開 ID) |
room.kind | string | interview / connection_test |
session.uuid | string | Session が存在する場合 |
participant.uuid | string | 参加者起点の操作 |
participant.role | string | moderator / observer など(JWT の role) |
token.jti | string | 入室ゲート・トークン失効 |
recording.uuid / transcription.uuid | string | 録画・文字起こし |
event.id / webhook.type / webhook.attempt | string / int | Webhook 配信・受信 |
livekit.room_sid | string | LiveKit 連携全て |
idempotency.key | string | 作成系 POST(秘匿情報ではない) |
禁止属性: JWT / API key の生値、display_name、email、チャット・発話の本文。これらはいかなるシグナルにも載せない(原則 P5)。
障害調査の 4 手: オペ画面の Room 詳細で room.uuid をコピー → ログ検索 → trace_id へジャンプ → provider_inspector_url でメディア層を確認。この導線が成立するよう、オペ画面に room.uuid のコピー導線を置く。
シグナル設計
トレース — 手動スパン
HTTP・DB・Redis は自動計装に任せ、ドメイン上意味のある操作だけ手動 span を切る。
| Span 名 | サービス | 内容・主属性 |
|---|---|---|
room.provision | core | Room 作成時の LiveKit CreateRoom 同期呼び出し+ロールバック。失敗時 error.type=RTC_PROVISION_FAILED |
token.issue | core | 参加トークン発行・Participant 生成。participant.role, force |
entry_gate.verify | core | 入室検証。却下理由を entry_gate.result に記録: ok / expired / banned |
chat.dispatch | core | 認可 → 永続化 → sendData 配信の 3 段 |
webhook.deliver | core | 配信 attempt 単位。webhook.type, webhook.attempt, ステータスコード |
webhook.receive.livekit | core | LiveKit webhook 処理。livekit.event, livekit.room_sid |
recording.control | core | StartEgress / StopEgress 呼び出し |
transcription.stage | worker | stage=extract(ffmpeg)/ stt / normalize(WebVTT)の 3 span |
core_client.request | www | core API 呼び出し(自動計装+ endpoint 属性) |
webhook.receive.core | www | W1〜W6 受信。署名検証の結果も属性化 |
メトリクスカタログ
HTTP の基本メトリクスは自動計装に任せ、ここではドメインメトリクスのみ定義する。接頭辞 interview.。
| メトリクス | 型 | ラベル | 目的 |
|---|---|---|---|
interview.room.provisions | counter | kind, result | Room 作成成功率。rtc_failed は LiveKit 障害の一次指標 |
interview.token.issuances | counter | role, forced, result | 発行量と異常(ban 済みへの再発行 409 など)の検知 |
interview.entry_gate.decisions | counter | result(3 値) | 入室失敗の理由別内訳。expired の急増はトークン再取得まわりの不具合シグナル |
interview.participants.active | gauge | kind | LiveKit webhook(joined / left)から算出する現在参加者数 |
interview.recording.transitions | counter | to_status | failed は即アラート |
interview.webhook.deliveries | counter | type, result, attempt | 配信健全性 |
interview.webhook.exhausted | counter | type | 5 回リトライ失効 → 破棄の件数。受信側の GET ポーリング補填が必要になった契約上の重大イベント |
interview.transcription.jobs | counter | result | 文字起こしジョブの成否 |
interview.transcription.stage.duration | histogram | stage | ffmpeg / STT / normalize の所要時間。STT ベンダの性能劣化検知 |
interview.transcription.queue.depth | gauge | — | BullMQ の waiting + delayed。滞留検知 |
interview.transcription.freshness | histogram | — | 録画完了 → 文字起こし完了通知(W6)までの経過秒。遅延監視 |
interview.connection_test.results | counter | result, failed_step | 接続テスト 8 ステップの失敗分布 |
interview.core_client.fallbacks | counter | reason | www 基盤 down 検知 → OpenTok フォールバック判断の発火回数 |
ログ
- 形式: 全サービス JSON 構造化ログ(1 行 1 イベント)。フィールドは標準属性+
severity/message/trace_id/span_id(OTel ログブリッジで自動注入)。 - レベル運用:
INFO= ドメインイベント(Room 作成・入退室・録画遷移・webhook 配信結果)、WARN= 自動回復した異常(リトライ成功・入室拒否)、ERROR= 要調査(provision 失敗・録画 failed・配信 exhausted)。 - アクセスログはインフラ層の標準機能に任せ、アプリでは重複して出さない。
- 本文の扱い: チャット・文字起こし・提示物の中身はログに載せない(P5)。長さ・件数のみ可。
- 監査との分界: 「誰が何をしたか」の証跡は
RoomEvent(DB)が正本。ログは運用調査用で保持 30 日。監査要件をログ基盤に負わせない。
エラートラッキング(Sentry)
未捕捉例外・要調査エラーの追跡は Sentry に集約する。minedia-www は既に Sentry を「エラー 100% 送信・本番の performance tracing 無効」で運用しており、本設計の分界と矛盾しない。
Sentry が担うもの
未捕捉例外・スタックトレースの自動グルーピング・リグレッション検知・release health。OTel が標準化しない領域であり、既存運用・通知フローに乗る。
OTel が担うもの
計装はすべて OTel で書く。Sentry の performance tracing は全サービス無効(tracesSampleRate: 0)にして二重トレースを防ぐ。
| サービス | SDK | 備考 |
|---|---|---|
| core(Next.js) | @sentry/nextjs または @sentry/cloudflare | デプロイ先(Cloud Run / Workers)に応じて選択。サーバー側のみ |
| 文字起こしワーカー | @sentry/node | ジョブの最終リトライ超過例外を必ず capture |
| minedia-www 新エンジン部 | 既存 sentry-ruby | 追加作業はタグ付与のみ。before_send の除外ルールに実査系を誤って含めない |
| AI Agent(Python) | sentry-sdk | Managed Hosting でも DSN(環境変数)だけで動作 |
- プロジェクト分割:
interview-core/interview-transcriber/interview-agentを www と別プロジェクトにする(issue のオーナーシップとアラート経路の分離)。 - タグ契約: 標準属性(
room.uuid/tenant.idなど)を同名で Sentry タグに付与。「Sentry issue →room.uuid→ ログ・トレース横断検索」が障害調査の 4 手に接続する。 - 依存の隔離: アプリコードから Sentry API を直接呼ばない。接点は「初期化 1 ファイル」+「明示 capture 用ラッパー 1 枚」に限定し、将来の乗り換えコストを固定する。
- Grafana を単一画面にする: Grafana Labs 公式の Sentry データソースプラグイン(Issues / Events / Org Stats をクエリ可・v2.0.0+ で Events の取得フィールド選択可・要 Grafana 10.4+・認証は Sentry Internal Integration トークン)で、ダッシュボードに Sentry issue をトレース・メトリクスと並置できる。Sentry UI ⇔ Grafana を行き来せず、Grafana 起点で調査を完結させる方向とする。
- PII:
sendDefaultPii: falseを明示。リクエストボディのキャプチャは無効。
コンポーネント別実装方針
インフラ未確定への備え: デプロイ先が Cloud Run(Node ランタイム)なら OTel Node SDK がフルに動く。Cloudflare Workers ではフル SDK が動かないため Workers 用 OTel ライブラリ+Workers Logs に切り替える。手動 span と属性契約はどちらでも同一に保つ — これが原則 P1 の効きどころで、インフラ決定を待たずに計装を書き始められる。
| コンポーネント | 実装方針 |
|---|---|
| core(Next.js) | OTel Node SDK+自動計装(http / pg / ioredis)。instrumentation.ts の register() から初期化。バンドラは require フックを壊し自動計装を無効化しうる点に注意 |
| 文字起こしワーカー | core と同じ SDK 構成。service.name=interview-transcriber。enqueue 時に traceparent を job payload へ同梱 |
| minedia-www 新エンジン部 | opentelemetry-ruby(rails / net_http / sidekiq)。計装スコープは新エンジン部のみで、既存 OpenTok レーンには手を入れない。エラーは既存 Sentry に任せる |
| AI Agent(Python) | opentelemetry-distro[otlp] で OTLP 直送。pre-fork 型ワーカーでは post-fork フックで provider 再初期化が必要 |
| LiveKit Cloud | 計装不可。provider_inspector_url と接続テスト結果で代替。webhook の欠落は日次の整合ジョブで補足(後述) |
| 実査フロント(ブラウザ) | 当面 RUM は入れない。接続テストの診断ログとサーバー側 span で代替し、導入は完全移行後に判断 |
共通環境変数(送信先の差し替えはここだけ)
OTEL_SERVICE_NAME=interview-core # サービスごとに変更
OTEL_RESOURCE_ATTRIBUTES="service.namespace=interview,deployment.environment=production"
OTEL_EXPORTER_OTLP_ENDPOINT=<OTLP エンドポイント>
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <credentials>"
アラート
方針(原則 P3): 実査の同時セッション数は一桁〜十数件で、5xx が 1 件出ただけで比率指標は大きく振れる。リアルタイム検知はイベント駆動アラートが担い、比率指標は月次レビュー用の後方指標に留める。
イベント駆動アラート
| Sev | 条件 | 意味 | 通知 |
|---|---|---|---|
| P1 即時 | recording.transitions{to_status="failed"} ≥ 1 | 納品物(録画)の毀損。現行 OpenTok の録画アラート運用の後継 | オンコール |
| P1 即時 | room.provisions{result="rtc_failed"} ≥ 1(実査時間帯) | LiveKit 障害=実査開始不能 | オンコール |
| P1 即時 | /health/ready 失敗 2 回連続 | 基盤 down。www 側フォールバック判断と連動 | オンコール |
| P2 15分 | entry_gate.decisions{result≠"ok"} が同一 Room で ≥ 3 | 特定 Room に入れない参加者の滞留 | Slack |
| P2 15分 | webhook.exhausted ≥ 1 | リトライ失効 → 破棄が発生。受信側のポーリング補填を確認 | Slack |
| P2 15分 | transcription.queue.depth > 0 が 60 分継続 / ジョブ最終失敗 | 文字起こしの滞留・完了通知の遅延 | Slack |
| P3 翌営業日 | core_client.fallbacks ≥ 1 / JWKS 取得・署名検証エラー | 構成劣化・鍵運用の異常 | Slack(低優先) |
| P3 翌営業日 | Sentry new issue / regression(interview-* プロジェクト) | 未知のエラーパターン出現 | Slack(Sentry 通知。ページングには使わない) |
整合性監視(split-brain 対策)
webhook 欠落時に core と minedia-www の状態が乖離するリスクに対し、www 側に日次の突合ジョブ(ローカル参照 ⇔ core の Room 状態照合)を置き、乖離件数を interview.reconciliation.mismatches counter で出す。> 0 で P3 アラート。
バックエンドとコスト
バックエンド比較
| 案 | 構成 | 評価 |
|---|---|---|
| Grafana Cloud 推奨 | 全サービス → OTLP gateway | インフラ選定(Cloudflare / GCP)と独立に成立する唯一の案。無料枠(メトリクス 10k series・ログ/トレース各 50GB/月)で PoC〜二重運用初期は基本料のみ。乗り換えも環境変数の差し替えだけ(P1) |
| GCP Cloud Operations | Cloud Trace / Logging / Monitoring | GCP 主軸確定なら自然。ただし Cloudflare 主軸に転んだ場合に作り直し |
| Cloudflare 内蔵+外部 | Workers Logs / Analytics Engine +外部トレース基盤 | Cloudflare 主軸でもトレースは外部が必要になり結局 2 系統。単独では不成立 |
エラートラッキングはいずれの案でも Sentry で確定(既存契約の増分利用)。バックエンドの最終決定はインフラ ADR と同時に行う。
コスト試算(Grafana Cloud Pro・月 200 インタビュー想定)
前提: 月 200 インタビュー・平均 6 参加者 × 60 分(= 1,200 参加者セッション/月)。
| 項目 | 見積もり量/月 | 超過費 |
|---|---|---|
| 基本料金(Pro) | — | $19 |
| トレース | ≤ 1GB(約 80 万 span) | $0 — 無料枠 50GB の 2%。100% 保持でも余裕 |
| ログ | ≤ 1GB | $0 — 無料枠 50GB 内 |
| メトリクス | 3k〜5k active series | $0 — 無料枠 10k series 内。series 数はトラフィック非依存のため案件が 10 倍でもほぼ不変 |
| (参考)合成監視 | readiness 86k 実行と仮定 | $0〜37 — 採用は意思決定待ち。採用時はロケーション数が乗数になる点に注意(1 ロケーションなら無料枠 100k 内) |
| ユーザー | 3 名まで込み | $0〜16(5 名なら +$16) |
| 合計 | — | $19〜75 / 月(現実的な着地 〜$35) |
コストが跳ねる契機は 2 つだけ: ①メトリクスのカーディナリティ事故(uuid をラベルに載せる等 — 規約で禁止済み)、②(合成監視を採用する場合)プローブ数 × ロケーション数。テレメトリ量は月 2,000 インタビューでも無料枠内であり、量を理由にサンプリングを導入する必要はない。
サンプリング・保持
- トレースは当面 100% 保持(
parentbased_always_on)。1 セッションの障害調査価値が高く、量も問題にならない。head sampling は「稀な障害を落とす」ため採らない。 - 量が問題になったら tail sampling(エラー・遅延トレースは全量、正常系を絞る)へ移行。
- 保持期間: ログ 30 日 / メトリクス 13 ヶ月 / トレース 30 日。
セキュリティ・PII
| ルール | 対象 |
|---|---|
JWT・API key secret・webhook secret の生値をログ / span / メトリクスラベルに載せない。識別には jti / key_id を使う | 全サービス |
display_name・email・発話 / チャット / 文字起こし本文を載せない。件数・バイト長のみ可 | 全サービス(P5) |
Authorization 等のヘッダは自動計装のキャプチャ対象から除外(デフォルト無効を維持) | HTTP 計装 |
| メトリクスラベルに高カーディナリティ値(uuid・jti)を使わない。uuid はログ・span 属性のみ | 全メトリクス |
| テレメトリ送信先の認証情報は Secret 管理(Secret Manager / wrangler secret)。環境変数直書き禁止 | デプロイ構成 |
| テレメトリに PII が無いことを維持し、データレジデンシ要求が確定した場合に監視バックエンドを対象外と整理できるようにする | インフラ ADR 連動 |
段階導入プラン
計装は後付けにせず、core の最初のエンドポイントから標準属性の契約で書き始める(contract-first はテレメトリにも適用)。
| Phase | 対応する開発段階 | 導入内容 |
|---|---|---|
| A | core PoC 着手時 | OTel SDK 初期化・構造化ログ・標準属性・/health /health/ready・Sentry SDK 導入(tracesSampleRate: 0)。バックエンドは Grafana Cloud Free で開始 |
| B | core API 実装(www 側は mock 相手の契約テスト) | 手動 span・ドメインメトリクス・webhook への traceparent 同梱。Sentry → トレースの相関検証 |
| C | www 側の契約実装 → core 結合 | www 新エンジン部の計装・webhook の span link 接続・整合ジョブメトリクス |
| D | 二重運用開始 | アラート本番化・ダッシュボード 2 枚(下記) |
| E | 完全移行後 | ブラウザ RUM の導入判断・tail sampling の要否判断 |
ダッシュボード構成(Phase D で 2 枚)
1. 実査ライブ
現在アクティブな Room 一覧・入室ゲート却下・録画状態・LiveKit provision 失敗。
2. API 健全性
管理API / ルーム内API / webhook それぞれのレート・エラー・レイテンシ + 配信リトライ・キュー滞留。
未決事項
| # | 事項 | 依存 |
|---|---|---|
| OBS-1 | Webhook 配信への traceparent ヘッダ追加を通信契約(Webhook 仕様)に反映する | 契約改訂 |
| OBS-2 | 監視バックエンドの最終決定をインフラ ADR に含める(本設計は Grafana Cloud 先行開始を推奨) | インフラ ADR |
| OBS-3 | オンコール体制(P1 アラートの受け手・エスカレーション)。現行の録画アラート運用の引き継ぎ先 | 運用設計 |
| OBS-4 | AI Agent(Managed Hosting)で OTLP 直送が実際に通るかの検証 | LiveKit PoC |