You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

185 lines
4.9 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""健康检查端点
提供三级健康检查:
- /health: 基础健康检查(负载均衡器用)
- /health/ready: 就绪检查(所有依赖就绪才算 ready
- /health/live: 存活检查(进程存活即可)
"""
import asyncio
import shutil
import time
from datetime import datetime
from fastapi import APIRouter, Request
from sqlalchemy import text
from app.common.constant import RET
from app.common.response import ErrorResponse, ResponseSchema, SuccessResponse
from app.config.setting import settings
from app.core.database import async_db_session
from app.core.logger import logger
from .schema import DependencyStatus, HealthOut, ReadinessOut
HealthRouter = APIRouter(prefix="/health", tags=["公共模块", "健康检查"])
async def _check_database() -> DependencyStatus:
"""检查数据库连接"""
try:
if not settings.SQL_DB_ENABLE:
return DependencyStatus(status=0)
start = time.perf_counter()
async with async_db_session() as session:
await session.execute(text("SELECT 1"))
latency = (time.perf_counter() - start) * 1000
return DependencyStatus(status=1, latency_ms=round(latency, 2))
except Exception as e:
logger.warning(f"数据库健康检查失败: {e}")
return DependencyStatus(status=0)
async def _check_redis(request: Request) -> DependencyStatus:
"""检查 Redis 连接"""
try:
if not settings.REDIS_ENABLE:
return DependencyStatus(status=0)
redis = getattr(request.app.state, "redis", None)
if not redis:
return DependencyStatus(status=0)
start = time.perf_counter()
await redis.ping()
latency = (time.perf_counter() - start) * 1000
return DependencyStatus(status=1, latency_ms=round(latency, 2))
except Exception as e:
logger.warning(f"Redis 健康检查失败: {e}")
return DependencyStatus(status=0)
def _get_disk_usage() -> float:
"""获取磁盘使用率"""
try:
usage = shutil.disk_usage("/")
return round(usage.used / usage.total * 100, 1)
except Exception:
return -1.0
# 应用启动时间戳
_start_time = datetime.now()
@HealthRouter.get(
"",
summary="基础健康检查",
description="仅检查进程是否存活,用于负载均衡器探测",
response_model=ResponseSchema[HealthOut],
)
async def health_check():
"""
基础健康检查
参数:
- 无
返回:
- SuccessResponse: 包含进程存活状态、启动时间、版本号的 JSON 响应。
"""
uptime = (datetime.now() - _start_time).total_seconds()
return SuccessResponse(
data=HealthOut(
status=1,
timestamp=datetime.now().isoformat(),
version=settings.VERSION,
uptime_seconds=uptime,
),
msg="系统健康",
)
@HealthRouter.get(
"/live",
summary="存活探针",
description="进程已启动即可返回 200供 K8s livenessProbe 使用",
response_model=ResponseSchema[HealthOut],
)
async def liveness_check():
"""
存活探针
参数:
- 无
返回:
- SuccessResponse: 包含进程存活状态、启动时间、版本号的 JSON 响应。
"""
uptime = (datetime.now() - _start_time).total_seconds()
return SuccessResponse(
data=HealthOut(
status=1,
timestamp=datetime.now().isoformat(),
version=settings.VERSION,
uptime_seconds=uptime,
),
msg="进程存活",
)
@HealthRouter.get(
"/ready",
summary="就绪探针",
description="探测数据库与 Redis任一项失败返回 503供 K8s readinessProbe 使用",
response_model=ResponseSchema[ReadinessOut],
)
async def readiness_check(request: Request):
"""
就绪探针
参数:
- request (Request): FastAPI 请求对象,用于获取 Redis 客户端。
返回:
- SuccessResponse | ErrorResponse: 依赖就绪时返回 200未就绪返回 503。
"""
uptime = (datetime.now() - _start_time).total_seconds()
db_status, redis_status = await asyncio.gather(
_check_database(),
_check_redis(request),
)
dependencies = {
"database": db_status,
"redis": redis_status,
}
# 判断总体状态
def is_ok(d: DependencyStatus) -> bool:
return d.status in ("up", "disabled")
all_ok = all(is_ok(d) for d in dependencies.values())
payload = ReadinessOut(
status=1 if all_ok else 0,
timestamp=datetime.now().isoformat(),
version=settings.VERSION,
uptime_seconds=uptime,
dependencies=dependencies,
disk_usage=_get_disk_usage(),
)
if all_ok:
return SuccessResponse(data=payload, msg="依赖就绪")
return ErrorResponse(
data=payload,
msg="依赖未就绪",
code=RET.SERVICE_UNAVAILABLE.code,
status_code=503,
success=False,
)