ForcePilot/backend/server/routers/channels/health_router.py

322 lines
14 KiB
Python
Raw Normal View History

"""健康检查域 RouterHLT-AGG-01 / HLT-AGG-02 / HLT-EXP-01 / HLT-SINGLE / HLT-PROBE
router 实现渠道健康检查域的全部 HTTP 端点覆盖聚合健康简略 / 详细
诊断导出单渠道健康主动探测共 5 个操作
模板选型
- HLT-AGG-01 / HLT-AGG-02 / HLT-EXP-01模板 B数据面端口路由
``use_cases.health_check`` 端口调用 ``HealthCheckService`` 类型化
方法不经控制面管道
- HLT-SINGLE / HLT-PROBE模板 A控制面端口路由
``use_cases.health_check_control`` 端口调用 ``ChannelControlService``
类型化方法走控制面管道五阶段auth permission rate_limit
dispatch audit
鉴权策略HLT-AGG-01 无鉴权K8s 探针 + 公开状态页其余端点使用
``get_admin_user`` 依赖要求管理员角色角色校验由依赖函数完成router
内不做角色判断规范 §4
路径设计静态路径``/health`` / ``/health/detail`` /
``/health/diagnostics/export``先于动态路径
``/{channel_type}/{account_id}/health`` /
``/{channel_type}/{account_id}/probe``声明避免 ``/health`` 被捕获为
``channel_type="health"``规范 §6.5 router 不自行设置 prefix
根前缀 ``/channels`` ``channels_router`` 聚合 router 统一追加
注册顺序``health_router`` 必须在 ``channels_router`` 聚合中先于含
``/{channel_type}`` 动态路径的 router ``account_router`` /
``login_router`` / ``directory_router`` 注册
端点清单对应健康检查域设计方案§2.1 + P1 缺口 §2.3.17
- GET /health HLT-AGG-01 get_health
- GET /health/detail HLT-AGG-02 get_health_detail
- POST /health/diagnostics/export HLT-EXP-01 export_diagnostics
- GET /{channel_type}/{account_id}/health HLT-SINGLE get_single_health
- POST /{channel_type}/{account_id}/probe HLT-PROBE probe_channel
"""
from __future__ import annotations
from typing import Any, Literal
from fastapi import APIRouter, Depends, Query, Request
from pydantic import BaseModel, ConfigDict, Field
from yuxi.channels.contract.dtos.channel import ChannelType
from yuxi.channels.contract.dtos.health import (
DiagnosticsExportRequest,
HealthQuery,
)
from yuxi.channels.contract.dtos.health_check import ProbeCmd, SingleHealthQuery
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
build_operator,
dataclass_to_dict,
get_channel_use_cases,
raiseOnControlFailure,
serialize_control_data,
)
from server.utils.auth_middleware import get_admin_user
# build_operator 仅在需要构造 Operator 注入端口方法时使用HLT-EXP-01
# HLT-AGG-01 / HLT-AGG-02 的 checkHealth(query) 契约不接受 operator不构造。
health_router = APIRouter(tags=["channels-health"])
# ---------------- 静态路径端点(须先于动态路径声明) ----------------
@health_router.get("/health", response_model=dict)
async def get_health(
use_cases=Depends(get_channel_use_cases),
) -> dict[str, Any]:
"""聚合健康检查简略HLT-AGG-01FR-35
无鉴权端点适用于 K8s liveness/readiness 探针与公开状态页
返回简略视图status / version / degraded_components / 简化渠道列表
channel_type / plugin_state不暴露 account_id / last_error /
worker_status / db_connection_pool_status / circuit_breaker_state
运维细节与业务标识避免无鉴权端点信息泄露
编排链路HTTP 入参 构造 HealthQuerywith_probe=False
use_cases.health_check.checkHealth(query) 直接调用端口方法
HealthSnapshot 提取简略字段构造响应 dict
采用模板 B数据面不经过控制面管道不调用 raiseOnControlFailure
契约异常由全局 unified_error_handler 统一映射为 HTTP 响应响应遵循
``{"success": True, "data": ...}`` 统一结构K8s 探针仅依赖 HTTP 200
状态码判定存活不解析响应体结构
"""
query = HealthQuery()
snapshot = await use_cases.health_check.checkHealth(query)
return {
"success": True,
"data": {
"status": snapshot.status,
"version": snapshot.version,
"degraded_components": list(snapshot.degraded_components),
"channels": [
{
"channel_type": ch.channel_type,
"plugin_state": ch.plugin_state,
}
for ch in snapshot.channels
],
},
}
@health_router.get("/health/detail", response_model=dict)
async def get_health_detail(
channel_type: ChannelType | None = Query(
default=None, description="按渠道过滤None 表示全渠道)"
),
with_probe: bool = Query(
default=False, description="是否绕过缓存触发实时聚合(非调用 probeChannel"
),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""聚合健康检查详细HLT-AGG-02FR-35
管理员鉴权端点适用于运维排障返回完整 HealthSnapshot
worker_status / redis_stream_status / db_connection_pool_status /
各渠道 circuit_breaker_state / queue_depth / connection_pool_status /
trace_id / error_code 等运维字段
编排链路HTTP 入参channel_type / with_probe
构造 HealthQuerychannel_filter / with_probe
use_cases.health_check.checkHealth(query) 直接调用端口方法
dataclass_to_dict(snapshot) 序列化完整快照返回
采用模板 B数据面不经过控制面管道不调用 raiseOnControlFailure
with_probe 参数语义with_probe=True 绕过短期缓存触发实时聚合
非调用 probeChannel 端口方法详见设计方案 §1.5 决策 5
checkHealth(query: HealthQuery) 契约不接受 operator 参数只读无副作用
查询current_user 仅用于鉴权get_admin_user 依赖副作用不在
函数体使用
"""
query = HealthQuery(channel_filter=channel_type, with_probe=with_probe)
snapshot = await use_cases.health_check.checkHealth(query)
return {"success": True, "data": dataclass_to_dict(snapshot)}
# ---------------- Request Schemas ----------------
class DiagnosticsExportPayload(BaseModel):
"""诊断导出请求体HLT-EXP-01Router 层 Pydantic schema
字段对齐契约层 DiagnosticsExportRequest DTO operator router
构造从认证用户不在 body 命名为 ``Payload`` 后缀以区分 Router
schema 与契约层 DTO避免同名冲突规范反模式别名导入降低可读性
"""
model_config = ConfigDict(frozen=True)
channel_filter: ChannelType | None = Field(
default=None, description="按渠道过滤None 表示全渠道)"
)
include_audit_logs: bool = Field(default=True, description="是否包含审计日志")
audit_log_count: int = Field(
default=100, ge=1, le=500, description="审计日志条数上限"
)
include_error_logs: bool = Field(default=True, description="是否包含错误日志")
error_log_count: int = Field(
default=100, ge=1, le=500, description="错误日志条数上限"
)
# ---------------- Endpoints ----------------
@health_router.post("/health/diagnostics/export", response_model=dict)
async def export_diagnostics(
payload: DiagnosticsExportPayload,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""导出诊断信息包HLT-EXP-01FR-35
管理员鉴权端点适用于故障排障导出 manifest 快照 + 插件状态 +
审计日志 + 错误日志 + 队列深度 + Worker / Redis / DB 池状态
敏感字段已脱敏部分遮蔽 app_secret / encrypt_key / verification_token /
access_token / refresh_token全量遮蔽 webhook_secret / webhook_url /
password / api_key审计日志写入 fail-closed
编排链路HTTP 入参 build_operator 构造操作人 构造
DiagnosticsExportRequest DTO use_cases.health_check.exportDiagnostics(request)
直接调用端口方法 dataclass_to_dict(bundle) 序列化诊断包返回
对应数据面端口方法 exportDiagnostics非控制面管道路径
不调用 raiseOnControlFailure敏感字段脱敏由 framework
DiagnosticsExporter 完成router 层不重复脱敏鉴权由 framework
``get_admin_user`` 守门对齐端口契约 ``@pre: operator 已通过权限校验
管理员角色``用例服务不重复校验权限避免双重鉴权
"""
operator = build_operator(current_user, request)
export_request = DiagnosticsExportRequest(
operator=operator,
channel_filter=payload.channel_filter,
include_audit_logs=payload.include_audit_logs,
audit_log_count=payload.audit_log_count,
include_error_logs=payload.include_error_logs,
error_log_count=payload.error_log_count,
)
bundle = await use_cases.health_check.exportDiagnostics(export_request)
return {"success": True, "data": dataclass_to_dict(bundle)}
# ---------------- 动态路径端点(须在静态路径之后声明) ----------------
class ProbeRequest(BaseModel):
"""主动探测请求体HLT-PROBERouter 层 Pydantic schema
字段对齐契约层 ``ProbeCmd`` DTO 的非操作人部分``operator`` router
构造从认证用户不在 body ``probe_type`` 仅允许 ``connectivity``
``full`` Pydantic Literal 约束在 HTTP 边界即拒绝非法值
契约层 ``ProbeCmd.__post_init__`` 与控制面管道 dispatch_stage 重复校验
防御深度
"""
model_config = ConfigDict(frozen=True)
probe_type: Literal["connectivity", "full"] = Field(
default="connectivity",
description="探测类型connectivity 仅测连通性full 含凭据/权限/Webhook 全项检查)",
)
timeout_ms: int = Field(
default=5000,
ge=1000,
le=30000,
description="探测超时毫秒数(默认 5000上限 30000",
)
@health_router.get(
"/{channel_type}/{account_id}/health",
response_model=dict,
)
async def get_single_health(
channel_type: ChannelType,
account_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询单渠道健康详情HLT-SINGLE
对应控制面操作 ``health/single`` ``ChannelControlService.getSingleHealth``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道
聚合指定渠道账户的账户状态熔断器状态队列深度插件状态连接池状态
worker 状态等健康指标供运维排障定位单渠道健康问题本端点为纯读聚合
GET 安全方法语义不触发渠道 API 调用主动深度探测由
``POST /{channel_type}/{account_id}/probe``HLT-PROBE承担
编排链路HTTP 入参 ``build_operator`` 构造操作人 构造
``SingleHealthQuery`` ``use_cases.health_check_control.getSingleHealth``
调用控制面端口方法 ``ControlResult`` ``raiseOnControlFailure`` 转译
失败 ``serialize_control_data`` 序列化单渠道健康结果返回
采用模板 A控制面 ``health_check_control`` 端口与数据面
``health_check`` 字段分离调用 ``ChannelControlService`` 类型化方法
走控制面管道五阶段
"""
operator = build_operator(current_user, request)
query = SingleHealthQuery(
channel_type=channel_type,
account_id=account_id,
)
result = await use_cases.health_check_control.getSingleHealth(
query, operator=operator
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@health_router.post(
"/{channel_type}/{account_id}/probe",
response_model=dict,
)
async def probe_channel(
channel_type: ChannelType,
account_id: str,
request: Request,
body: ProbeRequest,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""主动深度探测指定渠道HLT-PROBE
对应控制面操作 ``health/probe`` ``ChannelControlService.probeChannelDeep``
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道
管理员发起的主动深度探测通过插件 ``ProbeableAdapter.probeDeep``
执行深度探测``connectivity`` 仅测连通性``full`` 含凭据 / 权限 /
Webhook 全项检查渠道未实现 ``probeDeep`` 时控制面返回 501
编排链路HTTP 入参 ``build_operator`` 构造操作人 构造 ``ProbeCmd``
``use_cases.health_check_control.probeChannelDeep`` 调用控制面端口方法
``ControlResult`` ``raiseOnControlFailure`` 转译失败
``serialize_control_data`` 序列化探测结果返回
采用模板 A控制面 ``health_check_control`` 端口调用
``ChannelControlService`` 类型化方法走控制面管道五阶段探测可触发
副作用调用渠道 API审计事务策略 INDEPENDENTbest-effort
"""
operator = build_operator(current_user, request)
cmd = ProbeCmd(
channel_type=channel_type,
account_id=account_id,
operator=operator,
probe_type=body.probe_type,
timeout_ms=body.timeout_ms,
)
result = await use_cases.health_check_control.probeChannelDeep(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}