mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/ - 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层 - 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构 - 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations) - 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image - 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
238 lines
8.7 KiB
Python
238 lines
8.7 KiB
Python
"""普通用户可访问的监控与审计端点。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from datetime import datetime, timedelta, timezone
|
||
from typing import Any
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||
from sqlalchemy.orm import Session
|
||
|
||
from src.api.base.adapter import ApiAdapter, ApiMode
|
||
from src.api.base.context import ApiRequestContext
|
||
from src.api.base.pagination import PaginationMeta, build_pagination_payload, paginate_query
|
||
from src.api.base.pipeline import get_pipeline
|
||
from src.core.logger import logger
|
||
from src.database import get_db
|
||
from src.models.database import ApiKey, AuditLog
|
||
from src.services.rate_limit.user_rpm_limiter import SYSTEM_RPM_CONFIG_KEY, get_user_rpm_limiter
|
||
from src.services.system.config import SystemConfigService
|
||
|
||
router = APIRouter(prefix="/api/monitoring", tags=["Monitoring"])
|
||
pipeline = get_pipeline()
|
||
|
||
|
||
@router.get("/my-audit-logs")
|
||
async def get_my_audit_logs(
|
||
request: Request,
|
||
event_type: str | None = Query(None, description="事件类型筛选"),
|
||
days: int = Query(30, description="查询天数"),
|
||
limit: int = Query(50, description="返回数量限制"),
|
||
offset: int = Query(0, ge=0, description="偏移量"),
|
||
db: Session = Depends(get_db),
|
||
) -> Any:
|
||
"""
|
||
获取我的审计日志
|
||
|
||
获取当前用户的审计日志记录。需要登录。
|
||
|
||
**查询参数**:
|
||
- `event_type`: 可选,事件类型筛选
|
||
- `days`: 查询最近多少天的日志,默认 30 天
|
||
- `limit`: 返回数量限制,默认 50
|
||
- `offset`: 分页偏移量,默认 0
|
||
|
||
**返回字段**:
|
||
- `items`: 审计日志列表,每条日志包含:
|
||
- `id`: 日志 ID
|
||
- `event_type`: 事件类型
|
||
- `description`: 事件描述
|
||
- `ip_address`: IP 地址
|
||
- `status_code`: HTTP 状态码
|
||
- `created_at`: 创建时间
|
||
- `meta`: 分页元数据(total, limit, offset, count)
|
||
- `filters`: 筛选条件
|
||
"""
|
||
adapter = UserAuditLogsAdapter(event_type=event_type, days=days, limit=limit, offset=offset)
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
@router.get("/rate-limit-status")
|
||
async def get_rate_limit_status(request: Request, db: Session = Depends(get_db)) -> Any:
|
||
"""
|
||
获取速率限制状态
|
||
|
||
获取当前用户所有活跃 API Key 的速率限制状态。需要登录。
|
||
|
||
**返回字段**:
|
||
- `user_id`: 用户 ID
|
||
- `api_keys`: API Key 限流状态列表,每个包含:
|
||
- `api_key_name`: API Key 名称
|
||
- `limit`: 速率限制上限
|
||
- `remaining`: 剩余可用次数
|
||
- `reset_time`: 限制重置时间
|
||
- `window`: 时间窗口
|
||
"""
|
||
adapter = UserRateLimitStatusAdapter()
|
||
return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode)
|
||
|
||
|
||
class AuthenticatedApiAdapter(ApiAdapter):
|
||
"""需要用户登录的适配器基类。"""
|
||
|
||
mode = ApiMode.USER
|
||
|
||
def authorize(self, context: ApiRequestContext) -> None: # type: ignore[override]
|
||
if not context.user:
|
||
raise HTTPException(status_code=401, detail="未登录")
|
||
|
||
|
||
@dataclass
|
||
class UserAuditLogsAdapter(AuthenticatedApiAdapter):
|
||
event_type: str | None
|
||
days: int
|
||
limit: int
|
||
offset: int
|
||
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
db = context.db
|
||
user = context.user
|
||
if not user:
|
||
raise HTTPException(status_code=401, detail="未登录")
|
||
|
||
query = db.query(AuditLog).filter(AuditLog.user_id == user.id)
|
||
if self.event_type:
|
||
query = query.filter(AuditLog.event_type == self.event_type)
|
||
|
||
cutoff_time = datetime.now(timezone.utc) - timedelta(days=self.days)
|
||
query = query.filter(AuditLog.created_at >= cutoff_time)
|
||
query = query.order_by(AuditLog.created_at.desc())
|
||
|
||
total, logs = paginate_query(query, self.limit, self.offset)
|
||
|
||
items = [
|
||
{
|
||
"id": log.id,
|
||
"event_type": log.event_type,
|
||
"description": log.description,
|
||
"ip_address": log.ip_address,
|
||
"status_code": log.status_code,
|
||
"created_at": log.created_at.isoformat() if log.created_at else None,
|
||
}
|
||
for log in logs
|
||
]
|
||
|
||
meta = PaginationMeta(
|
||
total=total,
|
||
limit=self.limit,
|
||
offset=self.offset,
|
||
count=len(items),
|
||
)
|
||
|
||
return build_pagination_payload(
|
||
items,
|
||
meta,
|
||
filters={
|
||
"event_type": self.event_type,
|
||
"days": self.days,
|
||
},
|
||
)
|
||
|
||
|
||
class UserRateLimitStatusAdapter(AuthenticatedApiAdapter):
|
||
async def handle(self, context: ApiRequestContext) -> Any: # type: ignore[override]
|
||
db = context.db
|
||
user = context.user
|
||
if not user:
|
||
raise HTTPException(status_code=401, detail="未登录")
|
||
|
||
api_keys = (
|
||
db.query(ApiKey)
|
||
.filter(ApiKey.user_id == user.id, ApiKey.is_active.is_(True))
|
||
.order_by(ApiKey.created_at.desc())
|
||
.all()
|
||
)
|
||
|
||
try:
|
||
limiter = await get_user_rpm_limiter()
|
||
system_default_raw = SystemConfigService.get_config(
|
||
db, SYSTEM_RPM_CONFIG_KEY, default=0
|
||
)
|
||
system_default = max(int(system_default_raw or 0), 0)
|
||
reset_at = limiter.get_reset_at()
|
||
window = f"{limiter.bucket_seconds}s"
|
||
except Exception as exc:
|
||
logger.warning("读取新 RPM 限流状态失败,回退插件状态接口: {}", exc)
|
||
limiter = None
|
||
system_default = 0
|
||
reset_at = None
|
||
window = None
|
||
|
||
rate_limit_info = []
|
||
for key in api_keys:
|
||
if limiter is not None:
|
||
if key.is_standalone:
|
||
user_limit = key.rate_limit if key.rate_limit is not None else system_default
|
||
user_scope_key = limiter.get_standalone_rpm_key(key.id)
|
||
key_limit = 0
|
||
else:
|
||
user_limit = user.rate_limit if user.rate_limit is not None else system_default
|
||
user_scope_key = limiter.get_user_rpm_key(user.id)
|
||
key_limit = max(int(key.rate_limit or 0), 0)
|
||
|
||
user_count = (
|
||
await limiter.get_scope_count(user_scope_key)
|
||
if user_limit and user_limit > 0
|
||
else 0
|
||
)
|
||
key_count = (
|
||
await limiter.get_scope_count(limiter.get_key_rpm_key(key.id))
|
||
if key_limit > 0
|
||
else 0
|
||
)
|
||
user_remaining = max(user_limit - user_count, 0) if user_limit > 0 else None
|
||
key_remaining = max(key_limit - key_count, 0) if key_limit > 0 else None
|
||
|
||
scoped_statuses: list[tuple[str, int, int]] = []
|
||
if user_limit > 0 and user_remaining is not None:
|
||
scoped_statuses.append(("user", user_limit, user_remaining))
|
||
if key_limit > 0 and key_remaining is not None:
|
||
scoped_statuses.append(("key", key_limit, key_remaining))
|
||
|
||
primary_scope = (
|
||
min(scoped_statuses, key=lambda item: item[2]) if scoped_statuses else None
|
||
)
|
||
rate_limit_info.append(
|
||
{
|
||
"api_key_name": key.name or f"Key-{key.id}",
|
||
"limit": primary_scope[1] if primary_scope else None,
|
||
"remaining": primary_scope[2] if primary_scope else None,
|
||
"scope": primary_scope[0] if primary_scope else None,
|
||
"reset_time": reset_at.isoformat() if reset_at else None,
|
||
"window": window,
|
||
"user_limit": user_limit,
|
||
"user_remaining": user_remaining,
|
||
"key_limit": key_limit if key_limit > 0 else None,
|
||
"key_remaining": key_remaining,
|
||
}
|
||
)
|
||
continue
|
||
|
||
rate_limit_info.append(
|
||
{
|
||
"api_key_name": key.name or f"Key-{key.id}",
|
||
"limit": None,
|
||
"remaining": None,
|
||
"scope": None,
|
||
"reset_time": None,
|
||
"window": None,
|
||
"user_limit": None,
|
||
"user_remaining": None,
|
||
"key_limit": None,
|
||
"key_remaining": None,
|
||
}
|
||
)
|
||
|
||
return {"user_id": user.id, "api_keys": rate_limit_info}
|