astock-agent/backend/app/api/debug.py
2026-04-22 22:44:48 +08:00

149 lines
4.7 KiB
Python

"""Debug API — 系统日志与运行状态"""
import os
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends
from sqlalchemy import text
from app.core.deps import get_current_admin
from app.db.database import get_db
from app.config import settings, is_trading_hours
router = APIRouter(prefix="/api/debug", tags=["debug"])
@router.get("/errors")
async def get_errors(
limit: int = 50,
source: str = None,
level: str = None,
days: int = 7,
_admin: dict = Depends(get_current_admin),
):
"""获取错误日志(管理员)"""
start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
async with get_db() as db:
conditions = ["created_at >= :start"]
params = {"start": start}
if source:
conditions.append("source = :source")
params["source"] = source
if level:
conditions.append("level = :level")
params["level"] = level
where = " AND " + " AND ".join(conditions)
# 总数
count_result = await db.execute(
text(f"SELECT COUNT(*) FROM error_logs WHERE {where}"), params
)
total = count_result.scalar() or 0
# 查询
params["limit"] = limit
result = await db.execute(
text(
f"SELECT id, source, level, message, detail, created_at "
f"FROM error_logs WHERE {where} "
f"ORDER BY created_at DESC LIMIT :limit"
),
params,
)
rows = result.fetchall()
errors = []
for row in rows:
r = row._mapping
errors.append({
"id": r["id"],
"source": r["source"],
"level": r["level"],
"message": r["message"],
"detail": r["detail"] or "",
"created_at": str(r["created_at"]) if r["created_at"] else "",
})
# 可选的 source/level 列表(用于前端过滤)
sources_result = await db.execute(
text("SELECT DISTINCT source FROM error_logs ORDER BY source")
)
sources = [r[0] for r in sources_result.fetchall()]
levels_result = await db.execute(
text("SELECT DISTINCT level FROM error_logs ORDER BY level")
)
levels = [r[0] for r in levels_result.fetchall()]
return {
"total": total,
"errors": errors,
"sources": sources,
"levels": levels,
}
@router.delete("/errors")
async def clear_errors(
days: int = 30,
_admin: dict = Depends(get_current_admin),
):
"""清除旧错误日志(管理员)"""
cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
async with get_db() as db:
result = await db.execute(
text("DELETE FROM error_logs WHERE created_at < :cutoff"),
{"cutoff": cutoff},
)
deleted = result.rowcount
await db.commit()
return {"status": "ok", "deleted": deleted}
@router.get("/system")
async def system_status(_admin: dict = Depends(get_current_admin)):
"""系统运行状态摘要(管理员)"""
from app.engine.recommender import _scan_running, _scan_lock
async with get_db() as db:
# 各表数据量
tables_counts = {}
for t in ["recommendations", "sector_heat", "market_temperature",
"recommendation_tracking", "stock_diagnoses",
"error_logs", "users"]:
result = await db.execute(text(f"SELECT COUNT(*) FROM {t}"))
tables_counts[t] = result.scalar() or 0
# 最近 24h 错误数
since = (datetime.now() - timedelta(hours=24)).strftime("%Y-%m-%d %H:%M:%S")
result = await db.execute(
text("SELECT COUNT(*) FROM error_logs WHERE created_at >= :since"),
{"since": since},
)
recent_errors = result.scalar() or 0
# 最近错误
result = await db.execute(
text("SELECT source, message, created_at FROM error_logs ORDER BY created_at DESC LIMIT 5")
)
last_errors = [
{"source": r[0], "message": r[1], "created_at": str(r[2])}
for r in result.fetchall()
]
# 数据库文件大小
db_path = settings.database_url.replace("sqlite:///", "")
db_size_mb = 0
if os.path.exists(db_path):
db_size_mb = round(os.path.getsize(db_path) / 1024 / 1024, 2)
return {
"is_trading": is_trading_hours(),
"scan_running": _scan_running,
"scan_locked": _scan_lock.locked(),
"recent_errors": recent_errors,
"last_errors": last_errors,
"tables_counts": tables_counts,
"db_size_mb": db_size_mb,
}