Files
awoooi/docs/adr/ADR-121-otel-genai-semantic-conventions.md
Your Name 13e51802fe feat(awooop): Phase 0 全 ADR + Phase 1 control plane schema(含 critic 四項修正)
## Phase 0(文件層,全部 Accepted)
- ADR-106/107:AwoooP 平台架構 + 儲存策略
- ADR-111~118:Bootstrap → RLS 七項核心 ADR
- ADR-119~124:SAGA → Singleton Decomposition 六項 ADR
- ADR-UI-01~04:Operator Console 四個 UI ADR

## Phase 1(DB schema + migration)
- awooop_phase1_control_plane_2026-05-04.sql:7 張新表 + trigger + RLS
  - Step 1:三角色(platform_admin/migration BYPASSRLS,awooop_app 受 RLS)
  - Step 13:GRANT awooop_app 最小權限(7 條)
  - Step 14:RLS fail-closed,移除 __platform__ 後門
- awooop_phase1_batch1_rls_2026-05-04.sql:高流量四表三步式 ADD COLUMN
- awooop_phase1_batch1_backfill.py:SKIP LOCKED 分批回填腳本
- awooop_models.py:7 個 SQLAlchemy 2.x models

## Critic 修正(4 Critical + 3 Major)
- C-1:ADD CONSTRAINT IF NOT EXISTS → DO 塊 + pg_constraint 查詢
- C-2:__mapper_args__ 字串 list → primary_key=True on mapped_column
- C-3:__platform__ RLS 後門 → 全移除,改用 BYPASSRLS role
- C-4:awooop_app role 從未建立 → Step 1 + 7 條 GRANT
- M-1:active_pointer_guard SECURITY DEFINER(FORCE RLS 跨租戶保護)
- M-2:pg_partman create_parent 加冪等防護
- M-3:immutability trigger 新增身份欄位保護(project_id/family/contract_id)

## Task 1.2 修補
- agent_loader.py:硬編碼 Mac 路徑 → AGENTS_DIR 環境變數
- Dockerfile:補 COPY .claude/agents/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 13:37:11 +08:00

7.5 KiB
Raw Blame History

ADR-121: OTel GenAI Semantic Conventions

狀態Accepted 日期2026-05-03台北 決策者:統帥 範圍GenAI span 命名、token attribute、SignOz 上報、現有 telemetry.py 修補 關聯ADR-110GCP Ollama、ADR-120token budget、INV-4hardcoded IP


背景

OpenTelemetry 已發布 GenAI Semantic Conventions 1.02025 Q1定義了 AI/LLM 應用的標準 span 屬性。

現有 apps/api/src/services/telemetry.py 的問題:

  1. P0-08telemetry.py:71 hardcoded if "192.168.0.188" not in endpoint: raise(阻擋 EwoooC
  2. 不標準span 名稱和 attribute 不符合 GenAI Semantic Conventions
  3. token 追蹤缺失LLM call 的 prompt/completion token 沒有進 OTel span
  4. OTel 版本:需要 >=1.25.0 支援 GenAI semconvINV-5 確認目前 >=1.20.0

決策

D1 — GenAI Span 命名規範

標準格式GenAI semconv

{gen_ai.operation.name} {gen_ai.system}

例:
  chat ollama         ← Ollama chat 呼叫
  chat openai         ← OpenAI 呼叫(備援)
  chat gemini         ← Gemini 呼叫
  embeddings ollama   ← embedding 呼叫

禁止的命名方式

# 禁止(舊格式,不符合 semconv
"llm_call"
"openclaw_decision"
"ai_provider_call"

D2 — GenAI Span Attributes標準屬性

每個 LLM call span 必須包含以下 attributes

# 標準 GenAI attributesOpenTelemetry GenAI semconv 1.0
{
    # 系統識別
    "gen_ai.system": "ollama" | "openai" | "gemini" | "nvidia_nim",
    "gen_ai.operation.name": "chat" | "embeddings" | "completion",

    # 請求
    "gen_ai.request.model": "llama3.2",             # 請求的模型名稱
    "gen_ai.request.max_tokens": 4096,
    "gen_ai.request.temperature": 0.7,
    "gen_ai.request.top_p": 0.9,

    # 回應
    "gen_ai.response.model": "llama3.2",            # 實際使用的模型(可能因 fallback 不同)
    "gen_ai.response.finish_reasons": ["stop"],

    # Token 計數重要budget 對帳用)
    "gen_ai.usage.prompt_tokens": 512,
    "gen_ai.usage.completion_tokens": 128,

    # AwoooP 自訂 namespace非標準但必需
    "awooop.project_id": "awoooi",
    "awooop.agent_id": "openclaw-sre",
    "awooop.run_id": "uuid",
    "awooop.provider_tier": "primary" | "secondary" | "fallback",  # ADR-110 三層

    # W3C TraceContext由 OTel SDK 自動注入)
    # trace_id, span_id, parent_id
}

D3 — telemetry.py 修補P0-08 + semconv 升級)

P0-08 修補PR-01立即

# 修補前telemetry.py:71不安全
if "192.168.0.188" not in endpoint:
    raise ValueError(f"Unsupported OTEL endpoint: {endpoint}")

# 修補後(允許任何 HTTPS 或設定允許清單的 endpoint
ALLOWED_OTEL_ENDPOINTS = {
    settings.OTEL_EXPORTER_OTLP_ENDPOINT,   # 從環境變數讀取
}
# 移除 hardcode assert直接使用設定值

semconv 升級Phase 2

# apps/api/src/services/telemetry.py
from opentelemetry import trace
from opentelemetry.semconv._incubating.attributes import gen_ai_attributes

class GenAITracer:
    """符合 GenAI Semantic Conventions 1.0 的 LLM call tracer"""

    def __init__(self, tracer_name: str = "awooop.platform"):
        self._tracer = trace.get_tracer(tracer_name)

    def start_llm_span(
        self,
        system: str,         # "ollama", "openai", etc.
        operation: str,      # "chat", "embeddings"
        model: str,
        project_id: str,
        agent_id: str,
        run_id: str,
        provider_tier: str   # "primary", "secondary", "fallback"
    ) -> trace.Span:
        span_name = f"{operation} {system}"
        span = self._tracer.start_span(span_name)
        span.set_attributes({
            gen_ai_attributes.GEN_AI_SYSTEM: system,
            gen_ai_attributes.GEN_AI_OPERATION_NAME: operation,
            gen_ai_attributes.GEN_AI_REQUEST_MODEL: model,
            "awooop.project_id": project_id,
            "awooop.agent_id": agent_id,
            "awooop.run_id": run_id,
            "awooop.provider_tier": provider_tier,
        })
        return span

    def end_llm_span(
        self,
        span: trace.Span,
        response_model: str,
        prompt_tokens: int,
        completion_tokens: int,
        finish_reason: str
    ) -> None:
        span.set_attributes({
            gen_ai_attributes.GEN_AI_RESPONSE_MODEL: response_model,
            gen_ai_attributes.GEN_AI_USAGE_PROMPT_TOKENS: prompt_tokens,
            gen_ai_attributes.GEN_AI_USAGE_COMPLETION_TOKENS: completion_tokens,
            gen_ai_attributes.GEN_AI_RESPONSE_FINISH_REASONS: [finish_reason],
        })
        span.end()

D4 — OTel 依賴版本更新INV-5

# pyproject.toml 更新
opentelemetry-sdk = ">=1.25.0"                                    # GenAI semconv 支援
opentelemetry-exporter-otlp-proto-grpc = ">=1.25.0"
opentelemetry-semantic-conventions = ">=0.46b0"                   # incubating GenAI attrs

D5 — SignOz 上報設定188:24318

# apps/api/src/services/telemetry.py
# OTEL_EXPORTER_OTLP_ENDPOINT 從環境變數讀取(不再 hardcode
# K8s deployment 中設置:
#   OTEL_EXPORTER_OTLP_ENDPOINT=http://192.168.0.188:24318

# EwoooC 可設置不同的 endpointPR-01 修補後解除封鎖)

K8s ConfigMapper-tenant 可配置)

apiVersion: v1
kind: ConfigMap
metadata:
  name: awooop-telemetry-config
data:
  OTEL_EXPORTER_OTLP_ENDPOINT: "http://192.168.0.188:24318"
  OTEL_SERVICE_NAME: "awooop-platform"
  OTEL_RESOURCE_ATTRIBUTES: "deployment.environment=production"

D6 — GCP Ollama 三層拓撲的 span 標記ADR-110 整合)

# ADR-110 三層拓撲的 span 標記
PROVIDER_TIER_MAP = {
    "34.143.170.20": "primary",    # GCP-A
    "34.21.145.224": "secondary",  # GCP-B
    "192.168.0.111": "fallback",   # Local
}

def get_provider_tier(ollama_host: str) -> str:
    for ip, tier in PROVIDER_TIER_MAP.items():
        if ip in ollama_host:
            return tier
    return "unknown"

Fallback 事件primary → secondary 或 secondary → fallback必須在 span 上標記 awooop.provider_tier,讓 SignOz 可以分析 fallback 頻率。


後果

Benefits

  • GenAI semconv 標準化SignOz / Grafana 可用通用 GenAI dashboard
  • gen_ai.usage.prompt_tokens / completion_tokens 進 OTelbudget 對帳有 trace 依據
  • EwoooC 解封PR-01 移除 hardcode assert
  • GCP Ollama 三層 fallback 可視化

Costs

  • OTel 版本升級(>=1.25.0)需要更新 pyproject.toml + 驗證現有 instrumentation
  • 現有 LLM call 程式碼需要加 start_llm_span / end_llm_span 包裝

Risks

  • OTel GenAI semconv incubating 狀態not stable未來可能有 attribute 名稱變更
  • 緩解:awooop.* 自訂 namespace 隔離,即使 semconv 變更只需更新 GenAI attributes 部分

驗收標準

  • telemetry.py:71 hardcoded IP assert 移除PR-01 完成)
  • OTel 版本升級至 >=1.25.0Phase 2
  • LLM call span 包含 gen_ai.usage.prompt_tokens / completion_tokens(整合測試)
  • SignOz 可看到 chat ollama span 帶 token 計數Phase 2 驗收)
  • GCP Ollama fallback 事件在 SignOz 有 awooop.provider_tier 標記可查詢

關聯

  • ADR-110GCP Ollama 三層拓撲fallback 事件標記)
  • ADR-120token budgettelemetry token 計數對帳)
  • INV-4telemetry.py:71 hardcoded IPP0-08
  • INV-5OTel 版本相容性矩陣)