mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-09-05 15:36:45 +08:00
feat(error_logs): 添加错误日志搜索和日期过滤功能
- 在后端 (`services.py`, `log_routes.py`) 实现按 Gemini 密钥(模糊匹配)、错误类型/内容(模糊匹配)和日期范围(开始/结束日期)过滤错误日志的逻辑。
- 添加新函数 `get_error_logs_count` 以高效获取符合过滤条件的总日志数,用于分页。
- 更新 `/api/logs/errors` API 端点以接受 `key_search`, `error_search`, `start_date`, `end_date` 查询参数。端点现在返回包含过滤后日志和总数的对象。
- 增强前端 (`error_logs.html`, `error_logs.js`, `error_logs.css`):
- 添加用于密钥搜索、错误/日志搜索和日期范围选择的输入字段。
- 实现 JavaScript 逻辑以捕获搜索参数,使用过滤器触发 API 调用,并在新搜索时重置到第一页。
- 更新表格渲染以显示顺序行号而非数据库 ID。
- 在表格视图中遮罩 Gemini 密钥(显示前/后 4 个字符)以提高可读性,同时仍在详细信息模态框中显示完整密钥。
- 优化新搜索控件、表格外观(内边距、边框、悬停效果、斑马条纹)和按钮样式的 CSS,以提供更清晰的用户界面。
- 通过使用 `logger.exception` 包含堆栈跟踪来改进后端服务中的错误日志记录。
This commit is contained in:
@@ -4,8 +4,9 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
from typing import Dict, List, Optional, Any, Union
|
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.connection import database
|
||||||
from app.database.models import Settings, ErrorLog
|
from app.database.models import Settings, ErrorLog
|
||||||
@@ -152,21 +153,91 @@ async def add_error_log(
|
|||||||
return False
|
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:
|
Args:
|
||||||
limit: 限制数量
|
limit (int): 限制数量
|
||||||
offset: 偏移量
|
offset (int): 偏移量
|
||||||
|
key_search (Optional[str]): Gemini密钥搜索词 (模糊匹配)
|
||||||
|
error_search (Optional[str]): 错误类型或日志内容搜索词 (模糊匹配)
|
||||||
|
start_date (Optional[date]): 开始日期
|
||||||
|
end_date (Optional[date]): 结束日期
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[Dict[str, Any]]: 错误日志列表
|
List[Dict[str, Any]]: 错误日志列表
|
||||||
"""
|
"""
|
||||||
try:
|
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)
|
result = await database.fetch_all(query)
|
||||||
return [dict(row) for row in result]
|
return [dict(row) for row in result]
|
||||||
except Exception as e:
|
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
|
raise
|
||||||
|
|||||||
@@ -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 import APIRouter, HTTPException, Request, Query
|
||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
from app.core.security import verify_auth_token
|
from app.core.security import verify_auth_token
|
||||||
from app.log.logger import get_log_routes_logger
|
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"])
|
router = APIRouter(prefix="/api/logs", tags=["logs"])
|
||||||
@@ -15,11 +17,20 @@ router = APIRouter(prefix="/api/logs", tags=["logs"])
|
|||||||
logger = get_log_routes_logger()
|
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(
|
async def get_error_logs_api(
|
||||||
request: Request,
|
request: Request,
|
||||||
limit: int = Query(100, ge=1, le=1000),
|
limit: int = Query(20, ge=1, le=1000), # Default to 20 to match frontend
|
||||||
offset: int = Query(0, ge=0)
|
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: 偏移量
|
offset: 偏移量
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List[Dict[str, Any]]: 错误日志列表
|
ErrorLogResponse: An object containing the list of logs and the total count.
|
||||||
"""
|
"""
|
||||||
auth_token = request.cookies.get("auth_token")
|
auth_token = request.cookies.get("auth_token")
|
||||||
if not auth_token or not verify_auth_token(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)
|
return RedirectResponse(url="/", status_code=302)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logs = await get_error_logs(limit, offset)
|
# Fetch logs with search parameters
|
||||||
return logs
|
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:
|
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)}")
|
raise HTTPException(status_code=500, detail=f"Failed to get error logs: {str(e)}")
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
/* error_logs.css - Styles specific to the error logs page, complementing config_editor.css */
|
/* 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 */
|
/* Style the controls container */
|
||||||
.controls-container {
|
.controls-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -9,9 +17,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
background-color: #f8f9fa; /* Lighter background */
|
||||||
border-radius: 8px;
|
border-radius: 6px; /* Slightly smaller radius */
|
||||||
border: 1px solid rgba(0,0,0,0.05);
|
border: 1px solid #e9ecef; /* Lighter border */
|
||||||
flex-wrap: wrap; /* Allow wrapping on smaller screens */
|
flex-wrap: wrap; /* Allow wrapping on smaller screens */
|
||||||
gap: 15px; /* Add gap between items */
|
gap: 15px; /* Add gap between items */
|
||||||
}
|
}
|
||||||
@@ -67,13 +75,53 @@
|
|||||||
font-size: 1em;
|
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 and styled table */
|
||||||
.table-container {
|
.table-container {
|
||||||
overflow-x: auto; /* Allow horizontal scrolling for table */
|
overflow-x: auto; /* Allow horizontal scrolling for table */
|
||||||
background-color: white;
|
background-color: #ffffff;
|
||||||
border-radius: 8px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
|
box-shadow: 0 2px 8px rgba(0,0,0,0.06); /* Softer shadow */
|
||||||
border: 1px solid rgba(0,0,0,0.05);
|
border: 1px solid #e9ecef; /* Lighter border */
|
||||||
}
|
}
|
||||||
|
|
||||||
.styled-table {
|
.styled-table {
|
||||||
@@ -84,17 +132,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.styled-table thead tr {
|
.styled-table thead tr {
|
||||||
background-color: #f8f9fa; /* Light grey header */
|
background-color: #eef2f7; /* Lighter, slightly blueish header */
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-weight: bold;
|
font-weight: 600; /* Slightly bolder */
|
||||||
color: #2c3e50;
|
color: #495057; /* Darker grey text */
|
||||||
border-bottom: 2px solid #dee2e6;
|
border-bottom: 1px solid #dee2e6; /* Thinner border */
|
||||||
}
|
}
|
||||||
|
|
||||||
.styled-table th,
|
.styled-table th,
|
||||||
.styled-table td {
|
.styled-table td {
|
||||||
padding: 12px 15px;
|
padding: 14px 15px; /* Increased padding */
|
||||||
border-bottom: 1px solid #eee; /* Lighter border for rows */
|
border-bottom: 1px solid #f1f3f5; /* Very light border */
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +151,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.styled-table tbody tr:hover {
|
.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 {
|
.styled-table tbody tr:last-of-type {
|
||||||
@@ -121,18 +178,20 @@
|
|||||||
|
|
||||||
/* Style for the 'View Details' button */
|
/* Style for the 'View Details' button */
|
||||||
.btn-view-details {
|
.btn-view-details {
|
||||||
background-color: #667eea;
|
background-color: #e9ecef; /* Light grey background */
|
||||||
color: white;
|
color: #495057; /* Dark grey text */
|
||||||
border: none;
|
border: 1px solid #dee2e6; /* Subtle border */
|
||||||
padding: 5px 10px;
|
padding: 4px 8px; /* Slightly smaller padding */
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
transition: background-color 0.3s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-view-details:hover {
|
.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) */
|
/* Status Indicators (Loading, No Data, Error) */
|
||||||
@@ -204,10 +263,10 @@
|
|||||||
|
|
||||||
.pagination .page-link {
|
.pagination .page-link {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 8px 12px;
|
padding: 8px 14px; /* Slightly wider */
|
||||||
color: #667eea;
|
color: #6c757d; /* Grey text */
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #dee2e6; /* Lighter border */
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
@@ -215,15 +274,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pagination .page-link:hover {
|
.pagination .page-link:hover {
|
||||||
background-color: #f0f0f0;
|
background-color: #e9ecef; /* Light grey hover */
|
||||||
border-color: #ccc;
|
border-color: #dee2e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination .page-item.active .page-link {
|
.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;
|
color: white;
|
||||||
border-color: #667eea;
|
border-color: #708cf3;
|
||||||
font-weight: bold;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination .page-item.disabled .page-link {
|
.pagination .page-item.disabled .page-link {
|
||||||
@@ -263,10 +322,10 @@
|
|||||||
|
|
||||||
.detail-item p,
|
.detail-item p,
|
||||||
.detail-item pre {
|
.detail-item pre {
|
||||||
background-color: #f8f9fa;
|
background-color: #e9ecef; /* Lighter background for code blocks */
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
border-radius: 4px;
|
border-radius: 5px;
|
||||||
border: 1px solid #eee;
|
border: 1px solid #dee2e6; /* Match border color */
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ let currentPage = 1;
|
|||||||
let pageSize = 20;
|
let pageSize = 20;
|
||||||
// let totalPages = 1; // totalPages will be calculated dynamically based on API response if available, or based on fetched data length
|
// 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 errorLogs = []; // Store fetched logs for details view
|
||||||
|
let currentSearch = { // Store current search parameters
|
||||||
|
key: '',
|
||||||
|
error: '',
|
||||||
|
startDate: '',
|
||||||
|
endDate: ''
|
||||||
|
};
|
||||||
|
|
||||||
// DOM Elements Cache
|
// DOM Elements Cache
|
||||||
let pageSizeSelector;
|
let pageSizeSelector;
|
||||||
@@ -48,6 +54,11 @@ let noDataMessage;
|
|||||||
let errorMessage;
|
let errorMessage;
|
||||||
let logDetailModal;
|
let logDetailModal;
|
||||||
let modalCloseBtns; // Collection of close buttons for the modal
|
let modalCloseBtns; // Collection of close buttons for the modal
|
||||||
|
let keySearchInput;
|
||||||
|
let errorSearchInput;
|
||||||
|
let startDateInput;
|
||||||
|
let endDateInput;
|
||||||
|
let searchBtn;
|
||||||
|
|
||||||
// 页面加载完成后执行
|
// 页面加载完成后执行
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
@@ -62,6 +73,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
logDetailModal = document.getElementById('logDetailModal');
|
logDetailModal = document.getElementById('logDetailModal');
|
||||||
// Get all elements that should close the modal
|
// Get all elements that should close the modal
|
||||||
modalCloseBtns = document.querySelectorAll('#closeLogDetailModalBtn, #closeModalFooterBtn');
|
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
|
// Initialize page size selector
|
||||||
if (pageSizeSelector) {
|
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
|
// Initialize modal close buttons
|
||||||
if (logDetailModal && modalCloseBtns) {
|
if (logDetailModal && modalCloseBtns) {
|
||||||
modalCloseBtns.forEach(btn => {
|
modalCloseBtns.forEach(btn => {
|
||||||
@@ -112,7 +141,22 @@ async function loadErrorLogs() {
|
|||||||
const offset = (currentPage - 1) * pageSize;
|
const offset = (currentPage - 1) * pageSize;
|
||||||
|
|
||||||
try {
|
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) {
|
if (!response.ok) {
|
||||||
// Try to get error message from response body
|
// Try to get error message from response body
|
||||||
let errorData;
|
let errorData;
|
||||||
@@ -162,9 +206,11 @@ function renderErrorLogs(logs) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logs.forEach(log => {
|
const startIndex = (currentPage - 1) * pageSize; // Calculate starting index for the current page
|
||||||
const row = document.createElement('tr');
|
|
||||||
|
|
||||||
|
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
|
// Format date
|
||||||
let formattedTime = 'N/A';
|
let formattedTime = 'N/A';
|
||||||
try {
|
try {
|
||||||
@@ -181,9 +227,16 @@ function renderErrorLogs(logs) {
|
|||||||
// Truncate error log content for display
|
// Truncate error log content for display
|
||||||
const errorLogContent = log.error_log ? log.error_log.substring(0, 100) + (log.error_log.length > 100 ? '...' : '') : '无';
|
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 = `
|
row.innerHTML = `
|
||||||
<td>${log.id}</td>
|
<td>${sequentialId}</td> <!-- Use sequential ID -->
|
||||||
<td>${log.gemini_key || '无'}</td>
|
<td title="${log.gemini_key || ''}">${maskedKey}</td>
|
||||||
<td>${log.error_type || '未知'}</td>
|
<td>${log.error_type || '未知'}</td>
|
||||||
<td class="error-log-content" title="${log.error_log || ''}">${errorLogContent}</td>
|
<td class="error-log-content" title="${log.error_log || ''}">${errorLogContent}</td>
|
||||||
<td>${log.model_name || '未知'}</td>
|
<td>${log.model_name || '未知'}</td>
|
||||||
@@ -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('modalGeminiKey').textContent = log.gemini_key || '无';
|
||||||
document.getElementById('modalErrorType').textContent = log.error_type || '未知';
|
document.getElementById('modalErrorType').textContent = log.error_type || '未知';
|
||||||
document.getElementById('modalErrorLog').textContent = log.error_log || '无';
|
document.getElementById('modalErrorLog').textContent = log.error_log || '无';
|
||||||
|
|||||||
@@ -54,6 +54,18 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Controls -->
|
||||||
|
<div class="search-container">
|
||||||
|
<input type="text" id="keySearch" placeholder="搜索密钥 (部分)">
|
||||||
|
<input type="text" id="errorSearch" placeholder="搜索错误类型/日志"> <!-- Changed ID -->
|
||||||
|
<input type="date" id="startDate">
|
||||||
|
<span>至</span>
|
||||||
|
<input type="date" id="endDate">
|
||||||
|
<button id="searchBtn" class="action-btn">
|
||||||
|
<i class="fas fa-search"></i> 搜索
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-container"> <!-- New container for table -->
|
<div class="table-container"> <!-- New container for table -->
|
||||||
<table class="styled-table"> <!-- Use a custom table class -->
|
<table class="styled-table"> <!-- Use a custom table class -->
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
Reference in New Issue
Block a user