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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user