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) { ${sequentialId} ${maskedKey} ${log.error_type || '未知'} - ${errorLogContent} + ${errorCodeContent} ${log.model_name || '未知'} ${formattedTime} @@ -338,57 +333,88 @@ function renderErrorLogs(logs) { }); } -// 显示错误日志详情 (Custom Modal Logic) -function showLogDetails(logId) { - const log = errorLogs.find(l => l.id === logId); - if (!log || !logDetailModal) return; +// 显示错误日志详情 (从 API 获取) +async function showLogDetails(logId) { + if (!logDetailModal) return; - // Format date - let formattedTime = 'N/A'; - try { - const requestTime = new Date(log.request_time); - if (!isNaN(requestTime)) { - formattedTime = requestTime.toLocaleString('zh-CN', { - year: 'numeric', month: '2-digit', day: '2-digit', - hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false - }); - } - } catch (e) { console.error("Error formatting date:", e); } + // Show loading state in modal (optional) + // Clear previous content and show a spinner or message + document.getElementById('modalGeminiKey').textContent = '加载中...'; + document.getElementById('modalErrorType').textContent = '加载中...'; + document.getElementById('modalErrorLog').textContent = '加载中...'; + document.getElementById('modalRequestMsg').textContent = '加载中...'; + document.getElementById('modalModelName').textContent = '加载中...'; + document.getElementById('modalRequestTime').textContent = '加载中...'; - - // Format request message (handle potential JSON) - let formattedRequestMsg = '无'; - if (log.request_msg) { - try { - // Check if it's already an object/array - if (typeof log.request_msg === 'object' && log.request_msg !== null) { - formattedRequestMsg = JSON.stringify(log.request_msg, null, 2); - } - // Check if it's a JSON string - else if (typeof log.request_msg === 'string' && log.request_msg.trim().startsWith('{') || log.request_msg.trim().startsWith('[')) { - formattedRequestMsg = JSON.stringify(JSON.parse(log.request_msg), null, 2); - } - else { - formattedRequestMsg = String(log.request_msg); - } - } catch (e) { - formattedRequestMsg = String(log.request_msg); // Fallback to string - console.warn("Could not parse request_msg as JSON:", e); - } - } - - // Populate modal content (show full key in modal) - document.getElementById('modalGeminiKey').textContent = log.gemini_key || '无'; - document.getElementById('modalErrorType').textContent = log.error_type || '未知'; - document.getElementById('modalErrorLog').textContent = log.error_log || '无'; - document.getElementById('modalRequestMsg').textContent = formattedRequestMsg; - document.getElementById('modalModelName').textContent = log.model_name || '未知'; - document.getElementById('modalRequestTime').textContent = formattedTime; - - // Show the modal logDetailModal.classList.add('show'); - // Optional: Prevent body scrolling when modal is open - document.body.style.overflow = 'hidden'; + document.body.style.overflow = 'hidden'; // Prevent body scrolling + + try { + const response = await fetch(`/api/logs/errors/${logId}/details`); + if (!response.ok) { + let errorData; + try { + errorData = await response.json(); + } catch (e) { /* ignore */ } + throw new Error(errorData?.detail || `获取日志详情失败: ${response.statusText}`); + } + const logDetails = await response.json(); + + // Format date + let formattedTime = 'N/A'; + try { + const requestTime = new Date(logDetails.request_time); + if (!isNaN(requestTime)) { + formattedTime = requestTime.toLocaleString('zh-CN', { + year: 'numeric', month: '2-digit', day: '2-digit', + hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false + }); + } + } catch (e) { console.error("Error formatting date:", e); } + + // Format request message (handle potential JSON) + let formattedRequestMsg = '无'; + if (logDetails.request_msg) { + try { + if (typeof logDetails.request_msg === 'object' && logDetails.request_msg !== null) { + formattedRequestMsg = JSON.stringify(logDetails.request_msg, null, 2); + } else if (typeof logDetails.request_msg === 'string') { + // Try parsing if it looks like JSON, otherwise display as string + const trimmedMsg = logDetails.request_msg.trim(); + if (trimmedMsg.startsWith('{') || trimmedMsg.startsWith('[')) { + formattedRequestMsg = JSON.stringify(JSON.parse(logDetails.request_msg), null, 2); + } else { + formattedRequestMsg = logDetails.request_msg; + } + } else { + formattedRequestMsg = String(logDetails.request_msg); + } + } catch (e) { + formattedRequestMsg = String(logDetails.request_msg); // Fallback + console.warn("Could not parse request_msg as JSON:", e); + } + } + + // Populate modal content with fetched details + document.getElementById('modalGeminiKey').textContent = logDetails.gemini_key || '无'; + document.getElementById('modalErrorType').textContent = logDetails.error_type || '未知'; + document.getElementById('modalErrorLog').textContent = logDetails.error_log || '无'; // Full error log + document.getElementById('modalRequestMsg').textContent = formattedRequestMsg; // Full request message + document.getElementById('modalModelName').textContent = logDetails.model_name || '未知'; + document.getElementById('modalRequestTime').textContent = formattedTime; + + } catch (error) { + console.error('获取日志详情失败:', error); + // Show error in modal + document.getElementById('modalGeminiKey').textContent = '错误'; + document.getElementById('modalErrorType').textContent = '错误'; + document.getElementById('modalErrorLog').textContent = `加载失败: ${error.message}`; + document.getElementById('modalRequestMsg').textContent = '错误'; + document.getElementById('modalModelName').textContent = '错误'; + document.getElementById('modalRequestTime').textContent = '错误'; + // Optionally show a notification + showNotification(`加载日志详情失败: ${error.message}`, 'error', 5000); + } } // Close Log Detail Modal diff --git a/app/templates/error_logs.html b/app/templates/error_logs.html index 58158f9..1c63ae7 100644 --- a/app/templates/error_logs.html +++ b/app/templates/error_logs.html @@ -105,7 +105,7 @@ ID Gemini密钥 错误类型 - 错误日志 + 错误码 模型名称 请求时间 操作