feat(api): Phase 13 智能路由 + CI/CD 整合 (#74-88)

Phase 13.1 CI/CD Integration:
- #76 workflow_run handler for CI failure diagnosis
- #77 SignOz log query (query_logs, error_logs_summary MCP)
- #78 CIAutoRepairService with risk-based execution decisions

Phase 13.3 Smart Routing:
- #85 Intent Classifier v2.0 (rule engine + LLM fallback)
- #86 Complexity Scorer (9-dimension scoring)
- #87 AI Router v3.0 (routing decision matrix)
- #88 Token Counter (OTEL + Langfuse integration)

New files:
- services/ci_auto_repair.py (risk stratification)
- services/model_registry.py (centralized model config)
- services/token_counter.py (677 lines)
- Skill 08: Model Router Expert
- Skill 09: Strangler Pattern Expert
- ADR-023: Smart Routing Architecture
- ADR-024: API Layer Architecture

Tests:
- phase11-conversational.spec.ts (E2E tests)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-26 15:32:52 +08:00
parent b79e5f1a1a
commit 579da38b8b
15 changed files with 5895 additions and 245 deletions

View File

@@ -418,6 +418,140 @@ class SignOzClient:
},
}
# =========================================================================
# Log Query (Phase 13.1 #77)
# =========================================================================
async def get_logs(
self,
service_name: str | None = None,
severity: str | None = None,
search_text: str | None = None,
time_window_minutes: int = 30,
limit: int = 100,
) -> list[dict]:
"""
從 SignOz/ClickHouse 查詢日誌 (Phase 13.1 #77)
SignOz 日誌儲存在 signoz_logs.distributed_logs 表
Schema: timestamp, severity_text, body, resources, attributes
Args:
service_name: 服務名稱 (過濾 resources.service.name)
severity: 日誌級別 (ERROR, WARN, INFO, DEBUG)
search_text: 日誌內容搜尋文字
time_window_minutes: 時間窗口 (分鐘)
limit: 返回筆數上限
Returns:
list[dict]: 日誌記錄列表
"""
now = datetime.now(UTC)
start_ns = int((now - timedelta(minutes=time_window_minutes)).timestamp() * 1_000_000_000)
end_ns = int(now.timestamp() * 1_000_000_000)
# 構建 WHERE 條件
conditions = [
f"timestamp >= {start_ns}",
f"timestamp <= {end_ns}",
]
if service_name:
# SignOz 儲存 service.name 在 resources 欄位
conditions.append(f"resources['service.name'] = '{service_name}'")
if severity:
# 支援多個級別 (如 'ERROR,WARN')
severities = [s.strip().upper() for s in severity.split(",")]
severity_list = ", ".join([f"'{s}'" for s in severities])
conditions.append(f"severity_text IN ({severity_list})")
if search_text:
# 日誌內容搜尋 (避免 SQL injection)
safe_text = search_text.replace("'", "''")
conditions.append(f"body LIKE '%{safe_text}%'")
where_clause = " AND ".join(conditions)
query = f"""
SELECT
timestamp,
severity_text,
body,
resources,
attributes,
trace_id,
span_id
FROM signoz_logs.distributed_logs
WHERE {where_clause}
ORDER BY timestamp DESC
LIMIT {limit}
"""
results = await self._query_clickhouse(query)
# 格式化結果
formatted_logs = []
for row in results:
formatted_logs.append({
"timestamp": row.get("timestamp"),
"severity": row.get("severity_text", "UNKNOWN"),
"message": row.get("body", ""),
"service": row.get("resources", {}).get("service.name", "unknown"),
"trace_id": row.get("trace_id", ""),
"span_id": row.get("span_id", ""),
"attributes": row.get("attributes", {}),
})
logger.info(
"signoz_logs_query_completed",
service_name=service_name,
severity=severity,
result_count=len(formatted_logs),
time_window_minutes=time_window_minutes,
)
return formatted_logs
async def get_error_logs_summary(
self,
service_name: str,
time_window_minutes: int = 60,
) -> dict:
"""
取得錯誤日誌摘要 (Phase 13.1 #77 - CI 診斷用)
統計各類錯誤的出現次數和代表性訊息
"""
now = datetime.now(UTC)
start_ns = int((now - timedelta(minutes=time_window_minutes)).timestamp() * 1_000_000_000)
end_ns = int(now.timestamp() * 1_000_000_000)
query = f"""
SELECT
severity_text,
count() as count,
any(body) as sample_message
FROM signoz_logs.distributed_logs
WHERE
timestamp >= {start_ns}
AND timestamp <= {end_ns}
AND resources['service.name'] = '{service_name}'
AND severity_text IN ('ERROR', 'FATAL', 'CRITICAL')
GROUP BY severity_text
ORDER BY count DESC
LIMIT 10
"""
results = await self._query_clickhouse(query)
return {
"service_name": service_name,
"time_window_minutes": time_window_minutes,
"error_summary": results,
"total_errors": sum(r.get("count", 0) for r in results),
}
# =============================================================================
# Singleton