Files
aiagent/backend/app/api/system_logs.py
renjianbo beff3fac8d fix: delete agent 500 error + dynamic personality + deployment guide
- Fix delete agent 500: clean up FK records (agent_llm_logs, permissions,
  schedules, executions, team_members) and unbind goals/tasks before delete
- Remove hardcoded personality templates in Android, replace with dynamic
  system prompt generation from name + description
- Set promptSectionsEnabled=false to bypass PromptComposer for personality
- Add Tencent Cloud Linux deployment guide (Docker Compose)
- Accumulated backend service updates, frontend UI fixes, Android app changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-29 01:17:21 +08:00

351 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
系统日志统一查询 API
"""
import logging
from pathlib import Path
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
from fastapi import APIRouter, Depends, Query, HTTPException, status
from sqlalchemy.orm import Session
from sqlalchemy import func, text, case
from pydantic import BaseModel
from app.core.database import get_db
from app.core.config import settings
from app.api.auth import get_current_user
from app.models.user import User
from app.models.execution_log import ExecutionLog
from app.models.agent_execution_log import AgentExecutionLog
from app.models.agent_llm_log import AgentLLMLog
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/api/v1/system-logs",
tags=["system-logs"],
responses={
401: {"description": "未授权"},
403: {"description": "无权访问"},
500: {"description": "服务器内部错误"}
}
)
# ── Pydantic Schemas ─────────────────────────────────────────────
class UnifiedLogItem(BaseModel):
id: str
source: str # "execution" | "agent" | "llm"
level: Optional[str] = None
message: str
timestamp: Optional[datetime] = None
resource_type: Optional[str] = None
resource_id: Optional[str] = None
duration_ms: Optional[int] = None
username: Optional[str] = None
class Config:
from_attributes = True
class LogStatsResponse(BaseModel):
total_count: int
error_count: int
warn_count: int
info_count: int
source_breakdown: Dict[str, int] # {"execution": 123, "agent": 45, "llm": 67}
hourly_trend: List[Dict[str, Any]] # [{"hour": "2026-05-10T14", "count": 5}, ...]
class Config:
from_attributes = True
class AppLogItem(BaseModel):
line_number: int
content: str
# ── Helpers ──────────────────────────────────────────────────────
def _build_union_query(
db: Session,
source: Optional[str] = None,
level: Optional[str] = None,
keyword: Optional[str] = None,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
user_id: Optional[str] = None,
):
"""构建跨表 UNION ALL 查询"""
queries: list = []
params: dict = {}
# 1) 工作流执行日志
if source in (None, "all", "execution"):
q = db.query(
ExecutionLog.id.label("id"),
text("'execution'").label("source"),
ExecutionLog.level.label("level"),
ExecutionLog.message.label("message"),
ExecutionLog.timestamp.label("timestamp"),
ExecutionLog.node_type.label("resource_type"),
ExecutionLog.execution_id.label("resource_id"),
ExecutionLog.duration.label("duration_ms"),
text("NULL").label("username"),
)
if level:
q = q.filter(ExecutionLog.level == level.upper())
if keyword:
q = q.filter(ExecutionLog.message.contains(keyword))
if start_date:
q = q.filter(ExecutionLog.timestamp >= start_date)
if end_date:
q = q.filter(ExecutionLog.timestamp <= end_date)
queries.append(q)
# 2) Agent 执行日志
if source in (None, "all", "agent"):
q = db.query(
AgentExecutionLog.id.label("id"),
text("'agent'").label("source"),
case(
(AgentExecutionLog.status == "failed", "ERROR"),
(AgentExecutionLog.status == "completed", "INFO"),
else_="INFO"
).label("level"),
AgentExecutionLog.user_message.label("message"),
AgentExecutionLog.created_at.label("timestamp"),
text("'agent_chat'").label("resource_type"),
AgentExecutionLog.agent_id.label("resource_id"),
AgentExecutionLog.total_latency_ms.label("duration_ms"),
text("NULL").label("username"),
)
if level:
if level.upper() == "ERROR":
q = q.filter(AgentExecutionLog.status == "failed")
elif level.upper() == "WARN":
q = q.filter(AgentExecutionLog.status == "failed")
else:
q = q.filter(AgentExecutionLog.status != "failed")
if keyword:
q = q.filter(AgentExecutionLog.user_message.contains(keyword))
if start_date:
q = q.filter(AgentExecutionLog.created_at >= start_date)
if end_date:
q = q.filter(AgentExecutionLog.created_at <= end_date)
if user_id:
q = q.filter(AgentExecutionLog.user_id == user_id)
queries.append(q)
# 3) LLM 调用日志
if source in (None, "all", "llm"):
q = db.query(
AgentLLMLog.id.label("id"),
text("'llm'").label("source"),
case(
(AgentLLMLog.status == "error", "ERROR"),
(AgentLLMLog.status == "rate_limited", "WARN"),
else_="INFO"
).label("level"),
AgentLLMLog.assistant_content.label("message"),
AgentLLMLog.created_at.label("timestamp"),
text("'llm_call'").label("resource_type"),
AgentLLMLog.agent_id.label("resource_id"),
AgentLLMLog.latency_ms.label("duration_ms"),
text("NULL").label("username"),
)
if level:
if level.upper() == "ERROR":
q = q.filter(AgentLLMLog.status == "error")
elif level.upper() == "WARN":
q = q.filter(AgentLLMLog.status == "rate_limited")
else:
q = q.filter(AgentLLMLog.status == "success")
if keyword:
q = q.filter(AgentLLMLog.assistant_content.contains(keyword))
if start_date:
q = q.filter(AgentLLMLog.created_at >= start_date)
if end_date:
q = q.filter(AgentLLMLog.created_at <= end_date)
queries.append(q)
return queries
def _check_admin(current_user: User):
if getattr(current_user, "role", None) != "admin":
from app.core.exceptions import ForbiddenError
raise ForbiddenError("仅管理员可访问系统日志")
# ── Endpoints ────────────────────────────────────────────────────
@router.get("", response_model=List[UnifiedLogItem])
async def get_system_logs(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
source: Optional[str] = Query(None, description="日志来源: execution/agent/llm/all"),
level: Optional[str] = Query(None, description="日志级别: INFO/WARN/ERROR"),
keyword: Optional[str] = Query(None, description="关键词搜索"),
start_date: Optional[str] = Query(None, description="开始时间 ISO格式"),
end_date: Optional[str] = Query(None, description="结束时间 ISO格式"),
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=1000),
):
"""
统一日志查询:跨 execution_logs / agent_execution_logs / agent_llm_logs 联合查询。
管理员可查全部,普通用户只能查看自己相关的 Agent 执行日志和 LLM 日志。
"""
_check_admin(current_user)
# 解析时间
sd = datetime.fromisoformat(start_date) if start_date else None
ed = datetime.fromisoformat(end_date) if end_date else None
# 非 admin 限制用户范围
user_id = None
if getattr(current_user, "role", None) != "admin":
user_id = current_user.id
source_val = source if source else "all"
queries = _build_union_query(db, source_val, level, keyword, sd, ed, user_id)
if not queries:
return []
# UNION ALL
union = queries[0]
for q in queries[1:]:
union = union.union_all(q)
# 排序 + 分页
total = union.count() if hasattr(union, 'count') else 0
rows = union.order_by(text("timestamp DESC")).offset(skip).limit(limit).all()
return rows
@router.get("/stats", response_model=LogStatsResponse)
async def get_system_logs_stats(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""获取系统日志统计各级别计数、各来源计数、24小时趋势"""
_check_admin(current_user)
now = datetime.utcnow()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
hours_24 = now - timedelta(hours=24)
# 工作流执行日志统计
exec_total = db.query(func.count(ExecutionLog.id)).filter(
ExecutionLog.timestamp >= today_start
).scalar() or 0
exec_errors = db.query(func.count(ExecutionLog.id)).filter(
ExecutionLog.timestamp >= today_start,
ExecutionLog.level == "ERROR"
).scalar() or 0
exec_warns = db.query(func.count(ExecutionLog.id)).filter(
ExecutionLog.timestamp >= today_start,
ExecutionLog.level == "WARN"
).scalar() or 0
# Agent 执行日志统计
agent_total = db.query(func.count(AgentExecutionLog.id)).filter(
AgentExecutionLog.created_at >= today_start
).scalar() or 0
agent_errors = db.query(func.count(AgentExecutionLog.id)).filter(
AgentExecutionLog.created_at >= today_start,
AgentExecutionLog.status == "failed"
).scalar() or 0
# LLM 调用日志统计
llm_total = db.query(func.count(AgentLLMLog.id)).filter(
AgentLLMLog.created_at >= today_start
).scalar() or 0
llm_errors = db.query(func.count(AgentLLMLog.id)).filter(
AgentLLMLog.created_at >= today_start,
AgentLLMLog.status == "error"
).scalar() or 0
total_count = exec_total + agent_total + llm_total
error_count = exec_errors + agent_errors + llm_errors
warn_count = exec_warns
info_count = total_count - error_count - warn_count
# 24小时趋势按小时聚合
hourly: list[dict[str, Any]] = []
for h in range(23, -1, -1):
slot_start = now.replace(minute=0, second=0, microsecond=0) - timedelta(hours=h)
slot_end = slot_start + timedelta(hours=1)
exec_h = db.query(func.count(ExecutionLog.id)).filter(
ExecutionLog.timestamp >= slot_start,
ExecutionLog.timestamp < slot_end
).scalar() or 0
agent_h = db.query(func.count(AgentExecutionLog.id)).filter(
AgentExecutionLog.created_at >= slot_start,
AgentExecutionLog.created_at < slot_end
).scalar() or 0
llm_h = db.query(func.count(AgentLLMLog.id)).filter(
AgentLLMLog.created_at >= slot_start,
AgentLLMLog.created_at < slot_end
).scalar() or 0
hourly.append({
"hour": slot_start.isoformat(),
"count": exec_h + agent_h + llm_h
})
return {
"total_count": total_count,
"error_count": error_count,
"warn_count": warn_count,
"info_count": info_count,
"source_breakdown": {
"execution": exec_total,
"agent": agent_total,
"llm": llm_total
},
"hourly_trend": hourly
}
@router.get("/app-logs", response_model=List[AppLogItem])
async def get_app_logs(
current_user: User = Depends(get_current_user),
lines: int = Query(200, ge=10, le=2000, description="读取行数"),
level: Optional[str] = Query(None, description="按级别过滤: INFO/WARNING/ERROR"),
):
"""读取应用程序文件日志的尾部行"""
_check_admin(current_user)
log_file = Path(settings.LOG_DIR) / "app.log"
if not log_file.exists():
return []
level_filter = level.upper() if level else None
try:
with open(log_file, "r", encoding="utf-8", errors="replace") as f:
all_lines = f.readlines()
except Exception:
return []
# 取尾部 N 行,倒序输出
tail = all_lines[-lines:]
result: list[dict] = []
for i, raw in enumerate(tail):
content = raw.rstrip("\n").rstrip("\r")
if level_filter and level_filter not in content.upper():
continue
result.append({
"line_number": len(all_lines) - len(tail) + i + 1,
"content": content
})
return result