Files
Aether/_deprecated_py_src/services/health/endpoint.py

493 lines
18 KiB
Python
Raw Normal View History

2025-12-10 20:52:44 +08:00
"""
端点健康状态服务
提供统一的端点健康监控功能支持
1. API 格式聚合的健康状态
2. 基于时间窗口的状态追踪
3. 管理员和普通用户的差异化视图
4. Redis 缓存优化
"""
from __future__ import annotations
2025-12-10 20:52:44 +08:00
import json
from collections import defaultdict
from datetime import datetime, timedelta, timezone
from typing import Any
2025-12-10 20:52:44 +08:00
from sqlalchemy import case, func
from sqlalchemy.orm import Session, load_only
2025-12-10 20:52:44 +08:00
from src.core.logger import logger
from src.models.database import Provider, ProviderAPIKey, ProviderEndpoint, RequestCandidate
# 缓存配置
CACHE_TTL_SECONDS = 30 # 缓存 30 秒
CACHE_KEY_PREFIX = "health:endpoint:"
def _get_redis_client() -> Any:
2025-12-10 20:52:44 +08:00
"""获取 Redis 客户端,失败返回 None"""
try:
from src.clients.redis_client import redis_client
2025-12-10 20:52:44 +08:00
return redis_client
except Exception:
return None
class EndpointHealthService:
"""端点健康状态服务"""
@staticmethod
def get_endpoint_health_by_format(
db: Session,
lookback_hours: int = 6,
include_admin_fields: bool = False,
use_cache: bool = True,
) -> list[dict[str, Any]]:
2025-12-10 20:52:44 +08:00
"""
获取按 API 格式聚合的端点健康状态
Args:
db: 数据库会话
lookback_hours: 回溯小时数
include_admin_fields: 是否包含管理员字段provider_count, key_count等
use_cache: 是否使用缓存仅对普通用户视图有效
Returns:
API 格式聚合的健康状态列表
"""
# 尝试从缓存获取
cache_key = f"{CACHE_KEY_PREFIX}format:{lookback_hours}:{include_admin_fields}"
if use_cache:
cached = EndpointHealthService._get_from_cache(cache_key)
if cached is not None:
return cached
now = datetime.now(timezone.utc)
# 查询所有活跃的端点(一次性获取所有需要的数据)
endpoints = (
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
db.query(ProviderEndpoint)
.join(Provider)
.filter(
ProviderEndpoint.is_active.is_(True),
Provider.is_active.is_(True),
)
.all()
2025-12-10 20:52:44 +08:00
)
# 收集所有 provider_ids
all_provider_ids = list({ep.provider_id for ep in endpoints})
2025-12-10 20:52:44 +08:00
# 批量查询所有密钥(通过 provider_id 关联,只加载分组和统计所需列)
2025-12-10 20:52:44 +08:00
all_keys = (
(
db.query(ProviderAPIKey)
.filter(ProviderAPIKey.provider_id.in_(all_provider_ids))
.options(
load_only(
ProviderAPIKey.id,
ProviderAPIKey.provider_id,
ProviderAPIKey.is_active,
ProviderAPIKey.api_formats,
ProviderAPIKey.health_by_format,
ProviderAPIKey.circuit_breaker_by_format,
)
)
.all()
)
if all_provider_ids
else []
)
2025-12-10 20:52:44 +08:00
# 按 api_format 分组密钥(通过 api_formats 字段)
keys_by_format: dict[str, list[ProviderAPIKey]] = defaultdict(list)
2025-12-10 20:52:44 +08:00
for key in all_keys:
for fmt in key.api_formats or []:
keys_by_format[fmt].append(key)
2025-12-10 20:52:44 +08:00
# 按 API 格式聚合
format_stats = defaultdict(
lambda: {
"total_endpoints": 0,
"total_keys": 0,
"active_keys": 0,
"health_scores": [],
"endpoint_ids": [],
"provider_ids": set(),
"key_ids": [],
}
)
for ep in endpoints:
api_format = ep.api_format if ep.api_format else "UNKNOWN"
# 统计端点数
format_stats[api_format]["total_endpoints"] += 1
format_stats[api_format]["endpoint_ids"].append(ep.id)
format_stats[api_format]["provider_ids"].add(ep.provider_id)
# 统计每个格式的密钥(直接从 keys_by_format 获取)
for api_format, keys in keys_by_format.items():
if api_format not in format_stats:
# 如果有 Key 但没有对应的 Endpoint跳过
continue
# 去重(同一个 Key 可能支持多个格式)
seen_key_ids = set()
unique_keys = []
for key in keys:
if key.id not in seen_key_ids:
seen_key_ids.add(key.id)
unique_keys.append(key)
format_stats[api_format]["total_keys"] = len(unique_keys)
for key in unique_keys:
format_stats[api_format]["key_ids"].append(key.id)
# 检查该格式的熔断器状态
circuit_by_format = key.circuit_breaker_by_format or {}
format_circuit = circuit_by_format.get(api_format, {})
is_circuit_open = format_circuit.get("open", False)
if key.is_active and not is_circuit_open:
format_stats[api_format]["active_keys"] += 1
# 获取该格式的健康度
health_by_format = key.health_by_format or {}
format_health = health_by_format.get(api_format, {})
health_score = float(format_health.get("health_score") or 1.0)
format_stats[api_format]["health_scores"].append(health_score)
2025-12-10 20:52:44 +08:00
# 批量生成所有格式的时间线数据
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
format_endpoint_mapping: dict[str, list[str]] = {}
2025-12-10 20:52:44 +08:00
for api_format, stats in format_stats.items():
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
format_endpoint_mapping[api_format] = stats["endpoint_ids"]
2025-12-10 20:52:44 +08:00
# 一次性查询所有时间线数据
timeline_data_map = EndpointHealthService._generate_timeline_batch(
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
db, format_endpoint_mapping, now, lookback_hours
2025-12-10 20:52:44 +08:00
)
# 生成结果
result = []
for api_format, stats in format_stats.items():
timeline_data = timeline_data_map.get(
api_format,
{
"timeline": ["unknown"] * 100,
"time_range_start": None,
"time_range_end": None,
},
)
2025-12-10 20:52:44 +08:00
timeline = timeline_data["timeline"]
time_range_start = timeline_data.get("time_range_start")
time_range_end = timeline_data.get("time_range_end")
# 基于时间线计算实际健康度
if timeline:
healthy_count = sum(1 for status in timeline if status == "healthy")
warning_count = sum(1 for status in timeline if status == "warning")
unhealthy_count = sum(1 for status in timeline if status == "unhealthy")
known_count = healthy_count + warning_count + unhealthy_count
if known_count > 0:
avg_health = (healthy_count * 1.0 + warning_count * 0.8) / known_count
else:
if stats["health_scores"]:
avg_health = sum(stats["health_scores"]) / len(stats["health_scores"])
elif stats["total_keys"] == 0:
avg_health = 0.0
else:
avg_health = 0.1
else:
avg_health = 0.0
item = {
"api_format": api_format,
"display_name": EndpointHealthService._format_display_name(api_format),
"health_score": avg_health,
"timeline": timeline,
"time_range_start": time_range_start.isoformat() if time_range_start else None,
"time_range_end": time_range_end.isoformat() if time_range_end else None,
}
if include_admin_fields:
item.update(
{
"total_endpoints": stats["total_endpoints"],
"total_keys": stats["total_keys"],
"active_keys": stats["active_keys"],
"provider_count": len(stats["provider_ids"]),
}
)
result.append(item)
result.sort(key=lambda x: x["health_score"], reverse=True)
# 写入缓存
if use_cache:
EndpointHealthService._set_to_cache(cache_key, result)
return result
@staticmethod
def _generate_timeline_batch(
db: Session,
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
format_endpoint_mapping: dict[str, list[str]],
2025-12-10 20:52:44 +08:00
now: datetime,
lookback_hours: int,
segments: int = 100,
) -> dict[str, dict[str, Any]]:
2025-12-10 20:52:44 +08:00
"""
批量生成多个 API 格式的时间线数据基于 RequestCandidate
使用 RequestCandidate 表可以
1. 记录所有尝试包括 fallback 中失败的尝试
2. 准确反映每个 Provider/Key 的真实健康状态
3. 失败的请求会显示为红色节点
Args:
db: 数据库会话
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
format_endpoint_mapping: API格式 -> endpoint_ids 的映射
2025-12-10 20:52:44 +08:00
now: 当前时间
lookback_hours: 回溯小时数
segments: 时间段数量
Returns:
API格式 -> 时间线数据的映射
"""
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
# 收集所有 endpoint_ids
all_endpoint_ids = []
for endpoint_ids in format_endpoint_mapping.values():
all_endpoint_ids.extend(endpoint_ids)
2025-12-10 20:52:44 +08:00
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
if not all_endpoint_ids:
2025-12-10 20:52:44 +08:00
return {
api_format: {
"timeline": ["unknown"] * 100,
"time_range_start": None,
"time_range_end": None,
}
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
for api_format in format_endpoint_mapping.keys()
2025-12-10 20:52:44 +08:00
}
# 参数校验API 层已通过 Query(ge=1) 保证,这里做防御性检查)
if lookback_hours <= 0 or segments <= 0:
raise ValueError(
f"lookback_hours and segments must be positive, "
f"got lookback_hours={lookback_hours}, segments={segments}"
)
# 计算时间范围
segment_seconds = (lookback_hours * 3600) / segments
2025-12-10 20:52:44 +08:00
start_time = now - timedelta(hours=lookback_hours)
# 使用 RequestCandidate 表查询所有尝试记录
# 只统计最终状态success, failed, skipped
final_statuses = ["success", "failed", "skipped"]
segment_expr = func.floor(
func.extract("epoch", RequestCandidate.created_at - start_time) / segment_seconds
).label("segment_idx")
2025-12-10 20:52:44 +08:00
candidate_stats = (
db.query(
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
RequestCandidate.endpoint_id,
2025-12-10 20:52:44 +08:00
segment_expr,
func.count(RequestCandidate.id).label("total_count"),
func.sum(case((RequestCandidate.status == "success", 1), else_=0)).label(
"success_count"
),
func.sum(case((RequestCandidate.status == "failed", 1), else_=0)).label(
"failed_count"
),
func.min(RequestCandidate.created_at).label("min_time"),
func.max(RequestCandidate.created_at).label("max_time"),
2025-12-10 20:52:44 +08:00
)
.filter(
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
RequestCandidate.endpoint_id.in_(all_endpoint_ids),
2025-12-10 20:52:44 +08:00
RequestCandidate.created_at >= start_time,
RequestCandidate.created_at <= now,
RequestCandidate.status.in_(final_statuses),
)
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
.group_by(RequestCandidate.endpoint_id, segment_expr)
2025-12-10 20:52:44 +08:00
.all()
)
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
# 构建 endpoint_id -> api_format 的反向映射
endpoint_to_format: dict[str, str] = {}
for api_format, endpoint_ids in format_endpoint_mapping.items():
for endpoint_id in endpoint_ids:
endpoint_to_format[endpoint_id] = api_format
2025-12-10 20:52:44 +08:00
# 按 api_format 和 segment 聚合数据
format_segment_data: dict[str, dict[int, dict]] = defaultdict(
lambda: defaultdict(
lambda: {
"total": 0,
"success": 0,
"failed": 0,
"min_time": None,
"max_time": None,
}
)
)
2025-12-10 20:52:44 +08:00
for row in candidate_stats:
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
endpoint_id = row.endpoint_id
2025-12-10 20:52:44 +08:00
segment_idx = int(row.segment_idx) if row.segment_idx is not None else 0
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
api_format = endpoint_to_format.get(endpoint_id)
2025-12-10 20:52:44 +08:00
if api_format and 0 <= segment_idx < segments:
seg_data = format_segment_data[api_format][segment_idx]
seg_data["total"] += row.total_count or 0
seg_data["success"] += row.success_count or 0
seg_data["failed"] += row.failed_count or 0
if row.min_time:
if seg_data["min_time"] is None or row.min_time < seg_data["min_time"]:
seg_data["min_time"] = row.min_time
if row.max_time:
if seg_data["max_time"] is None or row.max_time > seg_data["max_time"]:
seg_data["max_time"] = row.max_time
# 生成各格式的时间线
result: dict[str, dict[str, Any]] = {}
2025-12-10 20:52:44 +08:00
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
for api_format in format_endpoint_mapping.keys():
2025-12-10 20:52:44 +08:00
timeline = []
earliest_time = None
latest_time = None
segment_data = format_segment_data.get(api_format, {})
for i in range(segments):
seg = segment_data.get(i)
if not seg or seg["total"] == 0:
timeline.append("unknown")
else:
# 更新时间范围
if seg["min_time"]:
if earliest_time is None or seg["min_time"] < earliest_time:
earliest_time = seg["min_time"]
if seg["max_time"]:
if latest_time is None or seg["max_time"] > latest_time:
latest_time = seg["max_time"]
# 计算成功率 = success / (success + failed)
# skipped 不算失败,不影响成功率
actual_completed = seg["success"] + seg["failed"]
if actual_completed > 0:
success_rate = seg["success"] / actual_completed
else:
# 只有 skipped视为健康
success_rate = 1.0
if success_rate >= 0.95:
timeline.append("healthy")
elif success_rate >= 0.7:
timeline.append("warning")
else:
timeline.append("unhealthy")
result[api_format] = {
"timeline": timeline,
"time_range_start": earliest_time,
"time_range_end": latest_time if latest_time else now,
}
return result
@staticmethod
def _generate_timeline_from_usage(
db: Session,
endpoint_ids: list[str],
2025-12-10 20:52:44 +08:00
now: datetime,
lookback_hours: int,
segments: int = 100,
) -> dict[str, Any]:
2025-12-10 20:52:44 +08:00
"""
从真实使用记录生成时间线数据使用批量查询优化
2025-12-10 20:52:44 +08:00
Args:
db: 数据库会话
endpoint_ids: 端点ID列表
now: 当前时间
lookback_hours: 回溯小时数
segments: 时间段数量
Returns:
包含时间线和时间范围的字典
"""
if not endpoint_ids:
return {
"timeline": ["unknown"] * 100,
"time_range_start": None,
"time_range_end": None,
}
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
# 直接按 endpoint_id 聚合,避免共享 Key 时把不同 API 格式串桶。
format_endpoint_mapping = {"_single": endpoint_ids}
2025-12-10 20:52:44 +08:00
result = EndpointHealthService._generate_timeline_batch(
feat: 流式空闲超时、健康监控查询优化、限流桶内存上限与维护清理修复 Close #233 Co-authored-by: AAEE86 <ppk0227@hotmail.com> - cli_monitor_mixin: 引入 STREAM_IDLE_TIMEOUT_SECONDS(可通过环境变量配置), 流传输开始后若超出空闲窗口无新 chunk 则提前取消并返回 504,避免长时间挂起 - stream_context: 新增 managed_recorded_bodies 上下文管理器,确保 chunks 在 telemetry 完成后及时释放;stream_telemetry 使用该接口统一管理 response body 构建 - health endpoint: 将状态聚合改为 GROUP BY 直接统计,事件列表按 api_format 单独查询,避免单次 limit 拉取大量记录导致的遗漏与性能问题;同时过滤不活跃 provider/endpoint,与公开健康接口保持一致 - endpoint health service: 修正时间线数据按 endpoint_id 而非 key_id 聚合 - token_bucket: 引入 max_buckets/bucket_expiry 上限与定时清理,防止内存无限增长; 修复 refill_rate=0 时 get_reset_time 除零异常;新增 _is_unlimited_rate_limit 判断 - maintenance_scheduler: 调整清理顺序(先删整行再按窗口清理),新增 newer_than 边界参数,避免同一行在同一轮中被重复改写 - sync_execute: 新增 create_pending_usage 开关,允许已预创建记录的调用方跳过重复创建 - quota_reader / provider_ops balance: 小幅修复与健壮性提升 - Dockerfile: 添加 MALLOC_ARENA_MAX=2 环境变量以降低 gunicorn worker RSS - 补充相关测试覆盖
2026-03-18 23:38:26 +08:00
db, format_endpoint_mapping, now, lookback_hours, segments
2025-12-10 20:52:44 +08:00
)
return result.get(
"_single",
{
"timeline": ["unknown"] * 100,
"time_range_start": None,
"time_range_end": None,
},
)
2025-12-10 20:52:44 +08:00
@staticmethod
def _format_display_name(api_format: str) -> str:
"""格式化 API 格式的显示名称"""
raw = str(api_format or "").strip()
normalized = raw.lower()
if ":" not in normalized:
return raw or api_format
fam, kind = normalized.split(":", 1)
fam_label = {"claude": "Claude", "openai": "OpenAI", "gemini": "Gemini"}.get(fam, fam)
kind_label = {
"chat": "Chat",
"cli": "CLI",
"compact": "Compact",
"video": "Video",
"image": "Image",
}.get(kind, kind)
return f"{fam_label} {kind_label}"
2025-12-10 20:52:44 +08:00
@staticmethod
def _get_from_cache(key: str) -> list[dict[str, Any]] | None:
2025-12-10 20:52:44 +08:00
"""从 Redis 缓存获取数据"""
redis_client = _get_redis_client()
if not redis_client:
return None
try:
data = redis_client.get(key)
if data:
return json.loads(data)
except Exception as e:
logger.warning(f"Failed to get from cache: {e}")
return None
@staticmethod
def _set_to_cache(key: str, data: list[dict[str, Any]]) -> None:
2025-12-10 20:52:44 +08:00
"""写入 Redis 缓存"""
redis_client = _get_redis_client()
if not redis_client:
return
try:
redis_client.setex(key, CACHE_TTL_SECONDS, json.dumps(data, default=str))
except Exception as e:
logger.warning(f"Failed to set cache: {e}")