feat(api): Phase 15.3 Deep Linking 三系統互連

實現 Sentry ↔ SignOz ↔ Langfuse 零斷鏈觀測:

新增 deep_linking.py:
- SignOz Trace URL 生成器
- Langfuse Trace URL 生成器
- Sentry Issue URL 生成器
- get_all_links() 統一取得所有連結

整合點:
- main.py: Sentry before_send 注入 otel_trace_id + signoz_trace_url
- langfuse_client.py: 自動注入 OTEL trace_id 到 metadata
- openclaw.py: SignOz span 記錄 langfuse.trace_id 反向連結

架構圖:
┌─────────┐ trace_id ┌─────────┐ trace_id ┌──────────┐
│ Sentry  │◄────────►│ SignOz  │◄────────►│ Langfuse │
│ Errors  │          │ Traces  │          │ LLMOps   │
└─────────┘          └─────────┘          └──────────┘

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-26 00:48:28 +08:00
parent 0d31ccb911
commit b6cff31653
4 changed files with 333 additions and 8 deletions

View File

@@ -1,21 +1,23 @@
"""
Langfuse LLMOps Client - Phase 15.1
===================================
Langfuse LLMOps Client - Phase 15.1 + 15.3
==========================================
LLM 呼叫追蹤、成本監控、Prompt 版本管理
Phase 15.1 (2026-03-26)
Phase 15.1 (2026-03-26): 基礎整合
Phase 15.3 (2026-03-26): Deep Linking (Langfuse ↔ SignOz)
端點: http://192.168.0.110:3100 (DevOps 金庫)
Features:
- 自動追蹤所有 LLM 呼叫 (Ollama/Gemini/Claude)
- 成本估算與監控
- Prompt 版本管理
- 與 OTEL Trace 整合
- **Phase 15.3**: OTEL Trace 整合 (SignOz Deep Link)
Usage:
from src.services.langfuse_client import get_langfuse, langfuse_trace
# 方法 1: Context Manager
# 方法 1: Context Manager (自動整合 OTEL trace_id)
async with langfuse_trace("openclaw_decision") as trace:
result = await call_llm(prompt)
trace.generation(
@@ -24,6 +26,7 @@ Usage:
input=prompt,
output=result,
)
# trace.metadata 自動包含 signoz_trace_url
# 方法 2: 裝飾器
@langfuse_observe(name="analyze_incident")
@@ -88,21 +91,53 @@ def get_langfuse():
class LangfuseTraceContext:
"""Langfuse Trace Context for tracking LLM calls"""
"""
Langfuse Trace Context for tracking LLM calls
Phase 15.3: 自動整合 OTEL trace_id實現 Langfuse ↔ SignOz Deep Linking
"""
def __init__(self, name: str, metadata: dict[str, Any] | None = None):
self.name = name
self.metadata = metadata or {}
self.trace = None
self._client = get_langfuse()
self._otel_trace_id: str | None = None
self._langfuse_trace_id: str | None = None
def __enter__(self):
if self._client:
try:
# Phase 15.3: 取得當前 OTEL trace_id 並注入 metadata
from src.core.deep_linking import DeepLinking
from src.core.telemetry import get_current_trace_id
self._otel_trace_id = get_current_trace_id()
# 建立含 SignOz Deep Link 的 metadata
enriched_metadata = {**self.metadata}
if self._otel_trace_id:
enriched_metadata["otel_trace_id"] = self._otel_trace_id
enriched_metadata["signoz_trace_url"] = DeepLinking.signoz_trace_url(
self._otel_trace_id
)
self.trace = self._client.trace(
name=self.name,
metadata=self.metadata,
metadata=enriched_metadata,
)
# 記錄 Langfuse trace_id 供反向連結
if self.trace:
self._langfuse_trace_id = self.trace.id
logger.debug(
"langfuse_trace_started",
name=self.name,
otel_trace_id=self._otel_trace_id,
langfuse_trace_id=self._langfuse_trace_id,
)
except Exception as e:
logger.warning("langfuse_trace_start_failed", error=str(e))
return self
@@ -111,6 +146,16 @@ class LangfuseTraceContext:
# Langfuse auto-flushes, no explicit close needed
pass
@property
def otel_trace_id(self) -> str | None:
"""取得關聯的 OTEL trace_id"""
return self._otel_trace_id
@property
def langfuse_trace_id(self) -> str | None:
"""取得 Langfuse trace_id"""
return self._langfuse_trace_id
def generation(
self,
name: str,

View File

@@ -832,7 +832,7 @@ class OpenClawService:
logger.info("mock_mode_enabled", using="mock_llm")
return self._generate_mock_response(alert_context or {}, signoz_metrics), "mock", True
# Phase 15.1: Langfuse 追蹤整合
# Phase 15.1 + 15.3: Langfuse 追蹤整合 + SignOz Deep Linking
with langfuse_trace(
"openclaw_fallback_chain",
metadata={
@@ -841,6 +841,21 @@ class OpenClawService:
"alert_fingerprint": (alert_context or {}).get("fingerprint", "unknown"),
},
) as trace:
# Phase 15.3: SignOz → Langfuse 反向連結
# 在當前 OTEL span 中記錄 Langfuse trace_id
if trace.langfuse_trace_id:
from opentelemetry import trace as otel_trace
from src.core.deep_linking import DeepLinking
current_span = otel_trace.get_current_span()
if current_span:
current_span.set_attribute("langfuse.trace_id", trace.langfuse_trace_id)
current_span.set_attribute(
"langfuse.trace_url",
DeepLinking.langfuse_trace_url(trace.langfuse_trace_id),
)
for provider in settings.AI_FALLBACK_ORDER:
logger.info("ai_provider_attempt", provider=provider)