feat(logs): unify filtering across list and stats

Make runtime log queries support request correlation and date-range diagnostics with shared filtering semantics so the log page can use one consistent contract.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 22:11:41 +08:00
parent 204cb223a3
commit a27736a832
4 changed files with 446 additions and 132 deletions

View File

@@ -1,11 +1,12 @@
from fastapi import APIRouter, Depends, Query
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
from typing import Optional
from typing import Any, Optional
from app.database import get_db
from app.models.user import User
from app.routers.auth import get_current_user
from app.services.log_service import LogService
from app.services.log_service import LogService, parse_datetime_filter, serialize_log
router = APIRouter(prefix="/api/logs", tags=["Log"])
@@ -15,14 +16,18 @@ class LogOut(BaseModel):
level: str
type: str
user_id: Optional[str]
request_id: Optional[str]
route: Optional[str]
method: Optional[str]
status_code: Optional[int]
error_type: Optional[str]
operation: Optional[str]
message: str
source: Optional[str]
details: Optional[str]
duration_ms: Optional[str]
created_at: str
updated_at: str
model_config = {"from_attributes": True}
details: Optional[dict[str, Any]]
duration_ms: Optional[int]
created_at: Optional[str]
updated_at: Optional[str]
class LogStatsOut(BaseModel):
@@ -43,12 +48,23 @@ async def list_logs(
log_type: Optional[str] = Query(None, description="日志类型: agent/system/chat"),
level: Optional[str] = Query(None, description="日志级别: debug/info/warning/error"),
source: Optional[str] = Query(None, description="来源模块"),
request_id: Optional[str] = Query(None, description="请求 ID"),
route: Optional[str] = Query(None, description="路由"),
operation: Optional[str] = Query(None, description="业务操作"),
status_code: Optional[int] = Query(None, description="HTTP 状态码"),
start_at: Optional[str] = Query(None, description="开始时间 ISO"),
end_at: Optional[str] = Query(None, description="结束时间 ISO"),
page: int = Query(1, ge=1),
page_size: int = Query(50, ge=1, le=200),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""查询日志列表"""
start_dt = parse_datetime_filter(start_at)
end_dt = parse_datetime_filter(end_at)
if start_dt and end_dt and start_dt > end_dt:
raise HTTPException(status_code=422, detail="开始时间不能晚于结束时间")
svc = LogService(db)
offset = (page - 1) * page_size
logs, total = await svc.list_logs(
@@ -56,11 +72,17 @@ async def list_logs(
level=level,
user_id=current_user.id,
source=source,
request_id=request_id,
route=route,
operation=operation,
status_code=status_code,
start_at=start_dt,
end_at=end_dt,
limit=page_size,
offset=offset,
)
return LogQueryOut(
logs=[LogOut.model_validate(log) for log in logs],
logs=[LogOut.model_validate(serialize_log(log)) for log in logs],
total=total,
page=page,
page_size=page_size,
@@ -69,13 +91,37 @@ async def list_logs(
@router.get("/stats", response_model=LogStatsOut)
async def get_log_stats(
hours: int = Query(24, ge=1, le=168),
log_type: Optional[str] = Query(None, description="日志类型: agent/system/chat"),
level: Optional[str] = Query(None, description="日志级别: debug/info/warning/error"),
source: Optional[str] = Query(None, description="来源模块"),
request_id: Optional[str] = Query(None, description="请求 ID"),
route: Optional[str] = Query(None, description="路由"),
operation: Optional[str] = Query(None, description="业务操作"),
status_code: Optional[int] = Query(None, description="HTTP 状态码"),
start_at: Optional[str] = Query(None, description="开始时间 ISO"),
end_at: Optional[str] = Query(None, description="结束时间 ISO"),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""获取日志统计"""
start_dt = parse_datetime_filter(start_at)
end_dt = parse_datetime_filter(end_at)
if start_dt and end_dt and start_dt > end_dt:
raise HTTPException(status_code=422, detail="开始时间不能晚于结束时间")
svc = LogService(db)
stats = await svc.get_log_stats(hours=hours)
stats = await svc.get_log_stats(
log_type=log_type,
level=level,
user_id=current_user.id,
source=source,
request_id=request_id,
route=route,
operation=operation,
status_code=status_code,
start_at=start_dt,
end_at=end_dt,
)
return LogStatsOut(**stats)
@@ -89,5 +135,5 @@ async def get_recent_logs(
):
"""获取最近的日志"""
svc = LogService(db)
logs = await svc.get_recent_logs(log_type=log_type, hours=hours, limit=limit)
return [LogOut.model_validate(log) for log in logs]
logs = await svc.get_recent_logs(log_type=log_type, user_id=current_user.id, hours=hours, limit=limit)
return [LogOut.model_validate(serialize_log(log)) for log in logs]