mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-05-31 05:09:46 +08:00
新增功能允许用户在 Keys 状态页面点击“详情”按钮,查看指定 API 密钥在过去 24 小时内按模型分类的请求次数统计。
主要变更包括:
后端:
- 新增 `app/router/stats_routes.py`,包含 `/api/key-usage-details/{key}` API 端点用于获取密钥使用详情。
- 重构 `app/service/stats_service.py`,将统计相关函数封装到 `StatsService` 类中,并添加 `get_key_usage_details_last_24h` 方法。
- 在 `app/router/routes.py` 中注册新的 `stats_routes`,并更新对 `stats_service` 的调用方式以使用类实例。
- 更新 `app/log/logger.py` 添加 `get_scheduler_routes` 日志记录器,并在 `app/router/scheduler_routes.py` 中使用它。
前端:
- 在 `app/templates/keys_status.html` 中为每个有效和无效密钥列表项添加“详情”按钮。
- 在 `app/templates/keys_status.html` 中添加用于显示密钥使用详情的模态框 HTML 结构。
- 在 `app/static/js/keys_status.js` 中添加 JavaScript 函数 (`showKeyUsageDetails`, `closeKeyUsageDetailsModal`, `renderKeyUsageDetails`) 来处理按钮点击事件、调用后端 API、控制模态框显示/隐藏以及渲染获取到的统计数据。
174 lines
7.1 KiB
Python
174 lines
7.1 KiB
Python
# app/service/stats_service.py
|
||
|
||
import datetime
|
||
from sqlalchemy import select, func
|
||
|
||
from app.database.connection import database
|
||
from app.database.models import RequestLog
|
||
from app.log.logger import get_stats_logger
|
||
|
||
logger = get_stats_logger()
|
||
|
||
|
||
class StatsService:
|
||
"""Service class for handling statistics related operations."""
|
||
|
||
async def get_calls_in_last_seconds(self, seconds: int) -> int:
|
||
"""获取过去 N 秒内的调用次数 (包括成功和失败)"""
|
||
try:
|
||
cutoff_time = datetime.datetime.now() - datetime.timedelta(seconds=seconds)
|
||
query = select(func.count(RequestLog.id)).where(
|
||
RequestLog.request_time >= cutoff_time
|
||
)
|
||
count_result = await database.fetch_one(query)
|
||
return count_result[0] if count_result else 0
|
||
except Exception as e:
|
||
logger.error(f"Failed to get calls in last {seconds} seconds: {e}")
|
||
return 0 # Return 0 on error
|
||
|
||
async def get_calls_in_last_minutes(self, minutes: int) -> int:
|
||
"""获取过去 N 分钟内的调用次数 (包括成功和失败)"""
|
||
return await self.get_calls_in_last_seconds(minutes * 60)
|
||
|
||
async def get_calls_in_last_hours(self, hours: int) -> int:
|
||
"""获取过去 N 小时内的调用次数 (包括成功和失败)"""
|
||
return await self.get_calls_in_last_seconds(hours * 3600)
|
||
|
||
async def get_calls_in_current_month(self) -> int:
|
||
"""获取当前自然月内的调用次数 (包括成功和失败)"""
|
||
try:
|
||
now = datetime.datetime.now()
|
||
start_of_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||
query = select(func.count(RequestLog.id)).where(
|
||
RequestLog.request_time >= start_of_month
|
||
)
|
||
count_result = await database.fetch_one(query)
|
||
return count_result[0] if count_result else 0
|
||
except Exception as e:
|
||
logger.error(f"Failed to get calls in current month: {e}")
|
||
return 0 # Return 0 on error
|
||
|
||
async def get_api_usage_stats(self) -> dict:
|
||
"""获取所有需要的 API 使用统计数据"""
|
||
try:
|
||
calls_1m = await self.get_calls_in_last_minutes(1)
|
||
calls_1h = await self.get_calls_in_last_hours(1)
|
||
calls_24h = await self.get_calls_in_last_hours(24)
|
||
calls_month = await self.get_calls_in_current_month()
|
||
|
||
return {
|
||
"calls_1m": calls_1m,
|
||
"calls_1h": calls_1h,
|
||
"calls_24h": calls_24h,
|
||
"calls_month": calls_month,
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"Failed to get API usage stats: {e}")
|
||
# Return default values on error
|
||
return {
|
||
"calls_1m": 0,
|
||
"calls_1h": 0,
|
||
"calls_24h": 0,
|
||
"calls_month": 0,
|
||
}
|
||
|
||
|
||
async def get_api_call_details(self, period: str) -> list[dict]:
|
||
"""
|
||
获取指定时间段内的 API 调用详情
|
||
|
||
Args:
|
||
period: 时间段标识 ('1m', '1h', '24h')
|
||
|
||
Returns:
|
||
包含调用详情的字典列表,每个字典包含 timestamp, key, model, status
|
||
|
||
Raises:
|
||
ValueError: 如果 period 无效
|
||
"""
|
||
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 == '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 # We might need to map this to 'success'/'failure' later
|
||
).where(
|
||
RequestLog.request_time >= start_time
|
||
).order_by(RequestLog.request_time.desc()) # Order by most recent first
|
||
|
||
results = await database.fetch_all(query)
|
||
|
||
# Convert results to list of dicts and map status_code
|
||
details = []
|
||
for row in results:
|
||
status = 'failure' # 默认状态为 failure,如果 status_code 有效且在 200-299 范围内则更新为 success
|
||
if row['status_code'] is not None: # 检查 status_code 是否为空
|
||
status = 'success' if 200 <= row['status_code'] < 300 else 'failure'
|
||
details.append({
|
||
"timestamp": row['timestamp'].isoformat(), # Use ISO format for JS compatibility
|
||
"key": row['key'],
|
||
"model": row['model'],
|
||
"status": status
|
||
})
|
||
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}")
|
||
# Re-raise the exception to be handled by the route
|
||
raise
|
||
|
||
async def get_key_usage_details_last_24h(self, key: str) -> dict | None:
|
||
"""
|
||
获取指定 API 密钥在过去 24 小时内按模型统计的调用次数。
|
||
|
||
Args:
|
||
key: 要查询的 API 密钥。
|
||
|
||
Returns:
|
||
一个字典,其中键是模型名称,值是调用次数。
|
||
如果查询出错或没有找到记录,可能返回 None 或空字典。
|
||
Example: {"gemini-pro": 10, "gemini-1.5-pro-latest": 5}
|
||
"""
|
||
logger.info(f"Fetching usage details for key ending in ...{key[-4:]} for the last 24h.")
|
||
cutoff_time = datetime.datetime.now() - datetime.timedelta(hours=24)
|
||
|
||
try:
|
||
query = select(
|
||
RequestLog.model_name,
|
||
func.count(RequestLog.id).label("call_count")
|
||
).where(
|
||
RequestLog.api_key == key,
|
||
RequestLog.request_time >= cutoff_time,
|
||
RequestLog.model_name.isnot(None) # Ensure model_name is not null
|
||
).group_by(
|
||
RequestLog.model_name
|
||
).order_by(
|
||
func.count(RequestLog.id).desc() # Order by count descending
|
||
)
|
||
|
||
results = await database.fetch_all(query)
|
||
|
||
if not results:
|
||
logger.info(f"No usage details found for key ending in ...{key[-4:]} in the last 24h.")
|
||
return {} # Return empty dict if no records found
|
||
|
||
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}")
|
||
return usage_details
|
||
|
||
except Exception as e:
|
||
logger.error(f"Failed to get key usage details for key ending in ...{key[-4:]}: {e}", exc_info=True)
|
||
# Depending on requirements, you might return None or raise the exception
|
||
# Raising allows the route handler to return a 500 error.
|
||
raise # Re-raise the exception |