實現 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>
228 lines
6.8 KiB
Python
228 lines
6.8 KiB
Python
"""
|
|
AWOOOI Deep Linking - Phase 15.3
|
|
================================
|
|
三系統觀測互連: Sentry ↔ SignOz ↔ Langfuse
|
|
|
|
Phase 15.3 (2026-03-26)
|
|
實現零斷鏈觀測的最後一哩路
|
|
|
|
架構:
|
|
┌─────────┐ trace_id ┌─────────┐ trace_id ┌──────────┐
|
|
│ Sentry │ ◄──────────────► │ SignOz │ ◄──────────────► │ Langfuse │
|
|
│ Errors │ │ Traces │ │ LLMOps │
|
|
└─────────┘ └─────────┘ └──────────┘
|
|
|
|
URL 格式:
|
|
- SignOz Trace: http://192.168.0.188:3301/trace/{trace_id}
|
|
- Langfuse Trace: http://192.168.0.110:3100/trace/{langfuse_trace_id}
|
|
- Sentry Issue: http://192.168.0.110:9000/organizations/sentry/issues/{issue_id}/
|
|
|
|
Usage:
|
|
from src.core.deep_linking import DeepLinking
|
|
|
|
# 取得 SignOz Trace URL
|
|
url = DeepLinking.signoz_trace_url(trace_id)
|
|
|
|
# 取得 Langfuse Trace URL
|
|
url = DeepLinking.langfuse_trace_url(langfuse_trace_id)
|
|
"""
|
|
|
|
from src.core.config import settings
|
|
|
|
|
|
class DeepLinking:
|
|
"""
|
|
三系統 Deep Linking URL 生成器
|
|
|
|
統帥鐵律 (Phase 15.3):
|
|
- 所有觀測系統必須能互相跳轉
|
|
- URL 必須使用內網 IP (非 localhost)
|
|
- 確保 trace_id 格式一致 (32 hex chars)
|
|
"""
|
|
|
|
# ==========================================================================
|
|
# SignOz URLs (Traces/Metrics/Logs)
|
|
# ==========================================================================
|
|
|
|
SIGNOZ_BASE_URL = "http://192.168.0.188:3301"
|
|
|
|
@classmethod
|
|
def signoz_trace_url(cls, trace_id: str) -> str:
|
|
"""
|
|
生成 SignOz Trace 詳情頁 URL
|
|
|
|
Args:
|
|
trace_id: 32 字元 hex 格式 (e.g., "0af7651916cd43dd8448eb211c80319c")
|
|
|
|
Returns:
|
|
SignOz Trace URL (e.g., http://192.168.0.188:3301/trace/0af7651916cd43dd8448eb211c80319c)
|
|
"""
|
|
if not trace_id:
|
|
return ""
|
|
# SignOz v3 URL 格式
|
|
return f"{cls.SIGNOZ_BASE_URL}/trace/{trace_id}"
|
|
|
|
@classmethod
|
|
def signoz_service_url(cls, service_name: str = "awoooi-api") -> str:
|
|
"""
|
|
生成 SignOz Service 監控頁 URL
|
|
|
|
Args:
|
|
service_name: 服務名稱
|
|
|
|
Returns:
|
|
SignOz Service URL
|
|
"""
|
|
return f"{cls.SIGNOZ_BASE_URL}/services/{service_name}"
|
|
|
|
@classmethod
|
|
def signoz_logs_url(cls, trace_id: str | None = None) -> str:
|
|
"""
|
|
生成 SignOz Logs 頁面 URL
|
|
|
|
Args:
|
|
trace_id: 可選,用於過濾特定 trace 的 logs
|
|
|
|
Returns:
|
|
SignOz Logs URL
|
|
"""
|
|
if trace_id:
|
|
# SignOz v3 logs 過濾語法
|
|
return f"{cls.SIGNOZ_BASE_URL}/logs?q=trace_id%3D{trace_id}"
|
|
return f"{cls.SIGNOZ_BASE_URL}/logs"
|
|
|
|
# ==========================================================================
|
|
# Langfuse URLs (LLMOps)
|
|
# ==========================================================================
|
|
|
|
LANGFUSE_BASE_URL = settings.LANGFUSE_URL or "http://192.168.0.110:3100"
|
|
|
|
@classmethod
|
|
def langfuse_trace_url(cls, trace_id: str, project: str = "awoooi-openclaw") -> str:
|
|
"""
|
|
生成 Langfuse Trace 詳情頁 URL
|
|
|
|
Args:
|
|
trace_id: Langfuse trace ID
|
|
project: Langfuse 專案名稱
|
|
|
|
Returns:
|
|
Langfuse Trace URL
|
|
"""
|
|
if not trace_id:
|
|
return ""
|
|
return f"{cls.LANGFUSE_BASE_URL}/project/{project}/traces/{trace_id}"
|
|
|
|
@classmethod
|
|
def langfuse_dashboard_url(cls, project: str = "awoooi-openclaw") -> str:
|
|
"""
|
|
生成 Langfuse Dashboard URL
|
|
|
|
Args:
|
|
project: Langfuse 專案名稱
|
|
|
|
Returns:
|
|
Langfuse Dashboard URL
|
|
"""
|
|
return f"{cls.LANGFUSE_BASE_URL}/project/{project}"
|
|
|
|
# ==========================================================================
|
|
# Sentry URLs (Error Tracking)
|
|
# ==========================================================================
|
|
|
|
SENTRY_BASE_URL = "http://192.168.0.110:9000"
|
|
SENTRY_ORG = "sentry" # Self-hosted 預設組織
|
|
|
|
@classmethod
|
|
def sentry_issue_url(cls, issue_id: str) -> str:
|
|
"""
|
|
生成 Sentry Issue 詳情頁 URL
|
|
|
|
Args:
|
|
issue_id: Sentry issue ID
|
|
|
|
Returns:
|
|
Sentry Issue URL
|
|
"""
|
|
if not issue_id:
|
|
return ""
|
|
return f"{cls.SENTRY_BASE_URL}/organizations/{cls.SENTRY_ORG}/issues/{issue_id}/"
|
|
|
|
@classmethod
|
|
def sentry_project_url(cls, project_slug: str) -> str:
|
|
"""
|
|
生成 Sentry Project 頁面 URL
|
|
|
|
Args:
|
|
project_slug: Sentry 專案 slug
|
|
|
|
Returns:
|
|
Sentry Project URL
|
|
"""
|
|
return f"{cls.SENTRY_BASE_URL}/organizations/{cls.SENTRY_ORG}/projects/{project_slug}/"
|
|
|
|
# ==========================================================================
|
|
# Cross-System Deep Linking
|
|
# ==========================================================================
|
|
|
|
@classmethod
|
|
def get_all_links(
|
|
cls,
|
|
otel_trace_id: str | None = None,
|
|
langfuse_trace_id: str | None = None,
|
|
sentry_issue_id: str | None = None,
|
|
) -> dict[str, str]:
|
|
"""
|
|
取得所有可用的 Deep Links
|
|
|
|
Args:
|
|
otel_trace_id: OpenTelemetry trace ID (32 hex)
|
|
langfuse_trace_id: Langfuse trace ID
|
|
sentry_issue_id: Sentry issue ID
|
|
|
|
Returns:
|
|
dict with available deep links
|
|
|
|
Example:
|
|
{
|
|
"signoz_trace": "http://...",
|
|
"langfuse_trace": "http://...",
|
|
"sentry_issue": "http://...",
|
|
}
|
|
"""
|
|
links = {}
|
|
|
|
if otel_trace_id:
|
|
links["signoz_trace"] = cls.signoz_trace_url(otel_trace_id)
|
|
links["signoz_logs"] = cls.signoz_logs_url(otel_trace_id)
|
|
|
|
if langfuse_trace_id:
|
|
links["langfuse_trace"] = cls.langfuse_trace_url(langfuse_trace_id)
|
|
|
|
if sentry_issue_id:
|
|
links["sentry_issue"] = cls.sentry_issue_url(sentry_issue_id)
|
|
|
|
return links
|
|
|
|
@classmethod
|
|
def format_for_log(
|
|
cls,
|
|
otel_trace_id: str | None = None,
|
|
langfuse_trace_id: str | None = None,
|
|
) -> str:
|
|
"""
|
|
格式化 Deep Links 供 log 輸出
|
|
|
|
Returns:
|
|
Formatted string for logging
|
|
"""
|
|
parts = []
|
|
|
|
if otel_trace_id:
|
|
parts.append(f"SignOz: {cls.signoz_trace_url(otel_trace_id)}")
|
|
|
|
if langfuse_trace_id:
|
|
parts.append(f"Langfuse: {cls.langfuse_trace_url(langfuse_trace_id)}")
|
|
|
|
return " | ".join(parts) if parts else "No trace links available"
|