forked from xiaohei/taiji-AI-PAD
417 lines
16 KiB
Python
417 lines
16 KiB
Python
"""
|
||
资源监控 API 路由
|
||
提供平台资源概览、用户资源使用汇总、资源使用趋势和Agent资源统计
|
||
"""
|
||
|
||
from typing import Optional
|
||
from datetime import datetime, timedelta
|
||
from fastapi import APIRouter, Depends, Query, HTTPException
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from sqlalchemy import select, func, and_, or_, case
|
||
from decimal import Decimal
|
||
|
||
from database import get_db
|
||
from app.auth import require_role
|
||
from models import (
|
||
User, Agent, ResourceUsage, BillingRecord,
|
||
AgentTrace, AgentBillingRecord, ModelBillingRecord
|
||
)
|
||
|
||
|
||
router = APIRouter(
|
||
prefix="/api/billing-admin/resources",
|
||
tags=["resource-monitoring"]
|
||
)
|
||
|
||
|
||
# =====================================================
|
||
# 1. 获取平台资源概览
|
||
# =====================================================
|
||
|
||
@router.get("/overview")
|
||
async def get_platform_overview(
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: dict = Depends(require_role(["super_admin", "billing_admin", "operations_admin"]))
|
||
):
|
||
"""
|
||
获取平台整体资源使用概览(管理员视图)
|
||
|
||
返回数据:
|
||
- todayCalls: 今日调用次数
|
||
- monthCalls: 本月调用次数
|
||
- activeUsersToday: 今日活跃用户数
|
||
- activeAgents: 活跃Agent数(平台端 + 自定义)
|
||
- monthTotalEu: 本月总EU消费
|
||
- platformAgents: 平台端 Agent 统计
|
||
- customAgents: 自定义 Agent 统计
|
||
|
||
**权限要求**: super_admin, billing_admin, operations_admin
|
||
"""
|
||
try:
|
||
now = datetime.utcnow()
|
||
today_start = datetime(now.year, now.month, now.day)
|
||
month_start = datetime(now.year, now.month, 1)
|
||
|
||
# 1. 今日调用次数(从resource_usage统计)
|
||
today_calls_result = await db.execute(
|
||
select(func.count(ResourceUsage.id))
|
||
.where(ResourceUsage.created_at >= today_start)
|
||
)
|
||
today_calls = today_calls_result.scalar() or 0
|
||
|
||
# 2. 本月调用次数
|
||
month_calls_result = await db.execute(
|
||
select(func.count(ResourceUsage.id))
|
||
.where(ResourceUsage.created_at >= month_start)
|
||
)
|
||
month_calls = month_calls_result.scalar() or 0
|
||
|
||
# 3. 今日活跃用户数(DISTINCT user_id)
|
||
active_users_result = await db.execute(
|
||
select(func.count(func.distinct(ResourceUsage.user_id)))
|
||
.where(ResourceUsage.created_at >= today_start)
|
||
)
|
||
active_users_today = active_users_result.scalar() or 0
|
||
|
||
# 4. 活跃Agent数(平台端 + 自定义)
|
||
# 4.1 平台端 Agent 统计
|
||
platform_agents_result = await db.execute(
|
||
select(Agent)
|
||
.where(and_(Agent.type == "platform", Agent.status == "active"))
|
||
)
|
||
platform_agents = platform_agents_result.scalars().all()
|
||
platform_agents_count = len(platform_agents)
|
||
platform_cpu = sum(float(a.cpu or 0) for a in platform_agents)
|
||
platform_memory = sum(float(a.memory or 0) for a in platform_agents)
|
||
platform_healthy = len([a for a in platform_agents if a.health_status == "healthy"])
|
||
|
||
# 4.2 自定义 Agent 统计
|
||
custom_agents_result = await db.execute(
|
||
select(Agent)
|
||
.where(and_(Agent.type == "custom", Agent.status == "active"))
|
||
)
|
||
custom_agents = custom_agents_result.scalars().all()
|
||
custom_agents_count = len(custom_agents)
|
||
custom_cpu = sum(float(a.cpu or 0) for a in custom_agents)
|
||
custom_memory = sum(float(a.memory or 0) for a in custom_agents)
|
||
custom_healthy = len([a for a in custom_agents if a.health_status == "healthy"])
|
||
|
||
# 总活跃 Agent 数
|
||
active_agents = platform_agents_count + custom_agents_count
|
||
|
||
# 5. 本月总EU消费(合并 Agent运行时间 + 模型Token 两种计费)
|
||
# 5.1 Agent 运行时间计费的 EU(从 agent_billing_records 表)
|
||
agent_eu_result = await db.execute(
|
||
select(func.sum(AgentBillingRecord.eu_consumed))
|
||
.where(AgentBillingRecord.period_start >= month_start)
|
||
)
|
||
agent_eu = agent_eu_result.scalar() or 0
|
||
|
||
# 5.2 模型 Token 计费的 EU(从 model_billing_records 表)
|
||
model_eu_result = await db.execute(
|
||
select(func.sum(ModelBillingRecord.eu_consumed))
|
||
.where(ModelBillingRecord.created_at >= month_start)
|
||
)
|
||
model_eu = model_eu_result.scalar() or 0
|
||
|
||
# 5.3 合并两种 EU 消耗
|
||
month_total_eu = float(agent_eu or 0) + float(model_eu or 0)
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"todayCalls": today_calls,
|
||
"monthCalls": month_calls,
|
||
"activeUsersToday": active_users_today,
|
||
"activeAgents": active_agents,
|
||
"monthTotalEu": round(float(month_total_eu), 2),
|
||
"timestamp": now.isoformat(),
|
||
# 平台端 Agent 统计
|
||
"platformAgents": {
|
||
"count": platform_agents_count,
|
||
"healthy": platform_healthy,
|
||
"cpu": round(platform_cpu, 2),
|
||
"memory": round(platform_memory, 2),
|
||
},
|
||
# 自定义 Agent 统计
|
||
"customAgents": {
|
||
"count": custom_agents_count,
|
||
"healthy": custom_healthy,
|
||
"cpu": round(custom_cpu, 2),
|
||
"memory": round(custom_memory, 2),
|
||
},
|
||
# 总资源使用
|
||
"totalResources": {
|
||
"cpu": round(platform_cpu + custom_cpu, 2),
|
||
"memory": round(platform_memory + custom_memory, 2),
|
||
},
|
||
}
|
||
}
|
||
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"获取平台资源概览失败: {str(e)}")
|
||
|
||
|
||
# =====================================================
|
||
# 2. 获取用户资源使用汇总
|
||
# =====================================================
|
||
|
||
@router.get("/user/{user_id}")
|
||
async def get_user_resource_summary(
|
||
user_id: str,
|
||
start_date: str = Query(..., description="开始日期 (ISO 8601格式)"),
|
||
end_date: str = Query(..., description="结束日期 (ISO 8601格式)"),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: dict = Depends(require_role(["super_admin", "billing_admin", "operations_admin"]))
|
||
):
|
||
"""
|
||
获取指定用户在时间范围内的资源使用汇总
|
||
|
||
返回数据:
|
||
- totalCpuSeconds: CPU总使用时间(秒)
|
||
- totalMemoryMbSeconds: 内存总使用量(MB·秒)
|
||
- totalNetworkBytes: 网络总流量(字节)
|
||
- totalStorageBytes: 存储总使用量(字节)
|
||
- totalApiCalls: API总调用次数
|
||
|
||
**权限要求**: super_admin, billing_admin, operations_admin
|
||
"""
|
||
try:
|
||
# 解析日期
|
||
start_dt = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
|
||
end_dt = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
|
||
|
||
# 验证用户存在
|
||
user_result = await db.execute(
|
||
select(User).where(User.id == user_id)
|
||
)
|
||
user = user_result.scalar_one_or_none()
|
||
if not user:
|
||
raise HTTPException(status_code=404, detail="用户不存在")
|
||
|
||
# 聚合ResourceUsage表
|
||
usage_result = await db.execute(
|
||
select(
|
||
func.sum(ResourceUsage.cpu_seconds).label("total_cpu"),
|
||
func.sum(ResourceUsage.memory_mb_seconds).label("total_memory"),
|
||
func.sum(ResourceUsage.network_bytes).label("total_network"),
|
||
func.sum(ResourceUsage.storage_bytes).label("total_storage"),
|
||
func.count(ResourceUsage.id).label("total_calls")
|
||
)
|
||
.where(
|
||
and_(
|
||
ResourceUsage.user_id == user_id,
|
||
ResourceUsage.created_at >= start_dt,
|
||
ResourceUsage.created_at <= end_dt
|
||
)
|
||
)
|
||
)
|
||
usage = usage_result.first()
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"totalCpuSeconds": round(float(usage.total_cpu or 0), 2),
|
||
"totalMemoryMbSeconds": round(float(usage.total_memory or 0), 2),
|
||
"totalNetworkBytes": int(usage.total_network or 0),
|
||
"totalStorageBytes": int(usage.total_storage or 0),
|
||
"totalApiCalls": int(usage.total_calls or 0),
|
||
"startDate": start_dt.isoformat(),
|
||
"endDate": end_dt.isoformat()
|
||
}
|
||
}
|
||
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=400, detail=f"日期格式错误: {str(e)}")
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"获取用户资源使用汇总失败: {str(e)}")
|
||
|
||
|
||
# =====================================================
|
||
# 3. 获取资源使用趋势
|
||
# =====================================================
|
||
|
||
@router.get("/trends")
|
||
async def get_resource_trends(
|
||
user_id: str = Query(..., description="用户ID"),
|
||
period: str = Query("7d", regex="^(7d|30d|90d)$", description="时间范围: 7d, 30d, 90d"),
|
||
granularity: str = Query("daily", regex="^(hourly|daily)$", description="粒度: hourly, daily"),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: dict = Depends(require_role(["super_admin", "billing_admin", "operations_admin"]))
|
||
):
|
||
"""
|
||
获取用户资源使用趋势
|
||
|
||
**查询参数**:
|
||
- user_id: 用户ID
|
||
- period: 时间范围 (7d, 30d, 90d)
|
||
- granularity: 粒度 (hourly, daily)
|
||
|
||
**权限要求**: super_admin, billing_admin, operations_admin
|
||
"""
|
||
try:
|
||
# 计算时间范围
|
||
now = datetime.utcnow()
|
||
period_days = {
|
||
"7d": 7,
|
||
"30d": 30,
|
||
"90d": 90
|
||
}
|
||
start_date = now - timedelta(days=period_days[period])
|
||
|
||
# 验证用户存在
|
||
user_result = await db.execute(
|
||
select(User).where(User.id == user_id)
|
||
)
|
||
user = user_result.scalar_one_or_none()
|
||
if not user:
|
||
raise HTTPException(status_code=404, detail="用户不存在")
|
||
|
||
# 根据粒度查询
|
||
if granularity == "hourly":
|
||
# PostgreSQL: DATE_TRUNC('hour', created_at)
|
||
time_bucket = func.date_trunc('hour', ResourceUsage.created_at)
|
||
else: # daily
|
||
# PostgreSQL: DATE(created_at)
|
||
time_bucket = func.date(ResourceUsage.created_at)
|
||
|
||
# 聚合查询
|
||
trends_result = await db.execute(
|
||
select(
|
||
time_bucket.label("period_start"),
|
||
func.sum(ResourceUsage.cpu_seconds).label("cpu_seconds"),
|
||
func.sum(ResourceUsage.memory_mb_seconds).label("memory_mb_seconds"),
|
||
func.sum(ResourceUsage.network_bytes).label("network_bytes"),
|
||
func.count(ResourceUsage.id).label("api_calls")
|
||
)
|
||
.where(
|
||
and_(
|
||
ResourceUsage.user_id == user_id,
|
||
ResourceUsage.created_at >= start_date
|
||
)
|
||
)
|
||
.group_by(time_bucket)
|
||
.order_by(time_bucket)
|
||
)
|
||
|
||
trends = trends_result.all()
|
||
|
||
# 构造响应
|
||
trends_data = []
|
||
for trend in trends:
|
||
period_start = trend.period_start
|
||
|
||
# 计算period_end
|
||
if granularity == "hourly":
|
||
period_end = period_start + timedelta(hours=1) - timedelta(seconds=1)
|
||
else:
|
||
period_end = period_start + timedelta(days=1) - timedelta(seconds=1)
|
||
|
||
trends_data.append({
|
||
"periodStart": period_start.isoformat() if hasattr(period_start, 'isoformat') else str(period_start),
|
||
"periodEnd": period_end.isoformat(),
|
||
"cpuSeconds": round(float(trend.cpu_seconds or 0), 2),
|
||
"memoryMbSeconds": round(float(trend.memory_mb_seconds or 0), 2),
|
||
"networkBytes": int(trend.network_bytes or 0),
|
||
"apiCalls": int(trend.api_calls or 0)
|
||
})
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"trends": trends_data
|
||
}
|
||
}
|
||
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"获取资源使用趋势失败: {str(e)}")
|
||
|
||
|
||
# =====================================================
|
||
# 4. 获取Agent资源统计
|
||
# =====================================================
|
||
|
||
@router.get("/agent/{agent_id}")
|
||
async def get_agent_resource_stats(
|
||
agent_id: str,
|
||
start_date: str = Query(..., description="开始日期 (ISO 8601格式)"),
|
||
end_date: str = Query(..., description="结束日期 (ISO 8601格式)"),
|
||
db: AsyncSession = Depends(get_db),
|
||
current_user: dict = Depends(require_role(["super_admin", "billing_admin", "operations_admin"]))
|
||
):
|
||
"""
|
||
获取指定Agent在时间范围内的资源统计
|
||
|
||
返回数据:
|
||
- agentId: Agent ID
|
||
- totalExecutions: 总执行次数
|
||
- avgExecutionTime: 平均执行时间(毫秒)
|
||
- totalEuConsumed: 总EU消耗
|
||
- successRate: 成功率(百分比)
|
||
|
||
**权限要求**: super_admin, billing_admin, operations_admin
|
||
"""
|
||
try:
|
||
# 解析日期
|
||
start_dt = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
|
||
end_dt = datetime.fromisoformat(end_date.replace('Z', '+00:00'))
|
||
|
||
# 验证Agent存在
|
||
agent_result = await db.execute(
|
||
select(Agent).where(Agent.id == agent_id)
|
||
)
|
||
agent = agent_result.scalar_one_or_none()
|
||
if not agent:
|
||
raise HTTPException(status_code=404, detail="Agent不存在")
|
||
|
||
# 从AgentTrace表统计
|
||
stats_result = await db.execute(
|
||
select(
|
||
func.count(AgentTrace.id).label("total_executions"),
|
||
func.avg(AgentTrace.duration_ms).label("avg_duration"),
|
||
func.sum(AgentTrace.eu_consumed).label("total_eu"), # 修复:使用正确的字段名 eu_consumed
|
||
func.sum(
|
||
func.case(
|
||
(AgentTrace.status == "success", 1),
|
||
else_=0
|
||
)
|
||
).label("success_count")
|
||
)
|
||
.where(
|
||
and_(
|
||
AgentTrace.agent_id == agent_id,
|
||
AgentTrace.created_at >= start_dt,
|
||
AgentTrace.created_at <= end_dt
|
||
)
|
||
)
|
||
)
|
||
stats = stats_result.first()
|
||
|
||
total_executions = int(stats.total_executions or 0)
|
||
success_count = int(stats.success_count or 0)
|
||
success_rate = (success_count / total_executions * 100) if total_executions > 0 else 0
|
||
|
||
return {
|
||
"success": True,
|
||
"data": {
|
||
"agentId": agent_id,
|
||
"totalExecutions": total_executions,
|
||
"avgExecutionTime": round(float(stats.avg_duration or 0), 2),
|
||
"totalEuConsumed": round(float(stats.total_eu or 0), 2),
|
||
"successRate": round(success_rate, 2),
|
||
"startDate": start_dt.isoformat(),
|
||
"endDate": end_dt.isoformat()
|
||
}
|
||
}
|
||
|
||
except ValueError as e:
|
||
raise HTTPException(status_code=400, detail=f"日期格式错误: {str(e)}")
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"获取Agent资源统计失败: {str(e)}")
|