概要
Protocol + Registry(Ch5)
if-elif チェーンを Protocol インターフェース + dict レジストリに置き換え、新チャネル追加時に既存コード変更ゼロ(OCP)を実現する。
StrEnum × dataclass(slots=True)
チャネル識別子を StrEnum で型安全化し、各 Notifier の設定を slots=True dataclass で値オブジェクト化する(Ch2 型の活用)。
Native Sidecar(K8s 1.28+)
initContainers の restartPolicy: 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 — アプローチ
ProtocolでNotifierインターフェースを定義する(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
| # | 問題点 | 分類 | 改善方法 |
|---|---|---|---|
| 1 | ntype が str — タイポを検知できない | 型安全性 | StrEnum Channel で列挙型化 |
| 2 | if-elif チェーンで新チャネル追加時に関数を変更しなければならない | OCP 違反 Ch5 | Protocol + レジストリ dict に分離 |
| 3 | 型ヒントが皆無 | 型安全性 Ch2 | 全引数・戻り値に型ヒント付与 |
| 4 | print のみ — 呼び出し元が失敗を検知できない | エラー処理 | 専用例外 NotificationError を raise |
| 5 | API URL がハードコード | 設定管理 | 名前付き定数または設定クラスに分離 |
| 6 | requests.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 Typing | Ch5 条件分岐の削減 |
StrEnum で型安全な識別子 | Value Object(型の活用) | Ch2 型の活用 |
レジストリ dict で if-elif 廃止 | Registry Pattern | Ch5 条件分岐の削減 |
| 各 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 に送信されコストが高い
要件
| # | 要件 |
|---|---|
| 1 | K8s 1.28+ の Native Sidecar Containers(initContainers + restartPolicy: Always)を使うこと |
| 2 | OTel Collector の設定を ConfigMap で管理すること |
| 3 | アプリは OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 のみ知っていれば良い |
| 4 | Tail 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 は
initContainersにrestartPolicy: Alwaysを指定する - OTel Collector の config は
ConfigMap→volumeMountでマウントし--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
模範解答 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
各 Notifier が持つ設定(API URL 等)を
dataclass(slots=True) で設定を値オブジェクト化(Ch2/Ch3)各 Notifier が持つ設定(API URL 等)を
slots=True dataclass に閉じ込めることで、設定の変更が型チェッカーで検出できる。slots=True はメモリ効率も向上し、想定外の属性追加を防ぐ。
3
raise_for_status() + カスタム例外でエラーを呼び出し元へ伝播print のみのエラー処理は「静かに失敗する」パターン。呼び出し元が失敗を検知できず、注文確定後にメール未送信という事態が起きる。専用例外 NotificationError を raise することで 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 は全スパンを一定時間バッファリングしてからポリシーで判断する。エラーや遅延トレースを取りこぼさずに、ヘルスチェックや高頻度の正常リクエストを間引ける。
Head Sampling(確率的サンプリング)と異なり、Tail Sampling は全スパンを一定時間バッファリングしてからポリシーで判断する。エラーや遅延トレースを取りこぼさずに、ヘルスチェックや高頻度の正常リクエストを間引ける。
decision_wait: 10s はレイテンシとメモリ消費のトレードオフで調整する。
6
ConfigMap + Secret で本番/ステージ差分を吸収
OTel Collector の exporter endpoint を 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 のみを知っていれば良く、バックエンド変更の影響をゼロにできる。