""" 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"