メインコンテンツへスキップ
友田 陽大
可観測性・SRE
可観測性
OpenTelemetry
アーキテクチャ設計
Python
Next.js

OpenTelemetry 本番可観測性ガイド:トレース・メトリクス・ログを相関させ、止まった処理を一目で追えるようにする

OpenTelemetryで本番システムを可観測にする実装ガイド。3シグナル(トレース/メトリクス/ログ)とコンテキスト伝播の考え方から、FastAPI(Python)とNext.js(Node)の計装、OTel Collector、Head/Tailサンプリング、ログとトレースの相関、PIIスクラブ、テレメトリのコスト最適化までを公式準拠の実コードで解説します。

公開日
読了時間
23分
著者
友田 陽大
シェア

本番で一番こわいのは「落ちること」ではありません。「動いているのに遅い」「どこかで詰まっているが、どこかが分からない」——この状態です。エラーログには何も出ていない。なのにユーザーからは「処理が返ってこない」と言われる。ログを grep しても、リクエストが複数サービス(フロント → API → ワーカー → 外部API → DB)をどう通ったのかが繋がらない。

この「繋がらなさ」を解くのが**可観測性(Observability)**であり、その業界標準が OpenTelemetry(OTel) です。この記事は、OpenTelemetry で本番システムを可観測にするための実装ガイドです。題材として、私が国内大手放送事業者向けに構築した社内AIプラットフォーム(長時間AIジョブのパイプライン)で、「どのチャンク・どの段で処理が詰まったか」を追えるようにした設計判断も交えます。

この記事のルール:用語・API・設定の出典は OpenTelemetry 公式ドキュメント(2026年6月時点) に基づきます。SDK・パッケージ名・設定は更新が速いため、本番投入前に必ず公式ドキュメントで最新を確認してください。そして最重要の鉄則——テレメトリにPII(個人情報)を載せない。属性・ログ本文・スパン名に個人名や連絡先を入れた瞬間、可観測性基盤そのものが情報漏洩経路になります。


0. なぜ「ログだけ」では足りないのか

まず頭の中のモデルを正します。「ログを出していれば可観測だ」という誤解が、本番障害の調査を地獄にします。

ログはです。「14:03:12 に payment サービスでエラー」——これは一点の事実でしかありません。しかし実際の障害は、こういう形をしています。

ユーザーがボタンを押す → Next.js の Route Handler が FastAPI を呼ぶ → FastAPI が外部決済APIを叩く → そのレスポンスを待つ間に DB ロックが詰まる → 結果、フロントには「30秒後にタイムアウト」

このとき知りたいのは「どの一点でエラーが出たか」ではなく、**「1つのリクエストが、どのサービスを、どの順で、それぞれ何ミリ秒かけて通ったか」**です。これは点(ログ)の集合では復元できません。**線(トレース)**が要ります。

OpenTelemetry は「システムの動作を記述する出力=シグナル(Signals)」を扱います。公式が定義する主要シグナルは4つです。

シグナル公式の定義何に答えるか
トレース(Traces)アプリケーションを通過するリクエストの経路(the path of a request through your application)「このリクエストはどこを通り、どこで時間を食ったか」
メトリクス(Metrics)実行時に取得される測定値(a measurement captured at runtime)「全体としてエラー率・レイテンシ・スループットは健全か」
ログ(Logs)イベントの記録(a recording of an event)「その瞬間、具体的に何が起きたか」
バゲッジ(Baggage)シグナル間で受け渡される文脈情報(contextual information that is passed between signals)「テナントID等の文脈を、経路全体に持ち回りたい」

加えて公式は Events(特定種のログ)Profiles(コードレベルのリソース使用記録) を開発中と位置づけています。

役割分担はこう覚えてください——メトリクスで「異常がある」と気づき、トレースで「どこか」を特定し、ログで「なぜか」を読む。 この3点が相関して初めて、冒頭の「動いているのに遅い」が一目で追えるようになります。

なぜ3つも要るのか、と疑問に思うかもしれません。一言でいえば**「解像度」と「コスト」のトレードオフが3者で違う**からです。メトリクスは最も安く(数値の集約なので)、常時全件を見られるが粒度が粗い。ログは最も詳細だが量が膨大で、単体では文脈(どのリクエストか)が欠ける。トレースはその中間で、リクエスト1本の経路を構造として持つが、全件保存するとコストがかさむ(だから7章のサンプリングが要る)。この3層を相関させるのが OpenTelemetry の設計思想であり、どれか1つだけでは本番の障害調査は完結しません。


1. 用語の最小セット:スパンとトレース

トレースは**スパン(Span)**の木構造です。1スパン=1つの作業単位(HTTPハンドラ、DBクエリ、外部API呼び出しなど)で、スパンが親子関係を持って繋がり、全体で1本のトレースになります。

公式の操作概念で、本番に必要な最小セットはこれだけです。

  • スパン名:作業の名前(例 POST /jobstranscribe_chunk)。低カーディナリティに保つ(IDを入れない)。
  • 属性(Attributes):スパンに付ける key-value(例 job.idchunk.indexhttp.response.status_code)。検索・絞り込みの軸になる。
  • スパンイベント(Span Events):スパン内で起きた時点イベント(例 「リトライ開始」)。
  • スパンステータス(Status)OK / ERROR。エラー時に ERROR を立てる。
  • 例外記録(record exception):例外をスパンに添付し、スタックトレースを残す。
  • スパンコンテキスト:trace ID と span ID。これが伝播され、サービスをまたいでも同じトレースに繋がる(次章)。

ここで早くも設計原則が効きます。属性はDRYに、スパン名はKISSに。 スパン名に job.id のような高カーディナリティ値を埋め込むと、バックエンドが集計できずコストが跳ねます。可変値は属性へ、名前は種類を表す低カーディナリティに——これが鉄則です。


2. コンテキスト伝播:サービスをまたいでもトレースが繋がる仕組み

分散トレースの心臓部が**コンテキスト伝播(Context Propagation)**です。ここを理解すると、なぜ別々のサービスのスパンが1本に繋がるのかが腑に落ちます。

公式の定義はこうです。

  • Context:送信側・受信側が「あるシグナルを別のシグナルと相関させる」ための情報を持つオブジェクト。サービスAがサービスBを呼ぶとき、trace ID と span ID を渡すことで、Bは同じトレースに属し、Aのスパンを親とする新しいスパンを作れる。
  • Propagation:その Context をサービス・プロセス間で動かす仕組み。Context オブジェクトを直列化/復元する。通常は計装ライブラリが透過的に処理する。
  • Propagator:その移動を実装するもの。デフォルトの伝播は W3C TraceContext 仕様のヘッダを使う。

実体は HTTP ヘッダ1本、traceparent です。フォーマットは公式どおり <version>-<trace-id>-<parent-id>-<trace-flags> で、具体例は次の形です。

traceparent: 00-a0892f3577b34da6a3ce929d0e0e4736-f03067aa0ba902b7-01

サービスAは送信時に自分のコンテキストを traceparent ヘッダに**注入(inject)し、サービスBは受信時にそれを抽出(extract)**して自分のローカルコンテキストに入れる。これで親子関係が成立し、両サービスのスパンが同一トレースに連結されます。

実務上の重要点:HTTP クライアント/サーバーの計装ライブラリを入れていれば、この inject/extract は自動で行われます。 あなたが手で書くのは「サービス内の手動スパン」だけ。ただし、メッセージキュー・SSE・独自プロトコルをまたぐときは自動伝播が効かないことがあるので、そこだけ手動で traceparent 相当を運ぶ設計を意識します。後述の長時間ジョブで、ここがまさに勘所になりました。

ここで、もう1つの伝播対象バゲッジ(Baggage)にも触れておきます。trace ID/span ID が「トレースの骨格」を運ぶのに対し、バゲッジは「アプリ独自の文脈情報」(例 tenant.idrequest.priority)を経路全体に持ち回ります。たとえば入口で決まったテナントIDを、下流のサービスでもメトリクスの次元として使いたい——そういうときバゲッジが効きます。ただしバゲッジに載せた値は伝播ヘッダ経由で各サービスに流れるため、ここにPIIを入れるのは厳禁です(7章のスクラブ以前の問題として、そもそも載せない)。バゲッジは「便利だが、低カーディナリティかつ非機微な値だけ」と覚えてください。


3. FastAPI(Python)を計装する

Python の計装には2つの道があります。両方使うのが正解です。

3.1 ゼロコード計装(まず全体に網をかける)

公式の「zero-code」アプローチは、アプリのコードに手を入れずに自動計装を入れます。FastAPI・リクエストライブラリ・DBドライバなどに実行時のモンキーパッチで計装を差し込みます。

# distro は API/SDK/bootstrap/instrument ツールを含む。OTLP エクスポーターも入れる
pip install opentelemetry-distro opentelemetry-exporter-otlp

# 入っているライブラリを走査し、対応する計装ライブラリを自動インストール
opentelemetry-bootstrap -a install

起動はアプリのコマンドを opentelemetry-instrument でラップするだけです。

# 例:uvicorn で動く FastAPI を計装して起動
opentelemetry-instrument \
    --service_name broadcaster-ai-api \
    --traces_exporter otlp \
    --metrics_exporter otlp \
    --exporter_otlp_endpoint http://otel-collector:4317 \
    uvicorn app.main:app --host 0.0.0.0 --port 8000

同じことは環境変数でも設定できます(コンテナ運用ではこちらが扱いやすい)。

export OTEL_SERVICE_NAME=broadcaster-ai-api
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317

CLI 引数と環境変数は1対1に対応します(--service_nameOTEL_SERVICE_NAME など)。シークレットや接続先はコードに焼かず env へ——可観測性の設定もこの原則の例外ではありません。

これで「HTTPハンドラ・DB・外部HTTP」の自動スパンが出始めます。まず全体に網をかけるのがゼロコードの価値です。

3.2 手動計装(ビジネス上の関心事に名前を付ける)

自動計装は「技術的な境界」しか見えません。「OCRの段」「音声認識の段」「マルウェア検査の段」といったドメインの関心事は、自分でスパンを切って初めて可視化されます。公式の手動計装API(traces は Stable、metrics も Stable、logs は Development)はこう使います。

from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode

# トレーサーはモジュール単位で一度だけ取得して使い回す(SRP: 取得と利用を分離)
tracer = trace.get_tracer("broadcaster.ai.pipeline")


async def transcribe_chunk(job_id: str, chunk_index: int, audio: bytes) -> str:
    # スパン名は「種類」。可変値(job_id 等)は属性へ → 低カーディナリティを死守
    with tracer.start_as_current_span("transcribe_chunk") as span:
        # 属性 = 後で絞り込む軸。IDはここに置く(スパン名には置かない)
        span.set_attribute("job.id", job_id)
        span.set_attribute("chunk.index", chunk_index)
        span.set_attribute("chunk.size_bytes", len(audio))

        try:
            span.add_event("asr.request.start")  # 時点イベント
            text = await call_asr(audio)
            span.add_event("asr.request.done")
            return text
        except Exception as ex:
            # エラーはステータスを立て、例外を添付(スタックトレースが残る)
            span.set_status(Status(StatusCode.ERROR))
            span.record_exception(ex)
            raise

ネストすれば親子のスパンになり、処理の入れ子がそのままトレースの木になります。

with tracer.start_as_current_span("process_job") as parent:
    parent.set_attribute("job.id", job_id)
    for i, chunk in enumerate(chunks):
        # この子スパンは自動的に process_job を親に持つ
        await transcribe_chunk(job_id, i, chunk)

ゼロコード(網)+手動(要所)の二段構えが本番のスイートスポットです。自動計装で抜け漏れをなくし、ビジネス的に重要な段にだけ手で名前を付ける。これは「全部手で書く(DRY違反・工数爆発)」でも「自動だけ(ドメインが見えない)」でもない、ちょうどいい中間です。

3.3 メトリクスも同じトレーサー観で

「全体の傾向」はメトリクスで取ります。公式のカウンタはこう作ります。

from opentelemetry import metrics

meter = metrics.get_meter("broadcaster.ai.pipeline")

chunk_counter = meter.create_counter(
    "asr.chunks.processed",
    unit="1",
    description="処理済みチャンク数",
)

# 属性で次元を切る(work.type ごとに集計可能になる)
chunk_counter.add(1, {"result": "ok"})

「処理時間の分布」はヒストグラム、「同時実行ジョブ数」のような瞬間値は observable gauge(コールバックで観測)を使います。メトリクスはカーディナリティに最も敏感です。属性に job.id のような無限に増える値を入れると時系列が爆発し、コストが青天井になります。メトリクスの属性は有限集合result=ok|errorstage=ocr|asr 等)に限る——これがコスト破綻を防ぐ一線です。


4. Next.js(Node)を計装する

このポートフォリオサイトと同じく、フロント/BFF が Next.js(Node ランタイム)のケースです。Node も「自動計装+手動」の二段構えで考えます。

4.1 NodeSDK で自動計装をブートする

公式の Node セットアップは、SDK をアプリより先にロードするのが要点です。

npm install @opentelemetry/sdk-node \
  @opentelemetry/api \
  @opentelemetry/auto-instrumentations-node \
  @opentelemetry/sdk-metrics \
  @opentelemetry/sdk-trace-node

instrumentation.ts を作り、NodeSDK を起動します(下は公式の構成。本番では Console エクスポーターを OTLP に差し替えます)。

import { NodeSDK } from "@opentelemetry/sdk-node";
import { ConsoleSpanExporter } from "@opentelemetry/sdk-trace-node";
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
import {
  PeriodicExportingMetricReader,
  ConsoleMetricExporter,
} from "@opentelemetry/sdk-metrics";

const sdk = new NodeSDK({
  traceExporter: new ConsoleSpanExporter(), // 本番では OTLP エクスポーターに置換
  metricReader: new PeriodicExportingMetricReader({
    exporter: new ConsoleMetricExporter(),
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});

sdk.start();

実行はアプリより前にこのファイルを読み込ませます。

# TypeScript(Node v20+)
npx tsx --import ./instrumentation.ts app.ts

# JavaScript(ESM。ファイルは instrumentation.mjs)
node --import ./instrumentation.mjs app.js

Next.js 固有の注意:Next.js(App Router)には instrumentation.ts という公式フックがあり、Node ランタイムで register() が起動時に呼ばれます。OTel の初期化はここに置くのが筋です。ただし計装が動くのは Node ランタイムのみ——Edge ランタイムや、ビルド時に静的化されるルートでは挙動が変わります。「どのルートが Node で動いているか」を取り違えると「スパンが出ない」とハマるので、計装したい処理は Node ランタイムに寄せます(このサイトのルールでも、サーバーデータが要るものは RSC/Route Handler 側に置く方針です)。

4.2 手動スパンで「自分の関心事」を可視化

公式の Node 手動API はコールバック形式です。span.end()必ず呼ぶのがPythonの with との違いで、ここを忘れるとスパンが閉じません。

import { trace, SpanStatusCode } from "@opentelemetry/api";

const tracer = trace.getTracer("portfolio-bff", "1.0.0");

export async function submitContact(input: ContactInput): Promise<void> {
  return tracer.startActiveSpan("contact.submit", async (span) => {
    try {
      // 属性は「集計・検索の軸」だけ。本文・メール・氏名は載せない(PII禁止)
      span.setAttribute("contact.phase", input.phase);
      await sendViaResend(input);
      span.setStatus({ code: SpanStatusCode.OK });
    } catch (error) {
      span.setStatus({ code: SpanStatusCode.ERROR });
      span.recordException(error as Error);
      throw error;
    } finally {
      span.end(); // Node では明示的に閉じる。忘れるとスパンがリークする
    }
  });
}

メトリクスも対称的に書けます。

import { metrics } from "@opentelemetry/api";

const meter = metrics.getMeter("portfolio-bff", "1.0.0");
const submits = meter.createCounter("contact.submit.count");
submits.add(1, { phase: "discovery" }); // 属性は有限集合に限る

ここでも原則は同じ——属性に氏名・メール・電話・自由記述本文を絶対に入れない。 入れていいのは「フェーズタグ」のような集計可能で個人を特定しないラベルだけです(このサイトの規約でも、問い合わせフォームのPIIはフェーズタグ以上を残さない方針です)。


5. OTel Collector:ベンダーロックインを断つ要

ここまでのアプリは OTLP でテレメトリを「どこか」に送ります。その「どこか」をアプリから隠蔽するのが OpenTelemetry Collector です。公式の言葉では「テレメトリを受信・処理・エクスポートする**ベンダー非依存(vendor-agnostic)**の方法」。

なぜアプリから直接バックエンドに送らず、Collector を挟むのか。公式が production で推奨する理由がそのまま設計上の利点です。

  • アプリは素早く手放せる:アプリは Collector に投げて終わり。リトライ・バッチ・暗号化・機微データのフィルタリングは Collector が担う。
  • ベンダーを差し替えられる:「Cloud Monitoring → Datadog → Grafana」とバックエンドを変えても、変えるのは Collector の設定1か所。アプリのコードは無傷。これが ETC(Easy To Change)そのものです。
  • 横断的処理を一元化:サンプリング・PIIスクラブ・属性整形を、各アプリにバラ撒かず Collector に集約できる(DRY)。

Collector のパイプラインは4種+拡張で構成されます。

構成要素役割
Receiversテレメトリを受け取る(例 OTLP)
Processors変換・フィルタ・バッチ・サンプリング
Exportersバックエンドへ送る
Connectorsパイプライン同士を繋ぐ
Extensionsヘルスチェック等の付加機能

最小構成の config.yaml はこう書きます。受信 → 処理 → 送出の流れがそのまま service.pipelines に現れます。

receivers:
  otlp:
    protocols:
      grpc:          # 既定の OTLP gRPC(4317)
      http:          # OTLP HTTP(4318)

processors:
  memory_limiter:    # メモリ上限を守り、Collector自身の落下を防ぐ(信頼性)
    check_interval: 1s
    limit_mib: 512
  batch:             # まとめて送り、送出回数とコストを下げる

exporters:
  otlp/backend:
    endpoint: your-backend:4317

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlp/backend]
    metrics:
      receivers: [otlp]
      processors: [memory_limiter, batch]
      exporters: [otlp/backend]

デプロイ形態は2つ。Agent(各サービスに併置)と Gateway(中央集約)です。小さく始めるなら Agent、横断ポリシー(サンプリング・スクラブ)を効かせたいなら Gateway、というのが定石です。多くの本番は「各ノードに Agent → 中央 Gateway → バックエンド」の二段で組みます。

放送事業者向けのプラットフォームでは、マルウェア検査の結果を OpenTelemetry で Cloud Monitoring に送出しました。Collector を挟む設計の効きどころは、アプリは GCP を知らなくていい点です。送出先が Cloud Monitoring でも別ベンダーでも、アプリのコードは「OTLP で Collector に投げる」だけ。可観測性の出口を、ビジネスロジックから切り離せます。


6. PIIスクラブ:テレメトリを漏洩経路にしない

これは「やった方がいい」ではなく必須です。可観測性基盤は全サービスのデータが流れ込むハブなので、ここにPIIが乗ると、漏洩時の被害が最大化します。守りは二重にかけます。

第一の砦=アプリ側(入れない)。 4章で繰り返したとおり、属性・スパン名・ログ本文に個人情報をそもそも書かない。氏名は user.id(不可逆ID)に、メール本文は載せない、自由記述は要約もせず除外。最も安いのは「最初から入れない」ことです。

第二の砦=Collector 側(削る)。 それでも漏れる可能性に備え、エクスポート前に Collector のプロセッサで属性を削除・マスクします。公式が「機微データのフィルタリング(sensitive data filtering)」を Collector の役割として挙げているのは、まさにこの第二の砦のためです。

processors:
  # 機微属性を削除/ハッシュ化してから外部へ出す(出口での最終防衛)
  attributes/scrub:
    actions:
      - key: enduser.email
        action: delete          # メールはそもそも消す
      - key: http.request.header.authorization
        action: delete          # 認証ヘッダを残さない
      - key: user.id
        action: hash             # 必要なIDは不可逆化して相関だけ残す

設計思想は明快です——相関に必要なのは「同じ人かどうか」であって「誰か」ではない。 だから user.id はハッシュ化すれば相関は保てる。氏名やメールは可観測性に一切不要なので、最初から載せない・出口で消す。これが「可観測性とプライバシーの両立」の具体です。


7. サンプリング:コストと網羅性のトレードオフ

全トレースを保存すれば理想的ですが、高トラフィックではコストとストレージが破綻します。そこでサンプリング(標本抽出)で「全体を代表する一部」だけを残します。公式は「1000トレース/秒を超えるなら検討」を一つの目安にしています。判断は Head(頭)か Tail(尾)かの二択が軸です。

観点Head-based サンプリングTail-based サンプリング
判断時点トレース開始時(早期)トレース完成後(全スパンを見てから)
代表手法Consistent Probability(trace IDベースの確率抽出。例 5%)エラー・遅延・属性で条件抽出
「全エラーを残す」保証できない(完成前に決めるため)できる(エラーは必ず残す等が可能)
実装・運用簡単・軽量・効率的難しい・ステートフル・リソース集約的
向く規模小〜中、健全トラフィックが大半大規模、賢い取捨が要る

公式の核心的トレードオフはこうです。Head は軽いがエラーを取りこぼし得る。Tail はエラー・遅延を狙い撃ちできるが、全スパンを一旦バッファするためステートフルで重い。

実務の処方箋:

  • まずは Head(確率サンプリング)から。 SDK 側で trace ID ベースの一貫確率サンプリングをかければ、同一トレースが全サービスで整合的に「採る/採らない」されます(バラバラに採られて木が欠ける事故を防ぐ)。これは KISS で、ほとんどの中規模システムには十分です。
  • 「エラーと遅延は1件も逃したくない」段階で Tail を足す。 Collector の Tail Sampling Processor で「エラーは100%、正常は1%」のような方針を組みます。ただしステートフルで重いので、入れるのは「Head では取りこぼす重要トレースがある」と確信してから(YAGNI——先回りで重い仕組みを入れない)。

Collector 側の確率サンプリング例:

processors:
  probabilistic_sampler:
    sampling_percentage: 10   # 10% を一貫サンプリング(trace IDベースで整合)

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [memory_limiter, probabilistic_sampler, batch]
      exporters: [otlp/backend]

コストはサンプリングだけで決まらない。 メトリクスの属性カーディナリティ、ログの冗長さ、スパンの粒度——この全部が課金に効きます。「全部を最高精度で観測」は YAGNI です。SLOに関係する経路は厚く、それ以外は薄く——可観測性も投資配分の問題だと捉えると、コストが制御可能になります。


8. ログとトレースを相関させる:障害調査が一変する瞬間

ここが、本記事で一番おいしい部分です。全てのログ行に trace ID を載せる。 これだけで調査体験が劇的に変わります。

可観測性の本番運用フローは、こう回ります。

  1. メトリクス:「asr.chunks.processed{result=error} が急増」とダッシュボードが気づく。
  2. トレース:該当時間帯の ERROR トレースを開く。process_job → transcribe_chunk の木で、4番目のチャンクで13秒ハングして外部APIタイムアウトだと一目で分かる。
  3. ログ:そのスパンの trace ID で全サービスのログを横断検索。FastAPI・ワーカー・外部呼び出しのログが1リクエスト分だけ並ぶ。grep で点を拾う作業が消える。

実装はシンプルです。OTel のコンテキストから現在のスパンの trace ID を取り、構造化ログに添える。

from opentelemetry import trace

def log_context() -> dict[str, str]:
    """現在のスパンから trace_id / span_id を取り、ログに添えて相関させる。"""
    span = trace.get_current_span()
    ctx = span.get_span_context()
    if not ctx.is_valid:
        return {}
    return {
        "trace_id": format(ctx.trace_id, "032x"),
        "span_id": format(ctx.span_id, "016x"),
    }

# 構造化ログに必ず混ぜる(本文にPIIは入れない)
logger.info("asr chunk failed", extra={**log_context(), "chunk.index": i})

これで「トレースで詰まり所を特定 → その trace ID でログに飛ぶ → 原因の例外を読む」が地続きになります。メトリクス(どこかおかしい)→ トレース(ここだ)→ ログ(これが理由だ)——3シグナルが trace ID で縫い合わさって初めて、可観測性は「ダッシュボードを眺める」から「障害を解く」道具になります。


9. 可観測性が信頼性を支える:長時間ジョブはどこで詰まるか

最後に、これら全部が**信頼性(Reliability)**にどう効くかを、実例で締めます。

放送事業者向けプラットフォームの中核は、長時間のAIジョブでした。スタックは FastAPI(async)+ Cloud Run Jobs + Cloud Workflows。OCRと音声認識を Cloud Workflows で並列化し、**処理時間を約30%短縮(逐次18分 → 並列13分)**しています。進捗は Firestore スナップショット+SSE で準リアルタイムに配信しました。

この「並列化して13分かかるジョブ」で必ず問われるのが——**「13分のうち、どの段が支配的か」「失敗するなら、どのチャンク・どの段で詰まるか」**です。これは可観測性なしには答えられません。

  • スパンの木を見れば、OCR の段と ASR の段のどちらが律速かが時間幅で一目で分かる。並列化の効果検証も、推測ではなくトレースで裏が取れる。
  • チャンク単位でスパンを切ってあるので、**「4番目のチャンクだけ毎回遅い」**といった偏りが浮かぶ。入力データ側の問題か、外部APIの問題かを切り分けられる。
  • マルウェア検査の結果は OpenTelemetry で Cloud Monitoring に送出。**「検査で弾かれたのか、処理で詰まったのか」**をシグナルで判別できる。

ここで効いた設計の勘所が、2章で触れた伝播の手動補完です。Cloud Run Jobs/Workflows/SSE をまたぐと、HTTP の自動伝播だけではトレースが切れがちです。段の境界でコンテキスト(trace ID)を意図的に持ち回ることで、**「1つのジョブ=1本のトレース」**を保ちました。これがあるからこそ「どのチャンク・どの段で止まったか」を一目で追えます。

信頼性の文脈に一言だけ繋げます。SLO(例 ジョブ成功率99%、p95処理時間15分)を決め、エラーバジェットを設定するなら、その達成度はメトリクスで測り、逸脱の原因はトレースとログで掘る。可観測性は SLO を「測れる・守れる」ものに変える土台です。観測できないものは、約束(SLO)もできません。


10. まとめ:本番可観測性チートシート

迷ったときの早見表です。

  • 3シグナルの役割:メトリクスで気づく → トレースで場所を特定 → ログで理由を読む。
  • トレースの基本:スパン名は低カーディナリティ(種類)、可変値(ID)は属性へ。
  • 伝播:HTTPなら計装ライブラリが traceparent を自動で inject/extract。MQ・SSE・独自経路だけ手動補完。
  • Pythonopentelemetry-distro でゼロコント網をかけ、要所だけ start_as_current_span で手動計装。
  • Node/Next.jsNodeSDKgetNodeAutoInstrumentations() で自動、startActiveSpan で手動(span.end() を忘れない)。Node ランタイムに計装処理を寄せる。
  • Collector:アプリは OTLP で投げるだけ。ベンダー差し替え・サンプリング・スクラブを集約 → ロックイン回避(ETC)。
  • PII:第一の砦=入れない、第二の砦=Collector で削る/ハッシュ化。相関に必要なのは「誰か」でなく「同じ人か」。
  • サンプリング:まず Head(確率)。エラー・遅延を1件も逃せない段階で Tail を足す(YAGNI)。
  • 相関:全ログに trace_id を載せる。これだけで調査が grep から「トレースを辿る」へ変わる。
  • 信頼性:SLO/エラーバジェットを測る土台。観測できないものは守れない。

可観測性は「ツールを入れること」ではなく、「障害が起きたとき、どこで何が詰まったかを一目で追える状態を設計すること」です。私は放送事業者向けの社内AIプラットフォームで、長時間AIジョブを OpenTelemetry で可観測化し、「どのチャンク・どの段で止まったか」をトレースで追える本番システムとして組み上げました。生成AI(Claude Code)で実装を加速しつつ、PIIスクラブやサンプリング方針のような人間が判断すべき設計ゲートは自分の手で締める——この組み合わせで、速く・安く・安全に可観測性を入れます。

「自社のシステムを、止まったときに追える状態にしたい」——その設計から計装・Collector運用まで、一気通貫で伴走できます。 既存システムへの後付け計装の相談からでも、お気軽にどうぞ。


参考(公式ドキュメント)

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事で解説した技術の適用事例

国内大手放送事業者の番組制作を支援する社内AIプラットフォーム(OpenTelemetry で長時間AIジョブを可観測化)

ケーススタディを見る