diff --git a/app/database/services.py b/app/database/services.py index 4158b7c..f73cd96 100644 --- a/app/database/services.py +++ b/app/database/services.py @@ -4,8 +4,9 @@ import datetime import json from typing import Dict, List, Optional, Any, Union +from datetime import date, timedelta # Import date and timedelta -from sqlalchemy import select, insert, update +from sqlalchemy import select, insert, update, func # Import func for COUNT from app.database.connection import database from app.database.models import Settings, ErrorLog @@ -152,21 +153,91 @@ async def add_error_log( return False -async def get_error_logs(limit: int = 100, offset: int = 0) -> List[Dict[str, Any]]: +async def get_error_logs( + limit: int = 20, + offset: int = 0, + key_search: Optional[str] = None, + error_search: Optional[str] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None +) -> List[Dict[str, Any]]: """ - 获取错误日志 - + 获取错误日志,支持搜索和日期过滤 + Args: - limit: 限制数量 - offset: 偏移量 - + limit (int): 限制数量 + offset (int): 偏移量 + key_search (Optional[str]): Gemini密钥搜索词 (模糊匹配) + error_search (Optional[str]): 错误类型或日志内容搜索词 (模糊匹配) + start_date (Optional[date]): 开始日期 + end_date (Optional[date]): 结束日期 + Returns: List[Dict[str, Any]]: 错误日志列表 """ try: - query = select(ErrorLog).order_by(ErrorLog.request_time.desc()).limit(limit).offset(offset) + query = select(ErrorLog) + + # Apply filters + if key_search: + query = query.where(ErrorLog.gemini_key.ilike(f"%{key_search}%")) + if error_search: + query = query.where( + (ErrorLog.error_type.ilike(f"%{error_search}%")) | + (ErrorLog.error_log.ilike(f"%{error_search}%")) + ) + if start_date: + query = query.where(ErrorLog.request_time >= start_date) + if end_date: + # Add 1 day to end_date to include the whole day + query = query.where(ErrorLog.request_time < end_date + timedelta(days=1)) + + # Apply ordering, limit, and offset + query = query.order_by(ErrorLog.request_time.desc()).limit(limit).offset(offset) + result = await database.fetch_all(query) return [dict(row) for row in result] except Exception as e: - logger.error(f"Failed to get error logs: {str(e)}") + logger.exception(f"Failed to get error logs with filters: {str(e)}") # Use exception for stack trace + raise + + +async def get_error_logs_count( + key_search: Optional[str] = None, + error_search: Optional[str] = None, + start_date: Optional[date] = None, + end_date: Optional[date] = None +) -> int: + """ + 获取符合条件的错误日志总数 + + Args: + key_search (Optional[str]): Gemini密钥搜索词 (模糊匹配) + error_search (Optional[str]): 错误类型或日志内容搜索词 (模糊匹配) + start_date (Optional[date]): 开始日期 + end_date (Optional[date]): 结束日期 + + Returns: + int: 日志总数 + """ + try: + query = select(func.count()).select_from(ErrorLog) + + # Apply the same filters as get_error_logs + if key_search: + query = query.where(ErrorLog.gemini_key.ilike(f"%{key_search}%")) + if error_search: + query = query.where( + (ErrorLog.error_type.ilike(f"%{error_search}%")) | + (ErrorLog.error_log.ilike(f"%{error_search}%")) + ) + if start_date: + query = query.where(ErrorLog.request_time >= start_date) + if end_date: + query = query.where(ErrorLog.request_time < end_date + timedelta(days=1)) + + count_result = await database.fetch_one(query) + return count_result[0] if count_result else 0 + except Exception as e: + logger.exception(f"Failed to count error logs with filters: {str(e)}") # Use exception for stack trace raise diff --git a/app/router/log_routes.py b/app/router/log_routes.py index 41eb759..bbd6b7b 100644 --- a/app/router/log_routes.py +++ b/app/router/log_routes.py @@ -1,13 +1,15 @@ """ 日志路由模块 """ -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional +from datetime import date +from pydantic import BaseModel from fastapi import APIRouter, HTTPException, Request, Query from fastapi.responses import RedirectResponse 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 +from app.database.services import get_error_logs, get_error_logs_count # 创建路由 router = APIRouter(prefix="/api/logs", tags=["logs"]) @@ -15,11 +17,20 @@ router = APIRouter(prefix="/api/logs", tags=["logs"]) logger = get_log_routes_logger() -@router.get("/errors", response_model=List[Dict[str, Any]]) +# Define a response model that includes the total count for pagination +class ErrorLogResponse(BaseModel): + logs: List[Dict[str, Any]] + total: int + +@router.get("/errors", response_model=ErrorLogResponse) async def get_error_logs_api( request: Request, - limit: int = Query(100, ge=1, le=1000), - offset: int = Query(0, ge=0) + limit: int = Query(20, ge=1, le=1000), # Default to 20 to match frontend + 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[date] = Query(None, description="Start date for filtering (YYYY-MM-DD)"), + end_date: Optional[date] = Query(None, description="End date for filtering (YYYY-MM-DD)") ): """ 获取错误日志 @@ -30,7 +41,7 @@ async def get_error_logs_api( offset: 偏移量 Returns: - List[Dict[str, Any]]: 错误日志列表 + ErrorLogResponse: An object containing the list of logs and the total count. """ auth_token = request.cookies.get("auth_token") if not auth_token or not verify_auth_token(auth_token): @@ -38,8 +49,23 @@ async def get_error_logs_api( return RedirectResponse(url="/", status_code=302) try: - logs = await get_error_logs(limit, offset) - return logs + # Fetch logs with search parameters + logs = await get_error_logs( + limit=limit, + offset=offset, + key_search=key_search, + error_search=error_search, + start_date=start_date, + end_date=end_date + ) + # Fetch total count with the same search parameters + total_count = await get_error_logs_count( + key_search=key_search, + error_search=error_search, + start_date=start_date, + end_date=end_date + ) + return ErrorLogResponse(logs=logs, total=total_count) except Exception as e: - logger.error(f"Failed to get error logs: {str(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)}") diff --git a/app/static/css/error_logs.css b/app/static/css/error_logs.css index 7f6d402..073fdb5 100644 --- a/app/static/css/error_logs.css +++ b/app/static/css/error_logs.css @@ -1,7 +1,15 @@ /* error_logs.css - Styles specific to the error logs page, complementing config_editor.css */ -/* Inherit body, container, h1, nav-tabs, config-section, scroll-buttons, refresh-btn, copyright from config_editor.css */ +/* Inherit body, container, h1, nav-tabs, scroll-buttons, refresh-btn, copyright from config_editor.css */ +/* Add padding to the main content section */ +.config-section { + padding: 25px; /* Increased padding */ + background-color: #fff; /* Ensure white background */ + border-radius: 12px; /* Slightly larger radius */ + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); /* Softer shadow */ + margin-top: 20px; +} /* Style the controls container */ .controls-container { display: flex; @@ -9,9 +17,9 @@ align-items: center; margin-bottom: 20px; padding: 15px; - background-color: rgba(255, 255, 255, 0.8); - border-radius: 8px; - border: 1px solid rgba(0,0,0,0.05); + background-color: #f8f9fa; /* Lighter background */ + border-radius: 6px; /* Slightly smaller radius */ + border: 1px solid #e9ecef; /* Lighter border */ flex-wrap: wrap; /* Allow wrapping on smaller screens */ gap: 15px; /* Add gap between items */ } @@ -67,13 +75,53 @@ font-size: 1em; } +/* Search Container Styles */ +.search-container { + display: flex; + flex-wrap: wrap; /* Allow wrapping on smaller screens */ + gap: 10px; /* Space between elements */ + align-items: center; + padding: 15px; + background-color: #f8f9fa; /* Consistent light background */ + border-radius: 6px; + border: 1px solid #e9ecef; /* Lighter border */ + margin-bottom: 25px; /* Increased margin */ +} + +.search-container input[type="text"], +.search-container input[type="date"] { + padding: 8px 12px; + border: 1px solid rgba(0,0,0,0.1); + border-radius: 6px; + font-size: 14px; + flex-grow: 1; /* Allow inputs to grow */ + min-width: 150px; /* Minimum width for inputs */ +} + +.search-container input[type="date"] { + min-width: 130px; /* Slightly less width for date inputs */ + flex-grow: 0; /* Don't let date inputs grow excessively */ +} + + +.search-container span { + color: #2c3e50; + margin: 0 5px; +} + +.search-container .action-btn { + padding: 8px 15px; /* Slightly smaller padding for search button */ + font-size: 14px; + flex-shrink: 0; /* Prevent button from shrinking */ +} + /* Table container and styled table */ .table-container { overflow-x: auto; /* Allow horizontal scrolling for table */ - background-color: white; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0,0,0,0.05); - border: 1px solid rgba(0,0,0,0.05); + background-color: #ffffff; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0,0,0,0.06); /* Softer shadow */ + border: 1px solid #e9ecef; /* Lighter border */ } .styled-table { @@ -84,17 +132,17 @@ } .styled-table thead tr { - background-color: #f8f9fa; /* Light grey header */ + background-color: #eef2f7; /* Lighter, slightly blueish header */ text-align: left; - font-weight: bold; - color: #2c3e50; - border-bottom: 2px solid #dee2e6; + font-weight: 600; /* Slightly bolder */ + color: #495057; /* Darker grey text */ + border-bottom: 1px solid #dee2e6; /* Thinner border */ } .styled-table th, .styled-table td { - padding: 12px 15px; - border-bottom: 1px solid #eee; /* Lighter border for rows */ + padding: 14px 15px; /* Increased padding */ + border-bottom: 1px solid #f1f3f5; /* Very light border */ vertical-align: middle; } @@ -103,7 +151,16 @@ } .styled-table tbody tr:hover { - background-color: #f1f1f1; /* Subtle hover effect */ + background-color: #f8f9fa; /* Lighter hover effect */ +} + +/* Zebra striping for table body */ +.styled-table tbody tr:nth-child(even) { + background-color: #fcfdff; /* Very light blue for even rows */ +} + +.styled-table tbody tr:nth-child(even):hover { + background-color: #f0f4f8; /* Slightly darker hover for even rows */ } .styled-table tbody tr:last-of-type { @@ -121,18 +178,20 @@ /* Style for the 'View Details' button */ .btn-view-details { - background-color: #667eea; - color: white; - border: none; - padding: 5px 10px; + background-color: #e9ecef; /* Light grey background */ + color: #495057; /* Dark grey text */ + border: 1px solid #dee2e6; /* Subtle border */ + padding: 4px 8px; /* Slightly smaller padding */ border-radius: 4px; cursor: pointer; font-size: 12px; - transition: background-color 0.3s ease; + transition: all 0.2s ease; } .btn-view-details:hover { - background-color: #5a6fd0; + background-color: #dee2e6; /* Darker grey on hover */ + border-color: #ced4da; + color: #212529; } /* Status Indicators (Loading, No Data, Error) */ @@ -204,10 +263,10 @@ .pagination .page-link { display: block; - padding: 8px 12px; - color: #667eea; + padding: 8px 14px; /* Slightly wider */ + color: #6c757d; /* Grey text */ background-color: white; - border: 1px solid #ddd; + border: 1px solid #dee2e6; /* Lighter border */ border-radius: 4px; text-decoration: none; transition: all 0.3s ease; @@ -215,15 +274,15 @@ } .pagination .page-link:hover { - background-color: #f0f0f0; - border-color: #ccc; + background-color: #e9ecef; /* Light grey hover */ + border-color: #dee2e6; } .pagination .page-item.active .page-link { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: linear-gradient(135deg, #708cf3 0%, #8b60d5 100%); /* Adjusted gradient */ color: white; - border-color: #667eea; - font-weight: bold; + border-color: #708cf3; + font-weight: 600; } .pagination .page-item.disabled .page-link { @@ -263,10 +322,10 @@ .detail-item p, .detail-item pre { - background-color: #f8f9fa; - padding: 10px; - border-radius: 4px; - border: 1px solid #eee; + background-color: #e9ecef; /* Lighter background for code blocks */ + padding: 12px; + border-radius: 5px; + border: 1px solid #dee2e6; /* Match border color */ font-size: 14px; color: #333; margin: 0; diff --git a/app/static/js/error_logs.js b/app/static/js/error_logs.js index c0b82ee..bea68a6 100644 --- a/app/static/js/error_logs.js +++ b/app/static/js/error_logs.js @@ -37,6 +37,12 @@ let currentPage = 1; let pageSize = 20; // let totalPages = 1; // totalPages will be calculated dynamically based on API response if available, or based on fetched data length let errorLogs = []; // Store fetched logs for details view +let currentSearch = { // Store current search parameters + key: '', + error: '', + startDate: '', + endDate: '' +}; // DOM Elements Cache let pageSizeSelector; @@ -48,6 +54,11 @@ let noDataMessage; let errorMessage; let logDetailModal; let modalCloseBtns; // Collection of close buttons for the modal +let keySearchInput; +let errorSearchInput; +let startDateInput; +let endDateInput; +let searchBtn; // 页面加载完成后执行 document.addEventListener('DOMContentLoaded', function() { @@ -62,6 +73,11 @@ document.addEventListener('DOMContentLoaded', function() { logDetailModal = document.getElementById('logDetailModal'); // Get all elements that should close the modal modalCloseBtns = document.querySelectorAll('#closeLogDetailModalBtn, #closeModalFooterBtn'); + keySearchInput = document.getElementById('keySearch'); + errorSearchInput = document.getElementById('errorSearch'); + startDateInput = document.getElementById('startDate'); + endDateInput = document.getElementById('endDate'); + searchBtn = document.getElementById('searchBtn'); // Initialize page size selector if (pageSizeSelector) { @@ -86,6 +102,19 @@ document.addEventListener('DOMContentLoaded', function() { }); } + // Initialize search button + if (searchBtn) { + searchBtn.addEventListener('click', function() { + // Update search parameters from input fields + currentSearch.key = keySearchInput ? keySearchInput.value.trim() : ''; + currentSearch.error = errorSearchInput ? errorSearchInput.value.trim() : ''; + currentSearch.startDate = startDateInput ? startDateInput.value : ''; + currentSearch.endDate = endDateInput ? endDateInput.value : ''; + currentPage = 1; // Reset to first page on new search + loadErrorLogs(); + }); + } + // Initialize modal close buttons if (logDetailModal && modalCloseBtns) { modalCloseBtns.forEach(btn => { @@ -112,7 +141,22 @@ async function loadErrorLogs() { const offset = (currentPage - 1) * pageSize; try { - const response = await fetch(`/api/logs/errors?limit=${pageSize}&offset=${offset}`); + // Construct the API URL with search parameters + let apiUrl = `/api/logs/errors?limit=${pageSize}&offset=${offset}`; + if (currentSearch.key) { + apiUrl += `&key_search=${encodeURIComponent(currentSearch.key)}`; + } + if (currentSearch.error) { + apiUrl += `&error_search=${encodeURIComponent(currentSearch.error)}`; + } + if (currentSearch.startDate) { + apiUrl += `&start_date=${encodeURIComponent(currentSearch.startDate)}`; + } + if (currentSearch.endDate) { + apiUrl += `&end_date=${encodeURIComponent(currentSearch.endDate)}`; + } + + const response = await fetch(apiUrl); if (!response.ok) { // Try to get error message from response body let errorData; @@ -162,9 +206,11 @@ function renderErrorLogs(logs) { return; } - logs.forEach(log => { - const row = document.createElement('tr'); + const startIndex = (currentPage - 1) * pageSize; // Calculate starting index for the current page + logs.forEach((log, index) => { // Add index parameter to forEach + const row = document.createElement('tr'); + const sequentialId = startIndex + index + 1; // Calculate sequential ID for the current page // Format date let formattedTime = 'N/A'; try { @@ -181,9 +227,16 @@ function renderErrorLogs(logs) { // Truncate error log content for display const errorLogContent = log.error_log ? log.error_log.substring(0, 100) + (log.error_log.length > 100 ? '...' : '') : '无'; + // Mask the Gemini key for display in the table + const maskKey = (key) => { + if (!key || key.length < 8) return key || '无'; // Don't mask short keys or null + return `${key.substring(0, 4)}...${key.substring(key.length - 4)}`; + }; + const maskedKey = maskKey(log.gemini_key); + row.innerHTML = ` - ${log.id} - ${log.gemini_key || '无'} + ${sequentialId} + ${maskedKey} ${log.error_type || '未知'} ${errorLogContent} ${log.model_name || '未知'} @@ -246,7 +299,7 @@ function showLogDetails(logId) { } } - // Populate modal content + // 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 || '无'; diff --git a/app/templates/error_logs.html b/app/templates/error_logs.html index d4fc347..5c70352 100644 --- a/app/templates/error_logs.html +++ b/app/templates/error_logs.html @@ -54,6 +54,18 @@ + +
+ + + + + + +
+