分页总数(#5): - get_all_tasks 的 total 原为 len(flat_tasks)(当前页条数),移动端 「我的任务」用 tasks.length < total 判断 hasMore,首页满员时恒为 false,列表永远停在第一页 20 条。改为独立 COUNT 查询(与 notifications.py 已有写法保持一致)。 可观测性(#9): - 新增 core/logging.py:单行 JSON 结构化日志 + request_id/user 上下文 注入;零第三方依赖;接管 uvicorn 自带 handler 避免格式绕过。 - 新增 core/middleware.py:RequestContextMiddleware 生成/透传 X-Request-ID 并回写响应头,输出含耗时/用户的结构化访问日志。 - 新增 core/health.py:拆分存活/就绪探针。/health/live 不触依赖; /health/ready 探主库,不可用返回 503;MOM 挂掉仅降级不摘流量。 - main.py:全局异常处理器只把堆栈写日志,响应体仅回 request_id; 接入可选 Sentry(未装 SDK 时静默跳过)。 - auth_service:解析 Token 后写入 user 上下文,日志自动带操作人。 注:异常处理器由 ServerErrorMiddleware 调用,此时 contextvar 已被重置, 故 request_id 同时写入 request.state(由 ASGI scope 承载)再读取。 已用 TestClient 验证:探针状态码/检查项、X-Request-ID 透传与生成、 500 响应携带可对账的 request_id 且不泄露堆栈。
94 lines
3.0 KiB
Python
94 lines
3.0 KiB
Python
"""健康检查 — 存活探针与就绪探针分离
|
||
|
||
为什么必须拆开:
|
||
- 存活探针(liveness)只回答「进程还活着吗」,绝不能探测外部依赖。
|
||
否则数据库抖一下,编排系统会判定进程已死并反复重启容器,
|
||
把一次依赖故障放大成全站雪崩。
|
||
- 就绪探针(readiness)回答「现在能对外服务吗」。依赖不可用时返回 503,
|
||
由负载均衡把该实例摘掉,依赖恢复后自动回来。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
|
||
import anyio
|
||
from fastapi import APIRouter
|
||
from fastapi.responses import JSONResponse
|
||
from sqlalchemy import text
|
||
|
||
from app.core.config import settings
|
||
from app.core.database import AsyncSessionLocal
|
||
from app.core.mom_database import mom_engine
|
||
|
||
logger = logging.getLogger("track.health")
|
||
|
||
router = APIRouter(tags=["健康检查"])
|
||
|
||
|
||
async def _probe_primary_db() -> bool:
|
||
"""主库探活 — 业务强依赖,失败即不就绪"""
|
||
try:
|
||
async with AsyncSessionLocal() as session:
|
||
await session.execute(text("SELECT 1"))
|
||
return True
|
||
except Exception:
|
||
logger.exception("主库探活失败")
|
||
return False
|
||
|
||
|
||
def _probe_mom_db_sync() -> bool:
|
||
try:
|
||
with mom_engine.connect() as conn:
|
||
conn.execute(text("SELECT 1"))
|
||
return True
|
||
except Exception:
|
||
logger.exception("MOM 库探活失败")
|
||
return False
|
||
|
||
|
||
async def _probe_mom_db() -> bool:
|
||
# MOM 用的是同步引擎,放线程池执行,避免阻塞事件循环
|
||
return await anyio.to_thread.run_sync(_probe_mom_db_sync)
|
||
|
||
|
||
async def _collect() -> tuple[bool, dict[str, str]]:
|
||
primary_ok = await _probe_primary_db()
|
||
mom_ok = await _probe_mom_db()
|
||
checks = {
|
||
"database": "ok" if primary_ok else "fail",
|
||
# MOM 是外部只读依赖:挂掉时登录/选料降级,但扫码、流转、看板仍可用。
|
||
# 因此只标记 degraded、不摘流量 —— 否则 MOM 一抖就让在产车间全线停摆。
|
||
"mom_database": "ok" if mom_ok else "degraded",
|
||
}
|
||
return primary_ok, checks
|
||
|
||
|
||
@router.get("/health/live", include_in_schema=False)
|
||
async def liveness() -> dict:
|
||
"""存活探针:不触碰任何依赖,恒定快速返回"""
|
||
return {"status": "ok"}
|
||
|
||
|
||
@router.get("/health/ready", include_in_schema=False)
|
||
async def readiness() -> JSONResponse:
|
||
"""就绪探针:主库不可用时返回 503,让负载均衡摘流量"""
|
||
ready, checks = await _collect()
|
||
return JSONResponse(
|
||
{"status": "ready" if ready else "not_ready", "checks": checks},
|
||
status_code=200 if ready else 503,
|
||
)
|
||
|
||
|
||
@router.get("/health", include_in_schema=False)
|
||
async def health() -> JSONResponse:
|
||
"""兼容旧监控脚本:语义等同就绪探针,并附带版本号"""
|
||
ready, checks = await _collect()
|
||
return JSONResponse(
|
||
{
|
||
"status": "ok" if ready else "unavailable",
|
||
"version": settings.APP_VERSION,
|
||
"checks": checks,
|
||
},
|
||
status_code=200 if ready else 503,
|
||
)
|