mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-08-28 11:37:00 +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:
@@ -146,7 +146,7 @@ class StatsService:
|
||||
period: 时间段标识 ('1m', '1h', '24h')
|
||||
|
||||
Returns:
|
||||
包含调用详情的字典列表,每个字典包含 timestamp, key, model, status
|
||||
包含调用详情的字典列表,每个字典包含 timestamp, key, model, status, status_code, latency_ms, error_log_id(可选)
|
||||
|
||||
Raises:
|
||||
ValueError: 如果 period 无效
|
||||
@@ -156,6 +156,8 @@ class StatsService:
|
||||
start_time = now - datetime.timedelta(minutes=1)
|
||||
elif period == "1h":
|
||||
start_time = now - datetime.timedelta(hours=1)
|
||||
elif period == "8h":
|
||||
start_time = now - datetime.timedelta(hours=8)
|
||||
elif period == "24h":
|
||||
start_time = now - datetime.timedelta(hours=24)
|
||||
else:
|
||||
@@ -167,7 +169,8 @@ class StatsService:
|
||||
RequestLog.request_time.label("timestamp"),
|
||||
RequestLog.api_key.label("key"),
|
||||
RequestLog.model_name.label("model"),
|
||||
RequestLog.status_code,
|
||||
RequestLog.status_code.label("status_code"),
|
||||
RequestLog.latency_ms.label("latency_ms"),
|
||||
)
|
||||
.where(RequestLog.request_time >= start_time)
|
||||
.order_by(RequestLog.request_time.desc())
|
||||
@@ -175,29 +178,138 @@ class StatsService:
|
||||
|
||||
results = await database.fetch_all(query)
|
||||
|
||||
details = []
|
||||
# 为失败调用尝试查找匹配的错误日志ID(时间窗口 +/- 5 分钟)
|
||||
from app.database.models import ErrorLog # 延迟导入避免循环依赖
|
||||
|
||||
details: list[dict] = []
|
||||
for row in results:
|
||||
status = "failure"
|
||||
if row["status_code"] is not None:
|
||||
status = "success" if 200 <= row["status_code"] < 300 else "failure"
|
||||
details.append(
|
||||
{
|
||||
"timestamp": row[
|
||||
"timestamp"
|
||||
].isoformat(),
|
||||
"key": row["key"],
|
||||
"model": row["model"],
|
||||
"status": status,
|
||||
}
|
||||
)
|
||||
|
||||
record = {
|
||||
"timestamp": row["timestamp"].isoformat(),
|
||||
"key": row["key"],
|
||||
"model": row["model"],
|
||||
"status": status,
|
||||
"status_code": row["status_code"],
|
||||
"latency_ms": row["latency_ms"],
|
||||
}
|
||||
|
||||
# 如果失败,尝试附带一个相关的错误日志ID,便于前端拉取详情
|
||||
if status == "failure" and row["key"]:
|
||||
try:
|
||||
ts = row["timestamp"]
|
||||
start_win = ts - datetime.timedelta(minutes=5)
|
||||
end_win = ts + datetime.timedelta(minutes=5)
|
||||
err_query = (
|
||||
select(ErrorLog.id)
|
||||
.where(
|
||||
ErrorLog.gemini_key == row["key"],
|
||||
ErrorLog.request_time >= start_win,
|
||||
ErrorLog.request_time <= end_win,
|
||||
)
|
||||
.order_by(ErrorLog.request_time.desc())
|
||||
.limit(1)
|
||||
)
|
||||
err = await database.fetch_one(err_query)
|
||||
if err:
|
||||
record["error_log_id"] = err["id"]
|
||||
except Exception as _e:
|
||||
logger.debug(
|
||||
f"No matching error log found for key ending ...{row['key'][-4:] if row['key'] else ''}: {_e}"
|
||||
)
|
||||
|
||||
details.append(record)
|
||||
|
||||
logger.info(
|
||||
f"Retrieved {len(details)} API call details for period '{period}'"
|
||||
)
|
||||
return details
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get API call details for period '{period}': {e}")
|
||||
raise
|
||||
|
||||
async def get_key_call_details(self, key: str, period: str) -> list[dict]:
|
||||
"""获取指定密钥在指定时间段内的调用详情 (与 get_api_call_details 结构一致)"""
|
||||
now = datetime.datetime.now()
|
||||
if period == "1m":
|
||||
start_time = now - datetime.timedelta(minutes=1)
|
||||
elif period == "1h":
|
||||
start_time = now - datetime.timedelta(hours=1)
|
||||
elif period == "8h":
|
||||
start_time = now - datetime.timedelta(hours=8)
|
||||
elif period == "24h":
|
||||
start_time = now - datetime.timedelta(hours=24)
|
||||
else:
|
||||
raise ValueError(f"无效的时间段标识: {period}")
|
||||
|
||||
try:
|
||||
query = (
|
||||
select(
|
||||
RequestLog.request_time.label("timestamp"),
|
||||
RequestLog.api_key.label("key"),
|
||||
RequestLog.model_name.label("model"),
|
||||
RequestLog.status_code.label("status_code"),
|
||||
RequestLog.latency_ms.label("latency_ms"),
|
||||
)
|
||||
.where(RequestLog.request_time >= start_time, RequestLog.api_key == key)
|
||||
.order_by(RequestLog.request_time.desc())
|
||||
)
|
||||
|
||||
results = await database.fetch_all(query)
|
||||
|
||||
from app.database.models import ErrorLog
|
||||
|
||||
details: list[dict] = []
|
||||
for row in results:
|
||||
status = "failure"
|
||||
if row["status_code"] is not None:
|
||||
status = "success" if 200 <= row["status_code"] < 300 else "failure"
|
||||
|
||||
record = {
|
||||
"timestamp": row["timestamp"].isoformat(),
|
||||
"key": row["key"],
|
||||
"model": row["model"],
|
||||
"status": status,
|
||||
"status_code": row["status_code"],
|
||||
"latency_ms": row["latency_ms"],
|
||||
}
|
||||
|
||||
if status == "failure" and row["key"]:
|
||||
try:
|
||||
ts = row["timestamp"]
|
||||
start_win = ts - datetime.timedelta(minutes=5)
|
||||
end_win = ts + datetime.timedelta(minutes=5)
|
||||
err_query = (
|
||||
select(ErrorLog.id)
|
||||
.where(
|
||||
ErrorLog.gemini_key == row["key"],
|
||||
ErrorLog.request_time >= start_win,
|
||||
ErrorLog.request_time < end_win,
|
||||
)
|
||||
.order_by(ErrorLog.request_time.desc())
|
||||
.limit(1)
|
||||
)
|
||||
err = await database.fetch_one(err_query)
|
||||
if err:
|
||||
record["error_log_id"] = err["id"]
|
||||
except Exception as _e:
|
||||
logger.debug(
|
||||
f"No matching error log found for key ending ...{row['key'][-4:] if row['key'] else ''}: {_e}"
|
||||
)
|
||||
|
||||
details.append(record)
|
||||
|
||||
logger.info(
|
||||
f"Retrieved {len(details)} key call details for key=...{key[-4:] if key else ''} period '{period}'"
|
||||
)
|
||||
return details
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to get API call details for period '{period}': {e}")
|
||||
f"Failed to get key call details for key=...{key[-4:] if key else ''} period '{period}': {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
async def get_key_usage_details_last_24h(self, key: str) -> Union[dict, None]:
|
||||
@@ -220,8 +332,7 @@ class StatsService:
|
||||
try:
|
||||
query = (
|
||||
select(
|
||||
RequestLog.model_name, func.count(
|
||||
RequestLog.id).label("call_count")
|
||||
RequestLog.model_name, func.count(RequestLog.id).label("call_count")
|
||||
)
|
||||
.where(
|
||||
RequestLog.api_key == key,
|
||||
@@ -240,8 +351,7 @@ class StatsService:
|
||||
)
|
||||
return {}
|
||||
|
||||
usage_details = {row["model_name"]: row["call_count"]
|
||||
for row in results}
|
||||
usage_details = {row["model_name"]: row["call_count"] for row in results}
|
||||
logger.info(
|
||||
f"Successfully fetched usage details for key ending in ...{key[-4:]}: {usage_details}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user