mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-31 13:19:48 +08:00
feat(api,ui): 新增按Key调用详情与错误日志查找并联动前端
引入按密钥维度的请求详情及错误日志关联,新增错误日志精确 查找接口,并扩展统计时间维度,提升故障定位与可观测性。 - 新增 /api/logs/errors/lookup 接口:支持按 gemini_key / timestamp / status_code 与时间窗口查找最接近的错误日志;ErrorLogDetailResponse 增加 error_code 字段 - Stats 接口增强:get_api_call_details 返回 status_code、latency_ms, 并在失败时尝试匹配 error_log_id;新增 /api/stats/key-details 获取指 定密钥调用详情;新增 8h 时间段 - DB 层:add_error_log 支持传入 request_datetime(默认使用 UTC);新增 find_error_log_by_info 封装按 key/时间窗口/状态码的查询 - 前端 keys_status:趋势图支持 8 小时区间;调用详情表新增状态码/耗时与 失败详情按钮;可按 key 查看期内调用详情并查看匹配错误日志;优化统计 摘要展示与模态层级(z-index) - OpenAIChatService:错误记录携带请求时间;改进日志与健壮性处理
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from sqlalchemy import asc, delete, desc, func, insert, select, update
|
||||
@@ -107,6 +107,7 @@ async def add_error_log(
|
||||
error_log: Optional[str] = None,
|
||||
error_code: Optional[int] = None,
|
||||
request_msg: Optional[Union[Dict[str, Any], str]] = None,
|
||||
request_datetime: Optional[datetime] = None,
|
||||
) -> bool:
|
||||
"""
|
||||
添加错误日志
|
||||
@@ -140,7 +141,9 @@ async def add_error_log(
|
||||
model_name=model_name,
|
||||
error_code=error_code,
|
||||
request_msg=request_msg_json,
|
||||
request_time=datetime.now(),
|
||||
request_time=(
|
||||
request_datetime if request_datetime else datetime.now(timezone.utc)
|
||||
),
|
||||
)
|
||||
await database.execute(query)
|
||||
logger.info(f"Added error log for key: {redact_key_for_logging(gemini_key)}")
|
||||
@@ -307,6 +310,78 @@ async def get_error_log_details(log_id: int) -> Optional[Dict[str, Any]]:
|
||||
raise
|
||||
|
||||
|
||||
# 新增函数:通过 gemini_key / error_code / 时间窗口 查找最接近的错误日志
|
||||
async def find_error_log_by_info(
|
||||
gemini_key: str,
|
||||
timestamp: datetime,
|
||||
status_code: Optional[int] = None,
|
||||
window_seconds: int = 1,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
在给定时间窗口内,根据 gemini_key(精确匹配)及可选的 status_code 查找最接近 timestamp 的错误日志。
|
||||
|
||||
假设错误日志的 error_code 存储的是 HTTP 状态码或等价错误码。
|
||||
|
||||
Args:
|
||||
gemini_key: 完整的 Gemini key 字符串。
|
||||
timestamp: 目标时间(UTC 或本地,与存储一致)。
|
||||
status_code: 可选的错误码,若提供则优先匹配该错误码。
|
||||
window_seconds: 允许的时间偏差窗口,单位秒,默认为 1 秒。
|
||||
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 最匹配的一条错误日志的完整详情(字段与 get_error_log_details 一致),若未找到则返回 None。
|
||||
"""
|
||||
try:
|
||||
start_time = timestamp - timedelta(seconds=window_seconds)
|
||||
end_time = timestamp + timedelta(seconds=window_seconds)
|
||||
|
||||
base_query = select(ErrorLog).where(
|
||||
ErrorLog.gemini_key == gemini_key,
|
||||
ErrorLog.request_time >= start_time,
|
||||
ErrorLog.request_time <= end_time,
|
||||
)
|
||||
|
||||
# 若提供了状态码,先尝试按状态码过滤
|
||||
if status_code is not None:
|
||||
query = base_query.where(ErrorLog.error_code == status_code).order_by(
|
||||
ErrorLog.request_time.desc()
|
||||
)
|
||||
candidates = await database.fetch_all(query)
|
||||
if not candidates:
|
||||
# 回退:不按状态码,仅按时间窗口
|
||||
query2 = base_query.order_by(ErrorLog.request_time.desc())
|
||||
candidates = await database.fetch_all(query2)
|
||||
else:
|
||||
query = base_query.order_by(ErrorLog.request_time.desc())
|
||||
candidates = await database.fetch_all(query)
|
||||
|
||||
if not candidates:
|
||||
return None
|
||||
|
||||
# 在 Python 中选择与 timestamp 最接近的一条
|
||||
def _to_dict(row: Any) -> Dict[str, Any]:
|
||||
d = dict(row)
|
||||
if "request_msg" in d and d["request_msg"] is not None:
|
||||
try:
|
||||
d["request_msg"] = json.dumps(
|
||||
d["request_msg"], ensure_ascii=False, indent=2
|
||||
)
|
||||
except TypeError:
|
||||
d["request_msg"] = str(d["request_msg"])
|
||||
return d
|
||||
|
||||
best = min(
|
||||
candidates,
|
||||
key=lambda r: abs((r["request_time"] - timestamp).total_seconds()),
|
||||
)
|
||||
return _to_dict(best)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Failed to find error log by info (key=***{gemini_key[-4:] if gemini_key else ''}, code={status_code}, ts={timestamp}, window={window_seconds}s): {str(e)}"
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
async def delete_error_logs_by_ids(log_ids: List[int]) -> int:
|
||||
"""
|
||||
根据提供的 ID 列表批量删除错误日志 (异步)。
|
||||
|
||||
Reference in New Issue
Block a user