diff --git a/app/database/services.py b/app/database/services.py index 40cd6f2..8d38b17 100644 --- a/app/database/services.py +++ b/app/database/services.py @@ -175,7 +175,15 @@ async def get_error_logs( List[Dict[str, Any]]: 错误日志列表 """ try: - query = select(ErrorLog) + query = select( + ErrorLog.id, + ErrorLog.gemini_key, + ErrorLog.model_name, + ErrorLog.error_type, + ErrorLog.error_log, + ErrorLog.error_code, + ErrorLog.request_time + ) # Apply filters if key_search: @@ -192,7 +200,7 @@ async def get_error_logs( query = query.where(ErrorLog.request_time < end_date) # Apply ordering, limit, and offset - query = query.order_by(ErrorLog.request_time.desc()).limit(limit).offset(offset) + query = query.order_by(ErrorLog.id.desc()).limit(limit).offset(offset) result = await database.fetch_all(query) return [dict(row) for row in result] @@ -242,6 +250,37 @@ async def get_error_logs_count( logger.exception(f"Failed to count error logs with filters: {str(e)}") # Use exception for stack trace raise + +# 新增函数:获取单条错误日志详情 +async def get_error_log_details(log_id: int) -> Optional[Dict[str, Any]]: + """ + 根据 ID 获取单个错误日志的详细信息 + + Args: + log_id (int): 错误日志的 ID + + Returns: + Optional[Dict[str, Any]]: 包含日志详细信息的字典,如果未找到则返回 None + """ + try: + query = select(ErrorLog).where(ErrorLog.id == log_id) + result = await database.fetch_one(query) + if result: + # 将 request_msg (JSONB) 转换为字符串以便在 API 中返回 + log_dict = dict(result) + if 'request_msg' in log_dict and log_dict['request_msg'] is not None: + # 确保即使是 None 或非 JSON 数据也能处理 + try: + log_dict['request_msg'] = json.dumps(log_dict['request_msg'], ensure_ascii=False, indent=2) + except TypeError: + log_dict['request_msg'] = str(log_dict['request_msg']) # Fallback to string + return log_dict + else: + return None + except Exception as e: + logger.exception(f"Failed to get error log details for ID {log_id}: {str(e)}") + raise + # 新增函数:添加请求日志 async def add_request_log( model_name: Optional[str], diff --git a/app/router/log_routes.py b/app/router/log_routes.py index 0f3d56c..5965662 100644 --- a/app/router/log_routes.py +++ b/app/router/log_routes.py @@ -1,15 +1,15 @@ """ 日志路由模块 """ -from typing import Any, Dict, List, Optional +from typing import List, Optional from datetime import datetime from pydantic import BaseModel -from fastapi import APIRouter, HTTPException, Request, Query -from fastapi.responses import RedirectResponse +from fastapi import APIRouter, HTTPException, Request, Query, Path from app.core.security import verify_auth_token from app.log.logger import get_log_routes_logger -from app.database.services import get_error_logs, get_error_logs_count +# 假设这些服务函数已更新或添加 +from app.database.services import get_error_logs, get_error_logs_count, get_error_log_details # 创建路由 router = APIRouter(prefix="/api/logs", tags=["logs"]) @@ -18,45 +18,61 @@ logger = get_log_routes_logger() # Define a response model that includes the total count for pagination -class ErrorLogResponse(BaseModel): - logs: List[Dict[str, Any]] +# 用于列表响应的模型,假设 get_error_logs 返回包含 error_code 的字典 +class ErrorLogListItem(BaseModel): + id: int + gemini_key: Optional[str] = None + error_type: Optional[str] = None + error_code: Optional[int] = None # 列表显示错误码 (应为整数) + model_name: Optional[str] = None + request_time: Optional[datetime] = None + +class ErrorLogListResponse(BaseModel): + logs: List[ErrorLogListItem] # 使用定义的模型列表 total: int -@router.get("/errors", response_model=ErrorLogResponse) +@router.get("/errors", response_model=ErrorLogListResponse) async def get_error_logs_api( request: Request, - limit: int = Query(20, ge=1, le=1000), # Default to 20 to match frontend + limit: int = Query(10, ge=1, le=1000), offset: int = Query(0, ge=0), key_search: Optional[str] = Query(None, description="Search term for Gemini key (partial match)"), - error_search: Optional[str] = Query(None, description="Search term for error type or log message"), - start_date: Optional[datetime] = Query(None, description="Start datetime for filtering (YYYY-MM-DDTHH:MM)"), - end_date: Optional[datetime] = Query(None, description="End datetime for filtering (YYYY-MM-DDTHH:MM)") + error_search: Optional[str] = Query(None, description="Search term for error type or log message"), # 数据库查询需处理 + start_date: Optional[datetime] = Query(None, description="Start datetime for filtering"), + end_date: Optional[datetime] = Query(None, description="End datetime for filtering") ): """ - 获取错误日志 - + 获取错误日志列表 (返回错误码) + Args: request: 请求对象 limit: 限制数量 offset: 偏移量 - + key_search: 密钥搜索 + error_search: 错误搜索 (可能搜索类型或日志内容,由DB层决定) + start_date: 开始日期 + end_date: 结束日期 + Returns: - ErrorLogResponse: An object containing the list of logs and the total count. + ErrorLogListResponse: An object containing the list of logs (with error_code) and the total count. """ auth_token = request.cookies.get("auth_token") if not auth_token or not verify_auth_token(auth_token): - logger.warning("Unauthorized access attempt to error logs") - return RedirectResponse(url="/", status_code=302) + logger.warning("Unauthorized access attempt to error logs list") + # API 返回 401 更合适 + raise HTTPException(status_code=401, detail="Not authenticated") try: - # Fetch logs with search parameters - logs = await get_error_logs( + # 假设 get_error_logs 现在返回包含 error_code 的字典列表 + # 并且可以接受 include_error_code 参数 (如果需要显式指定) + logs_data = await get_error_logs( limit=limit, offset=offset, key_search=key_search, - error_search=error_search, + error_search=error_search, # 数据库查询需要处理这个 start_date=start_date, - end_date=end_date + end_date=end_date, + # include_error_code=True # 如果需要显式传递 ) # Fetch total count with the same search parameters total_count = await get_error_logs_count( @@ -65,7 +81,45 @@ async def get_error_logs_api( start_date=start_date, end_date=end_date ) - return ErrorLogResponse(logs=logs, total=total_count) + # 验证并转换数据以匹配 Pydantic 模型 + validated_logs = [ErrorLogListItem(**log) for log in logs_data] + return ErrorLogListResponse(logs=validated_logs, total=total_count) except Exception as e: - logger.exception(f"Failed to get error logs: {str(e)}") # Use logger.exception for stack trace - raise HTTPException(status_code=500, detail=f"Failed to get error logs: {str(e)}") + logger.exception(f"Failed to get error logs list: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to get error logs list: {str(e)}") + + +# 新增:获取错误日志详情的路由 +class ErrorLogDetailResponse(BaseModel): + id: int + gemini_key: Optional[str] = None + error_type: Optional[str] = None + error_log: Optional[str] = None # 详情接口返回完整的 error_log + request_msg: Optional[str] = None # 详情接口返回 request_msg + model_name: Optional[str] = None + request_time: Optional[datetime] = None + +@router.get("/errors/{log_id}/details", response_model=ErrorLogDetailResponse) +async def get_error_log_detail_api(request: Request, log_id: int = Path(..., ge=1)): + """ + 根据日志 ID 获取错误日志的详细信息 (包括 error_log 和 request_msg) + """ + auth_token = request.cookies.get("auth_token") + if not auth_token or not verify_auth_token(auth_token): + logger.warning(f"Unauthorized access attempt to error log details for ID: {log_id}") + raise HTTPException(status_code=401, detail="Not authenticated") + + try: + # 假设存在一个函数 get_error_log_details(log_id) 来获取完整信息 + log_details = await get_error_log_details(log_id=log_id) + if not log_details: + raise HTTPException(status_code=404, detail="Error log not found") + + # 假设 get_error_log_details 返回一个字典或兼容 Pydantic 的对象 + return ErrorLogDetailResponse(**log_details) + except HTTPException as http_exc: + # Re-raise HTTPException (like 404) + raise http_exc + except Exception as e: + logger.exception(f"Failed to get error log details for ID {log_id}: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to get error log details: {str(e)}") diff --git a/app/static/js/error_logs.js b/app/static/js/error_logs.js index 98a4894..462b5e8 100644 --- a/app/static/js/error_logs.js +++ b/app/static/js/error_logs.js @@ -246,18 +246,13 @@ async function loadErrorLogs() { throw new Error(errorData?.detail || `网络响应异常: ${response.statusText}`); } const data = await response.json(); - // Assuming the API returns an object like { logs: [], total: count } - // If it only returns an array, we can't get the total count accurately for pagination - if (Array.isArray(data)) { - errorLogs = data; - renderErrorLogs(errorLogs); // Pass data directly - updatePagination(errorLogs.length, -1); // Indicate unknown total - } else if (data && Array.isArray(data.logs)) { - errorLogs = data.logs; - renderErrorLogs(errorLogs); // Pass logs array - updatePagination(errorLogs.length, data.total || -1); // Pass total count if available + // API 现在返回 { logs: [], total: count } + if (data && Array.isArray(data.logs)) { + errorLogs = data.logs; // Store the list data (contains error_code) + renderErrorLogs(errorLogs); + updatePagination(errorLogs.length, data.total || -1); } else { - throw new Error('无法识别的API响应格式'); + throw new Error('无法识别的API响应格式'); } @@ -302,8 +297,8 @@ function renderErrorLogs(logs) { } catch (e) { console.error("Error formatting date:", e); } - // Truncate error log content for display - const errorLogContent = log.error_log ? log.error_log.substring(0, 100) + (log.error_log.length > 100 ? '...' : '') : '无'; + // Display error code instead of truncated log + const errorCodeContent = log.error_code || '无'; // Mask the Gemini key for display in the table const maskKey = (key) => { @@ -316,7 +311,7 @@ function renderErrorLogs(logs) {