A+B 複合 — Protocol + Registry で if-elif 根絶(Ch5)× Native Sidecar + OTel Tail Sampling

2026-05-23 (Day 48) 土曜複合問題 ★★★☆☆ Python 3.12 / Protocol / StrEnum / dataclass(slots=True) K8s 1.30+ / Native Sidecar / OTel Collector Contrib 0.100+

概要

🔌

Protocol + Registry(Ch5)

if-elif チェーンを Protocol インターフェース + dict レジストリに置き換え、新チャネル追加時に既存コード変更ゼロ(OCP)を実現する。

🏷️

StrEnum × dataclass(slots=True)

チャネル識別子を StrEnum で型安全化し、各 Notifier の設定を slots=True dataclass で値オブジェクト化する(Ch2 型の活用)。

🛡️

Native Sidecar(K8s 1.28+)

initContainersrestartPolicy: Always でサイドカーの起動順序を保証。アプリが起動する前に OTel Collector が必ず Ready になる。

✂️

Tail Sampling でコスト削減

エラー or レイテンシ 200ms 超のトレースのみ DataDog に送信。ヘルスチェック・正常高頻度リクエストを間引き、可観測性コストを大幅削減する。

問題 A: コーディング — 条件分岐の削減 × Protocol(Ch5)

以下の「悪いコード」は ECサイトの通知サービス(メール/Slack/SMS)を実装したもの。問題点を全て洗い出し、Ch5(条件分岐の削減) および Ch2(型の活用) を適用してリファクタリングせよ。

制約・前提条件

  • Python 3.12+、型ヒント・dataclass(slots=True) を使うこと
  • 各通知チャネルを Protocol で抽象化すること
  • 新たなチャネルを追加する際に send_notification() を変更しなくて済む設計(OCP)にすること
  • Google スタイル docstring・インラインコメント・名前付き定数を含めること
期待する回答形式: 問題点の列挙(番号付き)+ 改善後コード + 実行例(input→output)+ 適用した設計パターン名と書籍対応章

悪いコード (Before) — カテゴリ A

このコードには 7つの設計上の問題 が隠れています。見つけてみてください。
bad_notifier.py — 問題だらけの通知サービス
import requests

# 問題1: ntype が str — タイポを検知できない
# 問題2: if-elif チェーン — 新チャネル追加のたびにこの関数を修正
# 問題3: 型ヒントなし
def send_notification(ntype, user, message):
    if ntype == "email":
        if user.get("email"):
            # 問題5: API URL がハードコード
            requests.post("https://email-api/send", json={
                "to": user["email"],
                "body": message
            })
        else:
            # 問題4: print のみ — 例外を投げない
            print("no email address")
    elif ntype == "slack":
        if user.get("slack_id"):
            requests.post("https://slack-api/chat.postMessage", json={
                "channel": user["slack_id"],
                "text": message
            })
        else:
            print("no slack id")
    elif ntype == "sms":
        if user.get("phone"):
            requests.post("https://sms-api/send", json={
                "to": user["phone"],
                "body": message
            })
        else:
            print("no phone number")
    else:
        # 問題7: LINE が必要になったらここにまた elif を追加…
        print(f"unknown type: {ntype}")

# 問題6: requests.post が直接呼ばれておりテスト不可(DI なし)
# → mock なしでは単体テストが書けない
問題点サマリー(7点)
1ntype が str — タイポを静的に検知できない
2if-elif チェーン(OCP 違反) — 新チャネルで関数を変更しなければならない
3型ヒントが皆無 — mypy で検査不可
4print のみのエラー処理 — 呼び出し元が失敗を検知できない
5API URL がハードコード — 環境変数化や設定管理ができない
6requests.post が直接呼び出し — 単体テストでモック不可
7設定と処理ロジックが混在 — 単一責任違反

ヒント A(段階的開示)

ヒント1 — 方向性
if ntype == "email": ... elif ntype == "slack": ... という条件分岐は「チャネルを追加するたびにこの関数を修正する」必要がある。これは「Open-Closed Principle(開放閉鎖原則)」に反する典型的なパターン。各チャネルを「インターフェース」として切り出し、辞書(レジストリ)にマッピングすることで if-elif を完全に廃止できる。
ヒント2 — アプローチ
  • ProtocolNotifier インターフェースを定義する(send(destination, message) メソッド)
  • EmailNotifier, SlackNotifier, SmsNotifier を個別クラスとして実装
  • 文字列 → Notifier インスタンスのマッピングは dict[Channel, Notifier] で持つ(if-elif 廃止)
  • チャネル識別子は StrEnum で定義するとタイポをコンパイル時に検出できる
  • 各 Notifier の API URL 等の設定は dataclass(slots=True) に閉じ込める
ヒント3 — コードの骨格
from typing import Protocol, runtime_checkable
from dataclasses import dataclass
from enum import StrEnum

class Channel(StrEnum):
    EMAIL = "email"
    SLACK = "slack"
    SMS   = "sms"

@runtime_checkable
class Notifier(Protocol):
    def send(self, destination: str, message: str) -> None: ...

@dataclass(slots=True)
class EmailNotifier:
    api_url: str
    def send(self, destination: str, message: str) -> None: ...

# レジストリ(if-elif の代わり)
NOTIFIER_REGISTRY: dict[Channel, Notifier] = {
    Channel.EMAIL: EmailNotifier(api_url=EMAIL_API_URL),
    Channel.SLACK: SlackNotifier(api_url=SLACK_API_URL),
    Channel.SMS:   SmsNotifier(api_url=SMS_API_URL),
}

def send_notification(channel: Channel, destination: str, message: str) -> None:
    notifier = NOTIFIER_REGISTRY.get(channel)
    if notifier is None:
        raise ValueError(f"Unknown channel: {channel!r}")
    notifier.send(destination, message)

問題点分析 — カテゴリ A

#問題点分類改善方法
1ntypestr — タイポを検知できない型安全性StrEnum Channel で列挙型化
2if-elif チェーンで新チャネル追加時に関数を変更しなければならないOCP 違反 Ch5Protocol + レジストリ dict に分離
3型ヒントが皆無型安全性 Ch2全引数・戻り値に型ヒント付与
4print のみ — 呼び出し元が失敗を検知できないエラー処理専用例外 NotificationErrorraise
5API URL がハードコード設定管理名前付き定数または設定クラスに分離
6requests.post が直接呼び出しでテスト不可テスタビリティraise_for_status() + try/except で安全化
7各チャネルの設定・ロジックが1関数に混在単一責任 Ch6各 Notifier クラスが1チャネルのみ担当

模範解答 A

"""notifier.py — 通知サービス(Email / Slack / SMS)の Protocol ベース実装。

良いコード・悪いコードで学ぶ設計入門(改訂新版)
  - Ch2: 型の活用(StrEnum, 型ヒント)
  - Ch5: 条件分岐の削減(Protocol + レジストリ dict)
  - Ch6: 単一責任(各 Notifier クラスが1チャネルの責務のみ持つ)
"""
from __future__ import annotations

import logging
from dataclasses import dataclass
from enum import StrEnum
from typing import Final, Protocol, runtime_checkable

import requests

logger = logging.getLogger(__name__)

# ── 定数(API URL を名前付き定数で管理)──────────────────────────────────
EMAIL_API_URL: Final[str] = "https://email-api/send"
SLACK_API_URL: Final[str] = "https://slack-api/chat.postMessage"
SMS_API_URL:   Final[str] = "https://sms-api/send"


# ── 列挙型(Ch2: 型の活用 — タイポをコンパイル時に検出)────────────────────
class Channel(StrEnum):
    """通知チャネル識別子。文字列互換でありつつ型安全。"""
    EMAIL = "email"
    SLACK = "slack"
    SMS   = "sms"
    # 新チャネル追加はここに1行足すだけ


# ── カスタム例外 ─────────────────────────────────────────────────────────
class NotificationError(RuntimeError):
    """通知送信に失敗した場合の例外。呼び出し元でキャッチ可能にする。"""


# ── Notifier Protocol(Ch5: if-elif を dict に置き換えるための共通型)────────
@runtime_checkable
class Notifier(Protocol):
    """通知チャネルの共通インターフェース。

    新チャネルを追加する際はこのプロトコルを実装したクラスを作成し
    NOTIFIER_REGISTRY に登録するだけでよい。send_notification() は変更不要(OCP)。
    """
    def send(self, destination: str, message: str) -> None:
        """通知を送信する。

        Args:
            destination: 送信先識別子(メールアドレス / チャンネルID / 電話番号)。
            message: 送信するメッセージ本文。

        Raises:
            NotificationError: 送信に失敗した場合。
        """
        ...


# ── 各チャネルの実装(dataclass で設定を値オブジェクト化)──────────────────
@dataclass(slots=True)  # slots=True でメモリ効率化 + 属性追加ミスを型検査で防ぐ
class EmailNotifier:
    """Email 通知チャネル。"""
    api_url: str  # 設定を dataclass フィールドに閉じ込める

    def send(self, destination: str, message: str) -> None:
        """Email を送信する。"""
        try:
            resp = requests.post(
                self.api_url,
                json={"to": destination, "body": message},
                timeout=10,  # タイムアウトを明示
            )
            resp.raise_for_status()  # 4xx/5xx で例外を送出(Fail-Fast)
        except requests.RequestException as exc:
            # 内部例外を NotificationError でラップして呼び出し元に伝播
            raise NotificationError(
                f"Email send failed to {destination}: {exc}"
            ) from exc
        logger.info("Email sent to %s", destination)


@dataclass(slots=True)
class SlackNotifier:
    """Slack 通知チャネル。"""
    api_url: str

    def send(self, destination: str, message: str) -> None:
        """Slack メッセージを送信する。"""
        try:
            resp = requests.post(
                self.api_url,
                json={"channel": destination, "text": message},
                timeout=10,
            )
            resp.raise_for_status()
        except requests.RequestException as exc:
            raise NotificationError(
                f"Slack send failed to {destination}: {exc}"
            ) from exc
        logger.info("Slack message sent to %s", destination)


@dataclass(slots=True)
class SmsNotifier:
    """SMS 通知チャネル。"""
    api_url: str

    def send(self, destination: str, message: str) -> None:
        """SMS を送信する。"""
        try:
            resp = requests.post(
                self.api_url,
                json={"to": destination, "body": message},
                timeout=10,
            )
            resp.raise_for_status()
        except requests.RequestException as exc:
            raise NotificationError(
                f"SMS send failed to {destination}: {exc}"
            ) from exc
        logger.info("SMS sent to %s", destination)


# ── レジストリ(if-elif チェーンを dict に置き換え)────────────────────────
# 新チャネルを追加する場合: クラスを作成してここに1行追加するだけ
NOTIFIER_REGISTRY: dict[Channel, Notifier] = {
    Channel.EMAIL: EmailNotifier(api_url=EMAIL_API_URL),
    Channel.SLACK: SlackNotifier(api_url=SLACK_API_URL),
    Channel.SMS:   SmsNotifier(api_url=SMS_API_URL),
}


def get_notifier(channel: Channel) -> Notifier:
    """チャネルに対応する Notifier を返す。

    Args:
        channel: 通知チャネル識別子。

    Returns:
        対応する Notifier インスタンス。

    Raises:
        ValueError: 未登録のチャネルが指定された場合(利用可能チャネル一覧を含むメッセージ)。
    """
    notifier = NOTIFIER_REGISTRY.get(channel)
    if notifier is None:
        available = ", ".join(c.value for c in NOTIFIER_REGISTRY)
        raise ValueError(
            f"Unknown channel: {channel!r}. Available: {available}"
        )
    return notifier


def send_notification(channel: Channel, destination: str, message: str) -> None:
    """指定チャネルへ通知を送信するエントリーポイント。

    新しいチャネルを追加する際、この関数を変更する必要はない(OCP)。

    Args:
        channel: 通知チャネル識別子。
        destination: 送信先識別子(チャネルにより意味が異なる)。
        message: 送信メッセージ本文。

    Raises:
        ValueError: 未登録のチャネルが指定された場合。
        NotificationError: 送信に失敗した場合。

    Example:
        >>> send_notification(Channel.EMAIL, "user@example.com", "注文が確定しました")
        >>> send_notification(Channel.SLACK, "#orders", "新規注文 #ORD-123 が到着しました")
    """
    notifier = get_notifier(channel)
    notifier.send(destination, message)

正常系(3チャネルへの通知)

send_notification(Channel.EMAIL, "user@example.com", "注文が確定しました")
send_notification(Channel.SLACK, "#orders", "新規注文 #ORD-123 が到着しました")
send_notification(Channel.SMS, "+819012345678", "発送しました")

出力(ログ)

INFO Email sent to user@example.com
INFO Slack message sent to #orders
INFO SMS sent to +819012345678

異常系(未登録チャネル)

send_notification("line", "U12345678", "テスト")
# → ValueError: Unknown channel: 'line'. Available: email, slack, sms

新チャネル追加(send_notification() は変更不要)

@dataclass(slots=True)
class LineNotifier:
    api_url: str
    def send(self, destination: str, message: str) -> None:
        # LINE Messaging API 呼び出し
        ...

# Channel enum に追加
class Channel(StrEnum):
    ...
    LINE = "line"  # ← 1行追加

# レジストリに登録するだけで send_notification() は変更不要
NOTIFIER_REGISTRY[Channel.LINE] = LineNotifier(api_url=LINE_API_URL)
ポイント適用した設計原則/パターン書籍対応章
Protocol による多態性Strategy Pattern / Duck TypingCh5 条件分岐の削減
StrEnum で型安全な識別子Value Object(型の活用)Ch2 型の活用
レジストリ dict で if-elif 廃止Registry PatternCh5 条件分岐の削減
各 Notifier が1チャネルのみ担当単一責任原則 (SRP)Ch6
新チャネル追加で既存コード変更不要Open-Closed Principle (OCP)Ch5/Ch7
dataclass(slots=True) で設定を値オブジェクト化Value Object + メモリ最適化Ch2/Ch3

問題 B: インフラ — K8s Native Sidecar × OTel Collector + Tail Sampling

ECサイトの注文 API(Python/FastAPI)が GKE Autopilot 上で稼働している。現状は各 Pod が直接 DataDog OTLP エンドポイントへトレースを送信しており、以下の課題がある:

現状の課題:
  • DataDog エンドポイントの URL がアプリコードにハードコードされている
  • サンプリングレートの変更に Pod 再デプロイが必要
  • 本番/ステージング環境でエクスポート先を切り替えられない
  • ヘルスチェックなど不要なトレースも全て DataDog に送信されコストが高い

要件

#要件
1K8s 1.28+ の Native Sidecar Containers(initContainers + restartPolicy: Always)を使うこと
2OTel Collector の設定を ConfigMap で管理すること
3アプリは OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 のみ知っていれば良い
4Tail Sampling: レイテンシ 200ms 超 または エラーのトレースのみ DataDog に送信すること
5本番/ステージングで exporter の向き先を Secret 経由の環境変数で切り替えられること

ヒント B(段階的開示)

ヒント1 — 方向性
OTel Collector をサイドカーとして配置するメリットは「アプリがバックエンドの実装詳細を知らなくて良い」こと。アプリは localhost:4317 に送るだけ。Collector 側で sampling / routing / retry を担う。Native Sidecar を使うことで「Collector が起動する前にアプリが接続しようとして失敗する」という競合状態を排除できる。
ヒント2 — アプローチ
  • K8s 1.28+ の Native Sidecar は initContainersrestartPolicy: Always を指定する
  • OTel Collector の config は ConfigMapvolumeMount でマウントし --config 引数で渡す
  • Tail Sampling Processor は全スパンを decision_wait: 10s バッファリングしてからポリシーで判断する
  • exporter の endpoint を環境変数 ${DATADOG_OTLP_ENDPOINT} で OTel Collector config に注入できる
ヒント3 — マニフェストの骨格
Native Sidecar の定義
initContainers:
  - name: otel-collector
    image: otel/opentelemetry-collector-contrib:0.100.0
    restartPolicy: Always   # ← Native Sidecar の肝
    args: ["--config=/conf/otel-config.yaml"]
    ports:
      - containerPort: 4317  # OTLP gRPC(Pod 内通信)
    volumeMounts:
      - name: otel-config
        mountPath: /conf
Tail Sampling 設定(ConfigMap)
tail_sampling:
  decision_wait: 10s
  num_traces: 50000
  policies:
    - name: errors-policy
      type: status_code
      status_code:
        status_codes: [ERROR]
    - name: slow-traces-policy
      type: latency
      latency:
        threshold_ms: 200

アーキテクチャ図 — Native Sidecar + OTel Collector + Tail Sampling

GKE Autopilot Pod App Container order-event-api (FastAPI) OTEL_EXPORTER_OTLP_ENDPOINT= http://localhost:4317 バックエンドを知らなくてよい OTLP gRPC OTel Collector (Native Sidecar) receiver: otlp (localhost:4317) Pod 内通信のみ受け付ける processor: tail_sampling decision_wait: 10s errors → PASS >200ms → PASS その他(正常・高頻度)→ DROP processor: batch (1000件 or 5s) exporter: otlp ${DATADOG_OTLP_ENDPOINT} (Secret 注入) restartPolicy: Always(起動順保証) ConfigMap otel-collector-config volumeMount → /conf/otel-config.yaml DataDog (OTLP) エラートレース P99 > 200ms トレース のみ到達(コスト最適化) エラー/低速のみ転送 DROP(転送しない) ヘルスチェック、正常・高頻度リクエスト DataDog コスト削減 OTLP gRPC(Pod 内) 転送(エラー/低速) DROP ConfigMap マウント

模範解答 B

# otel-collector-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-collector-config
  namespace: production
data:
  otel-config.yaml: |
    receivers:
      otlp:
        protocols:
          grpc:
            # Pod 内通信のみ受け付ける(外部からの接続不可)
            endpoint: localhost:4317

    processors:
      # ── Tail Sampling(エラー or 200ms 超のみ通す)──────────────
      tail_sampling:
        decision_wait: 10s      # スパンが揃うまで最大10秒待機してから判断
        num_traces: 50000       # メモリに保持するトレース数の上限
        policies:
          - name: errors-policy
            type: status_code
            status_code:
              status_codes: [ERROR]   # エラートレースは全件転送
          - name: slow-traces-policy
            type: latency
            latency:
              threshold_ms: 200       # 200ms 超のトレースは転送
          - name: health-check-drop
            type: string_attribute
            string_attribute:
              key: http.url
              values: ["/healthz", "/readyz"]
              invert_match: true      # /healthz, /readyz 以外を通す(ヘルスチェックを除外)

      # ── バッチ処理(DataDog への送信を効率化)────────────────────
      batch:
        send_batch_size: 1000
        timeout: 5s

    exporters:
      otlp:
        # ${DATADOG_OTLP_ENDPOINT} は Pod の env から注入(本番/ステージ切り替え)
        endpoint: "${DATADOG_OTLP_ENDPOINT}"
        headers:
          DD-API-KEY: "${DD_API_KEY}"
        retry_on_failure:
          enabled: true
          max_elapsed_time: 120s

    service:
      pipelines:
        traces:
          receivers:  [otlp]
          processors: [tail_sampling, batch]   # tail_sampling → batch の順が重要
          exporters:  [otlp]
# order-api-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-event-api
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: order-event-api
  template:
    metadata:
      labels:
        app: order-event-api
    spec:
      # ── Native Sidecar(K8s 1.28+)────────────────────────────────
      # initContainers + restartPolicy: Always で定義することで:
      # 1. メインコンテナより先に起動・Ready になることを K8s が保証
      # 2. メインコンテナが終了するまでサイドカーも継続稼働
      # 3. Job 利用時もサイドカーが自動終了しジョブを完了させられる
      initContainers:
        - name: otel-collector
          image: otel/opentelemetry-collector-contrib:0.100.0  # バージョン固定
          restartPolicy: Always   # ← これが Native Sidecar の肝
          args:
            - "--config=/conf/otel-config.yaml"
          env:
            # Secret から注入(本番: datadog-agent.prod:4317 / ステージ: datadog-agent.stg:4317)
            - name: DATADOG_OTLP_ENDPOINT
              valueFrom:
                secretKeyRef:
                  name: datadog-secrets
                  key: otlp_endpoint
            - name: DD_API_KEY
              valueFrom:
                secretKeyRef:
                  name: datadog-secrets
                  key: api_key
          ports:
            - name: otlp-grpc
              containerPort: 4317   # Pod 内通信のみ(hostPort は設定しない)
          volumeMounts:
            - name: otel-config
              mountPath: /conf
              readOnly: true
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "200m"
              memory: "256Mi"
          # ヘルスチェック(サイドカーが Ready になったことを確認)
          readinessProbe:
            httpGet:
              path: /
              port: 13133   # OTel Collector の health_check extension
            initialDelaySeconds: 3
            periodSeconds: 5

      # ── メインコンテナ ─────────────────────────────────────────────
      containers:
        - name: order-event-api
          image: "asia-northeast1-docker.pkg.dev/myproject/order-api/app@sha256:abc123"
          env:
            # サイドカー(localhost:4317)のみ知っていれば良い
            # DataDog エンドポイントはアプリコードに一切登場しない
            - name: OTEL_EXPORTER_OTLP_ENDPOINT
              value: "http://localhost:4317"
            - name: OTEL_SERVICE_NAME
              value: "order-event-api"
            - name: OTEL_RESOURCE_ATTRIBUTES
              value: "deployment.environment=production"
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "250m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"

      volumes:
        - name: otel-config
          configMap:
            name: otel-collector-config

Native Sidecar vs 従来サイドカーの比較

観点従来サイドカー(containers[])Native Sidecar(initContainers + restartPolicy: Always)
起動順序 保証なし(アプリと同時起動)
→ Collector 未起動でアプリが接続失敗する競合が起きる
メインコンテナより先に確実に起動・Ready になる
→ Collector が Ready になってからアプリが起動する
終了順序 不定 メインコンテナが終了してからサイドカーが終了
K8s Job との併用 サイドカーが終了せず Job が完了しない問題がある メインコンテナ完了後に自動終了 → Job が正常完了する
Argo Workflows との相性 ワークフローが終了しない問題が起きやすい 各ステップの Pod が正常に完了するため相性が良い
リソース課金(GKE Autopilot) Pod の requests 合計に含まれる 同上(Native Sidecar も Pod requests に含まれる)

Tail Sampling vs Head Sampling の比較

観点Head Sampling(確率的)Tail Sampling(ポリシーベース)
判断タイミングトレース開始時(1スパン目で決定)全スパンが揃ってから(decision_wait 後)
エラー取りこぼしあり(10%サンプリングなら90%のエラーを見逃す)なし(エラーポリシーで全件キャプチャ)
メモリ消費低い高い(全スパンをバッファリング)
コスト削減効果一律削減正常高頻度リクエストを選択的に間引ける
適用場面高スループット・メモリ制限ありSLO 監視・障害調査が重要な本番環境

ポイント解説

カテゴリ A

1 Protocol × StrEnum で if-elif を dict に置き換える(Ch5)
通知チャネルが増えるほど if-elif チェーンは長くなり、バグ混入リスクが上がる。Protocol でインターフェースを定め、dict[Channel, Notifier] で実装を登録することで、新チャネル追加時に send_notification() を一切変更しなくて良くなる(OCP)。
2 dataclass(slots=True) で設定を値オブジェクト化(Ch2/Ch3)
各 Notifier が持つ設定(API URL 等)を slots=True dataclass に閉じ込めることで、設定の変更が型チェッカーで検出できる。slots=True はメモリ効率も向上し、想定外の属性追加を防ぐ。
3 raise_for_status() + カスタム例外でエラーを呼び出し元へ伝播
print のみのエラー処理は「静かに失敗する」パターン。呼び出し元が失敗を検知できず、注文確定後にメール未送信という事態が起きる。専用例外 NotificationErrorraise することで Argo Workflows などの上位レイヤーが失敗を検知できる。

カテゴリ B

4 Native Sidecar Container の起動順序保証(K8s 1.28+)
従来のサイドカーでは「アプリが起動してすぐに localhost:4317 へ接続しようとするが、Collector がまだ起動していない」競合が起きた。initContainers + restartPolicy: Always(Native Sidecar)はメインコンテナより先に Ready になることが K8s によって保証される。
5 Tail Sampling でコストを選択的に削減
Head Sampling(確率的サンプリング)と異なり、Tail Sampling は全スパンを一定時間バッファリングしてからポリシーで判断する。エラーや遅延トレースを取りこぼさずに、ヘルスチェックや高頻度の正常リクエストを間引ける。decision_wait: 10s はレイテンシとメモリ消費のトレードオフで調整する。
6 ConfigMap + Secret で本番/ステージ差分を吸収
OTel Collector の exporter endpoint を Secret 経由の環境変数 ${DATADOG_OTLP_ENDPOINT} で注入することで、同じ ConfigMap/マニフェストを本番/ステージングで使い回せる。Kustomize の secretGenerator や Terraform の google_secret_manager_secret_version と組み合わせると管理が楽になる。

実務への応用

  • カテゴリ A: MOpsチームの施策通知(クーポン発行完了メール / Slack アラート / SMS)はまさにこのパターンが当てはまる。新しい通知チャネル(LINE, Push通知)が増えるたびに条件分岐が膨れ上がるのを防ぐために Protocol + レジストリへの移行を検討する価値がある。
  • カテゴリ B: Argo Workflows の各ステップ(Job)で OTel を使う際、従来サイドカーでは Job 完了後も Collector が終了しないため Argo がジョブをいつまでも Running と判断する問題があった。Native Sidecar によりこの問題が解消される。DataDog OTLP コストが高くなった場合の Tail Sampling 設定チューニングは E カテゴリのコスト管理にも直結する。

今日のまとめ

Protocol + dict レジストリで if-elif チェーンを根絶すると「新チャネル追加 = クラス1つ追加 + dict 登録のみ」になり OCP を満たせる。

インフラ側では K8s 1.28+ Native Sidecar で OTel Collector の起動順序を保証しつつ、Tail Sampling でエラー/低速トレースのみを DataDog に送信することで可観測性コストを大幅削減できる。アプリは localhost:4317 のみを知っていれば良く、バックエンド変更の影響をゼロにできる。

自己評価

自分の回答

気づき・メモ