fastapi-augment 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_augment/__init__.py +24 -0
- fastapi_augment/common/__init__.py +61 -0
- fastapi_augment/common/constants.py +26 -0
- fastapi_augment/common/exception_handlers.py +178 -0
- fastapi_augment/common/exceptions.py +162 -0
- fastapi_augment/common/utils/__init__.py +5 -0
- fastapi_augment/common/utils/strings.py +175 -0
- fastapi_augment/config/__init__.py +8 -0
- fastapi_augment/config/settings.py +104 -0
- fastapi_augment/db/__init__.py +5 -0
- fastapi_augment/db/sqlalchemy/__init__.py +20 -0
- fastapi_augment/db/sqlalchemy/alembic/__init__.py +5 -0
- fastapi_augment/db/sqlalchemy/alembic/env.py +141 -0
- fastapi_augment/db/sqlalchemy/base.py +9 -0
- fastapi_augment/db/sqlalchemy/crud_base.py +426 -0
- fastapi_augment/db/sqlalchemy/engine.py +238 -0
- fastapi_augment/db/sqlalchemy/migrate.py +356 -0
- fastapi_augment/db/sqlalchemy/mixins/__init__.py +18 -0
- fastapi_augment/db/sqlalchemy/mixins/audit.py +61 -0
- fastapi_augment/db/sqlalchemy/mixins/soft_delete.py +80 -0
- fastapi_augment/db/sqlalchemy/mixins/timestamp.py +48 -0
- fastapi_augment/db/sqlalchemy/model_base.py +47 -0
- fastapi_augment/db/sqlalchemy/session.py +160 -0
- fastapi_augment/factory.py +238 -0
- fastapi_augment/health/__init__.py +34 -0
- fastapi_augment/health/checker.py +101 -0
- fastapi_augment/health/checkers.py +109 -0
- fastapi_augment/health/router.py +87 -0
- fastapi_augment/lifespan.py +450 -0
- fastapi_augment/log/__init__.py +26 -0
- fastapi_augment/log/config.py +201 -0
- fastapi_augment/log/factory.py +32 -0
- fastapi_augment/log/filters.py +23 -0
- fastapi_augment/log/handlers.py +81 -0
- fastapi_augment/middlewares/__init__.py +20 -0
- fastapi_augment/middlewares/base.py +79 -0
- fastapi_augment/middlewares/request_id.py +82 -0
- fastapi_augment/openapi.py +110 -0
- fastapi_augment/py.typed +0 -0
- fastapi_augment/schemas/__init__.py +29 -0
- fastapi_augment/schemas/base.py +32 -0
- fastapi_augment/schemas/pagination.py +46 -0
- fastapi_augment/schemas/request.py +28 -0
- fastapi_augment/schemas/response.py +139 -0
- fastapi_augment/schemas/types.py +11 -0
- fastapi_augment-0.1.0.dist-info/METADATA +654 -0
- fastapi_augment-0.1.0.dist-info/RECORD +50 -0
- fastapi_augment-0.1.0.dist-info/WHEEL +5 -0
- fastapi_augment-0.1.0.dist-info/entry_points.txt +2 -0
- fastapi_augment-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author : hangu
|
|
3
|
+
@CreateDate : 2026/8/31
|
|
4
|
+
@Description : Async session factory with read/write splitting, built on top of EngineManager.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import AsyncGenerator
|
|
9
|
+
from contextlib import asynccontextmanager
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from sqlalchemy.ext.asyncio import (
|
|
13
|
+
AsyncSession,
|
|
14
|
+
async_sessionmaker,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from .engine import EngineManager
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SessionFactory:
|
|
21
|
+
"""
|
|
22
|
+
Async session factory that transparently routes reads and writes
|
|
23
|
+
to the correct database engine.
|
|
24
|
+
|
|
25
|
+
Usage::
|
|
26
|
+
|
|
27
|
+
manager = EngineManager(topology).start()
|
|
28
|
+
factory = SessionFactory(manager)
|
|
29
|
+
|
|
30
|
+
# Write session (always hits primary)
|
|
31
|
+
async with factory.write_session() as session:
|
|
32
|
+
session.add(User(name='alice'))
|
|
33
|
+
await session.commit()
|
|
34
|
+
|
|
35
|
+
# Read session (round-robin across replicas)
|
|
36
|
+
async with factory.read_session() as session:
|
|
37
|
+
result = await session.execute(select(User))
|
|
38
|
+
|
|
39
|
+
# FastAPI dependencies
|
|
40
|
+
@app.get('/users')
|
|
41
|
+
async def list_users(session: AsyncSession = Depends(factory.depends_read)):
|
|
42
|
+
...
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
engine_manager: EngineManager,
|
|
48
|
+
*,
|
|
49
|
+
expire_on_commit: bool = False,
|
|
50
|
+
session_kwargs: dict[str, Any] | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
self._manager = engine_manager
|
|
53
|
+
self._session_kwargs = session_kwargs or {}
|
|
54
|
+
self._expire_on_commit = expire_on_commit
|
|
55
|
+
|
|
56
|
+
self._write_factory = async_sessionmaker(
|
|
57
|
+
bind=engine_manager.write_engine,
|
|
58
|
+
class_=AsyncSession,
|
|
59
|
+
expire_on_commit=expire_on_commit,
|
|
60
|
+
**self._session_kwargs,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# 缓存读引擎对应的 session factory,避免每次 read_session 重复创建
|
|
64
|
+
self._read_factories: dict[str, async_sessionmaker[AsyncSession]] = {}
|
|
65
|
+
|
|
66
|
+
# ── Properties ───────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def engine_manager(self) -> EngineManager:
|
|
70
|
+
"""The underlying engine manager.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The engine manager.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
return self._manager
|
|
77
|
+
|
|
78
|
+
# ── Session Factories (async context managers) ───────────────────────
|
|
79
|
+
|
|
80
|
+
@asynccontextmanager
|
|
81
|
+
async def write_session(self) -> AsyncGenerator[AsyncSession, None]:
|
|
82
|
+
"""Yield a session bound to the **primary** (write) engine
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
An async session bound to the primary engine.
|
|
86
|
+
"""
|
|
87
|
+
async with self._write_factory() as session:
|
|
88
|
+
yield session
|
|
89
|
+
|
|
90
|
+
@asynccontextmanager
|
|
91
|
+
async def read_session(self) -> AsyncGenerator[AsyncSession, None]:
|
|
92
|
+
"""Yield a session bound to a **read** engine (round-robin across replicas).
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
An async session bound to a read engine.
|
|
96
|
+
"""
|
|
97
|
+
read_engine = self._manager.next_read_engine()
|
|
98
|
+
engine_key = id(read_engine)
|
|
99
|
+
|
|
100
|
+
factory = self._read_factories.get(engine_key)
|
|
101
|
+
if factory is None:
|
|
102
|
+
factory = async_sessionmaker(
|
|
103
|
+
bind=read_engine,
|
|
104
|
+
class_=AsyncSession,
|
|
105
|
+
expire_on_commit=self._expire_on_commit,
|
|
106
|
+
**self._session_kwargs,
|
|
107
|
+
)
|
|
108
|
+
self._read_factories[engine_key] = factory
|
|
109
|
+
|
|
110
|
+
async with factory() as session:
|
|
111
|
+
yield session
|
|
112
|
+
|
|
113
|
+
# ── FastAPI Dependencies ─────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
async def depends_write(self) -> AsyncGenerator[AsyncSession, None]:
|
|
116
|
+
"""FastAPI ``Depends()`` — inject a write session.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
An async session bound to the primary engine.
|
|
120
|
+
"""
|
|
121
|
+
async with self.write_session() as session:
|
|
122
|
+
yield session
|
|
123
|
+
|
|
124
|
+
async def depends_read(self) -> AsyncGenerator[AsyncSession, None]:
|
|
125
|
+
"""FastAPI ``Depends()`` — inject a read session.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
An async session bound to a read engine.
|
|
129
|
+
"""
|
|
130
|
+
async with self.read_session() as session:
|
|
131
|
+
yield session
|
|
132
|
+
|
|
133
|
+
# ── Transactional helpers ────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
@asynccontextmanager
|
|
136
|
+
async def transaction(self) -> AsyncGenerator[AsyncSession, None]:
|
|
137
|
+
"""Write session with automatic commit / rollback.
|
|
138
|
+
|
|
139
|
+
Commits on clean exit, rolls back on any exception::
|
|
140
|
+
|
|
141
|
+
async with factory.transaction() as session:
|
|
142
|
+
session.add(obj)
|
|
143
|
+
# auto-commit here
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
An async session bound to the primary engine.
|
|
147
|
+
"""
|
|
148
|
+
async with self.write_session() as session:
|
|
149
|
+
try:
|
|
150
|
+
yield session
|
|
151
|
+
await session.commit()
|
|
152
|
+
except Exception:
|
|
153
|
+
await session.rollback()
|
|
154
|
+
raise
|
|
155
|
+
|
|
156
|
+
# ── Lifecycle ────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
async def dispose(self) -> None:
|
|
159
|
+
"""Dispose the underlying engine manager and all connection pools."""
|
|
160
|
+
await self._manager.dispose()
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author : hangu
|
|
3
|
+
@CreateDate : 2026/9/3
|
|
4
|
+
@Description : FastAPI 应用工厂
|
|
5
|
+
- 统一创建 FastAPI 实例并自动装配生命周期、数据库、中间件、路由
|
|
6
|
+
- 支持可选的 SQLAlchemy 读写分离集成
|
|
7
|
+
- 通过 app.state 暴露核心组件供业务层使用
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from time import time
|
|
12
|
+
from typing import Sequence, Callable, Any
|
|
13
|
+
|
|
14
|
+
from fastapi import FastAPI, APIRouter
|
|
15
|
+
from starlette.middleware import Middleware
|
|
16
|
+
|
|
17
|
+
from .common.exception_handlers import register_exception_handlers
|
|
18
|
+
from .lifespan import HookRegistry, fastapi_lifespan
|
|
19
|
+
from .middlewares import RequestIdMiddleware
|
|
20
|
+
from .openapi import configure_openapi_schema, OpenAPICustomConfig
|
|
21
|
+
from .health import create_health_router
|
|
22
|
+
|
|
23
|
+
# -------------------------- 类型别名 --------------------------
|
|
24
|
+
# 路由注册回调:接收 app 实例,负责 include_router 等操作
|
|
25
|
+
_RouteRegistrar = Callable[[FastAPI], None]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_app(
|
|
29
|
+
*,
|
|
30
|
+
title: str = 'FastAPI',
|
|
31
|
+
version: str = '0.1.0',
|
|
32
|
+
description: str = '',
|
|
33
|
+
debug: bool = False,
|
|
34
|
+
docs_url: str | None = '/docs',
|
|
35
|
+
redoc_url: str | None = '/redoc',
|
|
36
|
+
openapi_url: str | None = '/openapi.json',
|
|
37
|
+
# CORS配置:None=不启用;传入非空序列才启用CORS中间件
|
|
38
|
+
cors_allow_origins: Sequence[str] | None = None,
|
|
39
|
+
cors_allow_methods: Sequence[str] | None = None,
|
|
40
|
+
cors_allow_headers: Sequence[str] | None = None,
|
|
41
|
+
# 生命周期钩子
|
|
42
|
+
registries: Sequence[HookRegistry] | None = None,
|
|
43
|
+
# 中间件
|
|
44
|
+
middlewares: Sequence[Middleware] | None = None,
|
|
45
|
+
# 路由注册
|
|
46
|
+
routers: Sequence[APIRouter | tuple[APIRouter, dict[str, Any]]] | None = None,
|
|
47
|
+
route_registrars: Sequence[_RouteRegistrar] | None = None,
|
|
48
|
+
# OpenAPI 自定义参数新增
|
|
49
|
+
openapi_remove_422: bool = True,
|
|
50
|
+
openapi_remove_validation_error: bool = True,
|
|
51
|
+
openapi_enable_bearer_auth: bool = False,
|
|
52
|
+
# 异常处理器
|
|
53
|
+
register_exceptions: bool = True,
|
|
54
|
+
# 数据库集成(可选)
|
|
55
|
+
engine_manager: Any | None = None,
|
|
56
|
+
session_factory: Any | None = None,
|
|
57
|
+
# 健康检查
|
|
58
|
+
health_check: bool = False,
|
|
59
|
+
# 额外 FastAPI 参数
|
|
60
|
+
**kwargs: Any,
|
|
61
|
+
) -> FastAPI:
|
|
62
|
+
"""创建并配置 FastAPI 应用实例。
|
|
63
|
+
|
|
64
|
+
工厂函数将以下组件统一装配到应用上:
|
|
65
|
+
1. 生命周期管理 — 自动接入 :func:`fastapi_lifespan`,合并用户注册表与 core_registry
|
|
66
|
+
2. 中间件 — 按列表顺序添加(先添加的在内层)
|
|
67
|
+
3. 路由 — 支持直接传入 APIRouter 或 (router, kwargs) 元组
|
|
68
|
+
4. 异常处理器 — 自动注册统一异常处理,返回标准 APIResponse 格式
|
|
69
|
+
5. 数据库 — 可选地将 EngineManager / SessionFactory 挂载到 app.state
|
|
70
|
+
|
|
71
|
+
装配完成后,``app.state`` 上可访问以下属性:
|
|
72
|
+
- ``app.state.registries`` — 生命周期注册表列表
|
|
73
|
+
- ``app.state.engine_manager`` — 数据库引擎管理器(若提供)
|
|
74
|
+
- ``app.state.session_factory`` — 会话工厂(若提供)
|
|
75
|
+
|
|
76
|
+
Example::
|
|
77
|
+
|
|
78
|
+
from fastapi_augment.factory import create_app
|
|
79
|
+
from fastapi_augment.lifespan import HookRegistry
|
|
80
|
+
from fastapi_augment.db.sqlalchemy import EngineManager, SessionFactory, ClusterTopology, NodeConfig
|
|
81
|
+
|
|
82
|
+
# 数据库
|
|
83
|
+
topology = ClusterTopology(primary=NodeConfig(url='sqlite+aiosqlite:///app.db'))
|
|
84
|
+
manager = EngineManager(topology).start()
|
|
85
|
+
sessions = SessionFactory(manager)
|
|
86
|
+
|
|
87
|
+
# 自定义钩子
|
|
88
|
+
my_registry = HookRegistry()
|
|
89
|
+
|
|
90
|
+
@my_registry.on_startup
|
|
91
|
+
async def init_cache() -> None:
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
app = create_app(
|
|
95
|
+
title='My Service',
|
|
96
|
+
registries=[my_registry],
|
|
97
|
+
engine_manager=manager,
|
|
98
|
+
session_factory=sessions,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
title: 应用标题
|
|
103
|
+
version: 应用版本
|
|
104
|
+
description: 应用描述
|
|
105
|
+
debug: 是否开启调试模式
|
|
106
|
+
docs_url: Swagger UI 路径,None 禁用
|
|
107
|
+
redoc_url: ReDoc 路径,None 禁用
|
|
108
|
+
openapi_url: OpenAPI schema 路径,None 禁用
|
|
109
|
+
cors_allow_origins: CORS 允许的源列表,None=不启用CORS
|
|
110
|
+
cors_allow_methods: CORS 允许的 HTTP 方法列表,None=不启用CORS
|
|
111
|
+
cors_allow_headers: CORS 允许的 HTTP 头列表,None=不启用CORS
|
|
112
|
+
registries: 生命周期钩子注册表
|
|
113
|
+
middlewares: Starlette 中间件列表
|
|
114
|
+
routers: 路由列表,元素可以是 APIRouter 或 (router, kwargs) 元组
|
|
115
|
+
route_registrars: 路由注册回调列表,接收 app 参数
|
|
116
|
+
openapi_remove_422: 是否移除 422 验证错误响应
|
|
117
|
+
openapi_remove_validation_error: 是否移除验证错误参数
|
|
118
|
+
openapi_enable_bearer_auth: 是否启用 Bearer 认证
|
|
119
|
+
register_exceptions: 是否自动注册统一异常处理器,默认 True
|
|
120
|
+
engine_manager: 数据库引擎管理器实例(可选)
|
|
121
|
+
session_factory: 会话工厂实例(可选)
|
|
122
|
+
health_check: 是否启用健康检查端点(默认 ``/health``);
|
|
123
|
+
当传入 ``engine_manager`` 时自动包含数据库连通性检查
|
|
124
|
+
**kwargs: 传递给 FastAPI() 构造函数的额外参数(不允许传 lifespan)
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
配置完成的 FastAPI 应用实例
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
ValueError: 当 kwargs 中包含 lifespan 时抛出
|
|
131
|
+
"""
|
|
132
|
+
# ---- 前置校验:禁止外部传入 lifespan ----
|
|
133
|
+
if 'lifespan' in kwargs:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
'不允许通过 kwargs 传递 lifespan,'
|
|
136
|
+
'请使用 registries 参数注册生命周期钩子,工厂会自动管理 lifespan'
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# ---- 1. 构建 FastAPI 实例 ----
|
|
140
|
+
app = FastAPI(
|
|
141
|
+
title=title,
|
|
142
|
+
version=version,
|
|
143
|
+
description=description,
|
|
144
|
+
debug=debug,
|
|
145
|
+
docs_url=docs_url,
|
|
146
|
+
redoc_url=redoc_url,
|
|
147
|
+
openapi_url=openapi_url,
|
|
148
|
+
middleware=middlewares,
|
|
149
|
+
**kwargs,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# ---- 2. 添加 RequestId 中间件 ----
|
|
153
|
+
app.add_middleware(RequestIdMiddleware)
|
|
154
|
+
|
|
155
|
+
# ---- 3. CORS 配置 ----
|
|
156
|
+
if cors_allow_origins:
|
|
157
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
158
|
+
|
|
159
|
+
opts = dict(
|
|
160
|
+
allow_origins=cors_allow_origins,
|
|
161
|
+
allow_credentials='*' not in cors_allow_origins,
|
|
162
|
+
allow_methods=cors_allow_methods or ['*'],
|
|
163
|
+
allow_headers=cors_allow_headers or ['*']
|
|
164
|
+
)
|
|
165
|
+
app.add_middleware(CORSMiddleware, **opts) # type: ignore
|
|
166
|
+
|
|
167
|
+
# ---- 4. 生命周期注册表 ----
|
|
168
|
+
resolved_registries = _resolve_registries(registries)
|
|
169
|
+
app.state.registries = resolved_registries
|
|
170
|
+
|
|
171
|
+
# 设置 lifespan(使用模块级的 fastapi_lifespan,它会自动将 core_registry 插入首位)
|
|
172
|
+
app.router.lifespan_context = fastapi_lifespan
|
|
173
|
+
|
|
174
|
+
# ---- 5. 路由 ----
|
|
175
|
+
if routers:
|
|
176
|
+
for item in routers:
|
|
177
|
+
if isinstance(item, APIRouter):
|
|
178
|
+
app.include_router(item)
|
|
179
|
+
elif isinstance(item, tuple) and len(item) == 2:
|
|
180
|
+
router, router_kwargs = item
|
|
181
|
+
app.include_router(router, **router_kwargs)
|
|
182
|
+
else:
|
|
183
|
+
raise TypeError(
|
|
184
|
+
f'routers 元素必须为 APIRouter 或 (APIRouter, dict) 元组,'
|
|
185
|
+
f'实际为 {type(item).__name__}'
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# ---- 6. 路由注册回调 ----
|
|
189
|
+
if route_registrars:
|
|
190
|
+
for registrar in route_registrars:
|
|
191
|
+
registrar(app)
|
|
192
|
+
|
|
193
|
+
# ---- 7. OpenAPI 配置 ----
|
|
194
|
+
configure_openapi_schema(
|
|
195
|
+
app,
|
|
196
|
+
config=OpenAPICustomConfig(
|
|
197
|
+
remove_422=openapi_remove_422,
|
|
198
|
+
remove_validation_error_schema=openapi_remove_validation_error,
|
|
199
|
+
enable_bearer_auth=openapi_enable_bearer_auth
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# ---- 8. 异常处理器 ----
|
|
204
|
+
if register_exceptions:
|
|
205
|
+
register_exception_handlers(app)
|
|
206
|
+
|
|
207
|
+
# ---- 9. 数据库组件挂载到 app.state ----
|
|
208
|
+
if engine_manager is not None:
|
|
209
|
+
app.state.engine_manager = engine_manager
|
|
210
|
+
|
|
211
|
+
if session_factory is not None:
|
|
212
|
+
app.state.session_factory = session_factory
|
|
213
|
+
|
|
214
|
+
# ---- 10. 健康检查 ----
|
|
215
|
+
if health_check:
|
|
216
|
+
app.state.start_time = time()
|
|
217
|
+
app.include_router(create_health_router(include_db_check=engine_manager is not None))
|
|
218
|
+
|
|
219
|
+
return app
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# -------------------------- 内部辅助 --------------------------
|
|
223
|
+
|
|
224
|
+
def _resolve_registries(
|
|
225
|
+
registries: Sequence[HookRegistry] | None,
|
|
226
|
+
) -> list[HookRegistry]:
|
|
227
|
+
"""将 registries 参数规范化为列表。
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
registries: 用户传入的注册表参数
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
规范化后的 HookRegistry 列表
|
|
234
|
+
"""
|
|
235
|
+
if registries is None:
|
|
236
|
+
return []
|
|
237
|
+
|
|
238
|
+
return list(registries)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author : zarkhan
|
|
3
|
+
@CreateDate : 2026/9/6
|
|
4
|
+
@Description: 健康检查模块
|
|
5
|
+
- 可扩展的检查器模式(继承 BaseChecker)
|
|
6
|
+
- 内置 AppChecker(应用状态)和 DatabaseChecker(数据库连通性)
|
|
7
|
+
- create_health_router() 一键创建健康检查路由
|
|
8
|
+
"""
|
|
9
|
+
from .checker import (
|
|
10
|
+
BaseChecker,
|
|
11
|
+
CheckResult,
|
|
12
|
+
HealthResponse,
|
|
13
|
+
STATUS_HEALTHY,
|
|
14
|
+
STATUS_DEGRADED,
|
|
15
|
+
STATUS_UNHEALTHY,
|
|
16
|
+
)
|
|
17
|
+
from .checkers import AppChecker, DatabaseChecker
|
|
18
|
+
from .router import create_health_router
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
# 基础
|
|
22
|
+
'BaseChecker',
|
|
23
|
+
'CheckResult',
|
|
24
|
+
'HealthResponse',
|
|
25
|
+
# 状态常量
|
|
26
|
+
'STATUS_HEALTHY',
|
|
27
|
+
'STATUS_DEGRADED',
|
|
28
|
+
'STATUS_UNHEALTHY',
|
|
29
|
+
# 内置检查器
|
|
30
|
+
'AppChecker',
|
|
31
|
+
'DatabaseChecker',
|
|
32
|
+
# 路由工厂
|
|
33
|
+
'create_health_router',
|
|
34
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author : zarkhan
|
|
3
|
+
@CreateDate : 2026/9/6
|
|
4
|
+
@Description: 健康检查基础模型与检查器抽象基类
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
|
|
13
|
+
from ..schemas import SchemaBase
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ── 结果模型 ──────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CheckResult(SchemaBase):
|
|
20
|
+
"""单项检查结果。
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
name: 检查项名称
|
|
24
|
+
status: 状态(healthy / degraded / unhealthy)
|
|
25
|
+
latency_ms: 检查耗时(毫秒)
|
|
26
|
+
details: 附加详情
|
|
27
|
+
"""
|
|
28
|
+
name: str = Field(description='检查项名称')
|
|
29
|
+
status: str = Field(description='状态:healthy / degraded / unhealthy')
|
|
30
|
+
latency_ms: float = Field(default=0.0, description='检查耗时(毫秒)')
|
|
31
|
+
details: dict[str, Any] | None = Field(default=None, description='附加详情')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class HealthResponse(SchemaBase):
|
|
35
|
+
"""健康检查响应体。
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
status: 总体状态(取所有检查项中最差的状态)
|
|
39
|
+
checks: 各检查项结果列表
|
|
40
|
+
"""
|
|
41
|
+
status: str = Field(description='总体状态:healthy / degraded / unhealthy')
|
|
42
|
+
checks: list[CheckResult] = Field(default_factory=list, description='各检查项结果')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── 状态常量 ──────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
STATUS_HEALTHY = 'healthy'
|
|
48
|
+
STATUS_DEGRADED = 'degraded'
|
|
49
|
+
STATUS_UNHEALTHY = 'unhealthy'
|
|
50
|
+
|
|
51
|
+
# 状态严重程度排序,用于聚合时取最差状态
|
|
52
|
+
_STATUS_SEVERITY = {STATUS_HEALTHY: 0, STATUS_DEGRADED: 1, STATUS_UNHEALTHY: 2}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _worst_status(*statuses: str) -> str:
|
|
56
|
+
"""从多个状态中取最差的一个。
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
*statuses: 待比较的状态字符串
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
最差状态
|
|
63
|
+
"""
|
|
64
|
+
return max(statuses, key=lambda s: _STATUS_SEVERITY.get(s, 99))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── 检查器基类 ────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BaseChecker(ABC):
|
|
71
|
+
"""健康检查器抽象基类。
|
|
72
|
+
|
|
73
|
+
所有检查器必须实现 ``name`` 属性和 ``check`` 异步方法::
|
|
74
|
+
|
|
75
|
+
class RedisChecker(BaseChecker):
|
|
76
|
+
@property
|
|
77
|
+
def name(self) -> str:
|
|
78
|
+
return 'redis'
|
|
79
|
+
|
|
80
|
+
async def check(self, app: FastAPI) -> CheckResult:
|
|
81
|
+
# 执行检查逻辑
|
|
82
|
+
return CheckResult(name=self.name, status='healthy', latency_ms=1.2)
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def name(self) -> str:
|
|
88
|
+
"""检查项名称,用于标识和展示。"""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
async def check(self, app: Any) -> CheckResult:
|
|
93
|
+
"""执行健康检查。
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
app: FastAPI 应用实例,可从中读取 app.state 等
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
单项检查结果
|
|
100
|
+
"""
|
|
101
|
+
...
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author : zarkhan
|
|
3
|
+
@CreateDate : 2026/9/6
|
|
4
|
+
@Description: 内置健康检查器
|
|
5
|
+
- AppChecker:应用基本信息(状态、版本、运行时长)
|
|
6
|
+
- DatabaseChecker:数据库连通性(基于 EngineManager)
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from fastapi import FastAPI
|
|
14
|
+
from sqlalchemy import text
|
|
15
|
+
|
|
16
|
+
from .checker import (
|
|
17
|
+
BaseChecker,
|
|
18
|
+
CheckResult,
|
|
19
|
+
STATUS_HEALTHY,
|
|
20
|
+
STATUS_UNHEALTHY,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AppChecker(BaseChecker):
|
|
25
|
+
"""应用基本健康检查。
|
|
26
|
+
|
|
27
|
+
返回应用运行状态、版本、运行时长。
|
|
28
|
+
通过 ``app.state.start_time`` 获取启动时间戳(由工厂或用户设置)。
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def name(self) -> str:
|
|
33
|
+
return 'app'
|
|
34
|
+
|
|
35
|
+
async def check(self, app: Any) -> CheckResult:
|
|
36
|
+
"""检查应用状态。
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
app: FastAPI 应用实例
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
包含版本、状态、运行时长的检查结果
|
|
43
|
+
"""
|
|
44
|
+
start_time = getattr(app.state, 'start_time', None)
|
|
45
|
+
uptime = time.time() - start_time if start_time else 0.0
|
|
46
|
+
|
|
47
|
+
return CheckResult(
|
|
48
|
+
name=self.name,
|
|
49
|
+
status=STATUS_HEALTHY,
|
|
50
|
+
latency_ms=0.0,
|
|
51
|
+
details={
|
|
52
|
+
'status': 'running',
|
|
53
|
+
'version': app.version if isinstance(app, FastAPI) else '',
|
|
54
|
+
'uptime_seconds': round(uptime, 2),
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class DatabaseChecker(BaseChecker):
|
|
60
|
+
"""数据库连通性健康检查。
|
|
61
|
+
|
|
62
|
+
通过 ``app.state.engine_manager`` 获取写引擎,执行 ``SELECT 1`` 验证连通性。
|
|
63
|
+
若未挂载 ``engine_manager``,则跳过检查并返回 ``unhealthy``。
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, name: str = 'database') -> None:
|
|
67
|
+
self._name = name
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def name(self) -> str:
|
|
71
|
+
return self._name
|
|
72
|
+
|
|
73
|
+
async def check(self, app: Any) -> CheckResult:
|
|
74
|
+
"""检查数据库连通性。
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
app: FastAPI 应用实例
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
数据库连通性检查结果
|
|
81
|
+
"""
|
|
82
|
+
engine_manager = getattr(app.state, 'engine_manager', None)
|
|
83
|
+
if engine_manager is None:
|
|
84
|
+
return CheckResult(
|
|
85
|
+
name=self.name,
|
|
86
|
+
status=STATUS_UNHEALTHY,
|
|
87
|
+
details={'error': 'engine_manager not found on app.state'},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
start = time.perf_counter()
|
|
91
|
+
try:
|
|
92
|
+
engine = engine_manager.write_engine
|
|
93
|
+
async with engine.connect() as conn:
|
|
94
|
+
await conn.execute(text('SELECT 1'))
|
|
95
|
+
latency = (time.perf_counter() - start) * 1000
|
|
96
|
+
|
|
97
|
+
return CheckResult(
|
|
98
|
+
name=self.name,
|
|
99
|
+
status=STATUS_HEALTHY,
|
|
100
|
+
latency_ms=round(latency, 2),
|
|
101
|
+
)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
latency = (time.perf_counter() - start) * 1000
|
|
104
|
+
return CheckResult(
|
|
105
|
+
name=self.name,
|
|
106
|
+
status=STATUS_UNHEALTHY,
|
|
107
|
+
latency_ms=round(latency, 2),
|
|
108
|
+
details={'error': str(e)},
|
|
109
|
+
)
|