springbootAI 1.8.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.
- spring/__init__.py +66 -0
- spring/ai/__init__.py +78 -0
- spring/ai/advisors.py +139 -0
- spring/ai/annotations.py +74 -0
- spring/ai/autoconfig.py +481 -0
- spring/ai/core.py +391 -0
- spring/ai/etl.py +188 -0
- spring/ai/memory.py +109 -0
- spring/ai/observability.py +129 -0
- spring/ai/providers.py +789 -0
- spring/ai/resilience.py +258 -0
- spring/ai/tools.py +106 -0
- spring/ai/vectorstore.py +303 -0
- spring/annotations/__init__.py +188 -0
- spring/annotations/cache.py +126 -0
- spring/annotations/cloud.py +207 -0
- spring/annotations/conditional.py +272 -0
- spring/annotations/core.py +864 -0
- spring/annotations/messaging.py +107 -0
- spring/aop/__init__.py +4 -0
- spring/aop/cloud_aop.py +404 -0
- spring/aop/comprehensive_aop.py +1015 -0
- spring/aop/method_interceptor.py +19 -0
- spring/aop/proxy_factory.py +55 -0
- spring/cloud/__init__.py +76 -0
- spring/cloud/discovery.py +364 -0
- spring/cloud/feign.py +469 -0
- spring/cloud/gateway.py +452 -0
- spring/cloud/load_balancer.py +149 -0
- spring/cloud/seata.py +557 -0
- spring/cloud/sentinel.py +525 -0
- spring/cloud/tracer.py +337 -0
- spring/config/__init__.py +21 -0
- spring/config/binding.py +206 -0
- spring/config/config_loader.py +405 -0
- spring/context/__init__.py +13 -0
- spring/context/application_context.py +589 -0
- spring/context/bean_definition.py +70 -0
- spring/context/bean_factory.py +1052 -0
- spring/context/registry.py +58 -0
- spring/context/scanner.py +106 -0
- spring/core/__init__.py +3 -0
- spring/core/graceful_shutdown.py +196 -0
- spring/core/typing_utils.py +50 -0
- spring/csv/__init__.py +52 -0
- spring/csv/annotations.py +402 -0
- spring/csv/converters.py +69 -0
- spring/csv/easy_csv.py +95 -0
- spring/csv/exceptions.py +27 -0
- spring/csv/reader.py +195 -0
- spring/csv/writer.py +155 -0
- spring/data/__init__.py +54 -0
- spring/data/page.py +181 -0
- spring/data/repository.py +274 -0
- spring/data/specification.py +228 -0
- spring/datasource/__init__.py +66 -0
- spring/datasource/annotations.py +133 -0
- spring/datasource/context.py +69 -0
- spring/datasource/dynamic.py +148 -0
- spring/event/__init__.py +7 -0
- spring/event/publisher.py +69 -0
- spring/excel/__init__.py +51 -0
- spring/excel/annotations.py +405 -0
- spring/excel/converters.py +231 -0
- spring/excel/easy_excel.py +94 -0
- spring/excel/exceptions.py +31 -0
- spring/excel/reader.py +254 -0
- spring/excel/style.py +95 -0
- spring/excel/writer.py +197 -0
- spring/i18n/__init__.py +97 -0
- spring/i18n/accessor.py +94 -0
- spring/i18n/auto_config.py +177 -0
- spring/i18n/holder.py +106 -0
- spring/i18n/locale.py +152 -0
- spring/i18n/locale_resolver.py +367 -0
- spring/i18n/message_source.py +250 -0
- spring/i18n/middleware.py +79 -0
- spring/i18n/properties.py +168 -0
- spring/i18n/sources.py +255 -0
- spring/logging/__init__.py +1 -0
- spring/logging/loguru_logger.py +228 -0
- spring/main.py +378 -0
- spring/messaging/__init__.py +1 -0
- spring/messaging/rabbitmq.py +302 -0
- spring/monitoring/__init__.py +1 -0
- spring/monitoring/prometheus.py +199 -0
- spring/orm/__init__.py +258 -0
- spring/orm/database.py +222 -0
- spring/orm/ddl_auto.py +1217 -0
- spring/orm/migration.py +419 -0
- spring/orm/mybatis_integration.py +400 -0
- spring/orm/pymybatis/__init__.py +86 -0
- spring/orm/pymybatis/annotations/__init__.py +30 -0
- spring/orm/pymybatis/annotations/annotations.py +332 -0
- spring/orm/pymybatis/cache/__init__.py +47 -0
- spring/orm/pymybatis/cache/cache.py +371 -0
- spring/orm/pymybatis/cache/redis_cache.py +434 -0
- spring/orm/pymybatis/circuit_breaker/__init__.py +21 -0
- spring/orm/pymybatis/circuit_breaker/circuit_breaker.py +424 -0
- spring/orm/pymybatis/configuration.py +525 -0
- spring/orm/pymybatis/core/__init__.py +10 -0
- spring/orm/pymybatis/core/sql_session.py +1382 -0
- spring/orm/pymybatis/core/sql_session_factory.py +76 -0
- spring/orm/pymybatis/dialect/__init__.py +9 -0
- spring/orm/pymybatis/dialect/dialect.py +445 -0
- spring/orm/pymybatis/dynamic_sql/__init__.py +9 -0
- spring/orm/pymybatis/dynamic_sql/dynamic_sql.py +900 -0
- spring/orm/pymybatis/interceptor/__init__.py +31 -0
- spring/orm/pymybatis/interceptor/interceptor.py +427 -0
- spring/orm/pymybatis/mapper/__init__.py +9 -0
- spring/orm/pymybatis/mapper/mapper.py +540 -0
- spring/orm/pymybatis/metrics/__init__.py +41 -0
- spring/orm/pymybatis/metrics/metrics.py +595 -0
- spring/orm/pymybatis/pool/__init__.py +9 -0
- spring/orm/pymybatis/pool/connection_pool.py +711 -0
- spring/orm/pymybatis/security/__init__.py +19 -0
- spring/orm/pymybatis/security/access_control.py +415 -0
- spring/orm/pymybatis/security/password_encoder.py +293 -0
- spring/orm/pymybatis/security/sensitive_data_masker.py +326 -0
- spring/orm/pymybatis/security/sql_injection_detector.py +675 -0
- spring/orm/pymybatis/transaction/__init__.py +9 -0
- spring/orm/pymybatis/transaction/transaction.py +288 -0
- spring/orm/pymybatis/type_handler/__init__.py +37 -0
- spring/orm/pymybatis/type_handler/type_handler.py +473 -0
- spring/orm/pymybatis/version.py +9 -0
- spring/orm/pymybatis/xml_parser/__init__.py +9 -0
- spring/orm/pymybatis/xml_parser/xml_parser.py +761 -0
- spring/retry/__init__.py +12 -0
- spring/retry/retry_annotations.py +71 -0
- spring/retry/retry_decorator.py +155 -0
- spring/scheduling/__init__.py +3 -0
- spring/scheduling/scheduler.py +389 -0
- spring/security/__init__.py +39 -0
- spring/security/jwt_utils.py +281 -0
- spring/security/replay_protection.py +206 -0
- spring/security/secret_manager.py +226 -0
- spring/security/security_aop.py +248 -0
- spring/security/security_context.py +172 -0
- spring/test/__init__.py +45 -0
- spring/test/slicing.py +341 -0
- spring/tracing/__init__.py +11 -0
- spring/tracing/skywalking.py +229 -0
- spring/tx/__init__.py +52 -0
- spring/tx/events.py +172 -0
- spring/tx/synchronization.py +143 -0
- spring/utils/__init__.py +5 -0
- spring/utils/banner.py +32 -0
- spring/utils/logger.py +73 -0
- spring/utils/redis_client.py +526 -0
- spring/validation/__init__.py +55 -0
- spring/validation/aop.py +141 -0
- spring/validation/constraints.py +357 -0
- spring/validation/exceptions.py +55 -0
- spring/validation/validator.py +139 -0
- spring/web/__init__.py +12 -0
- spring/web/actuator.py +319 -0
- spring/web/exception_handler.py +61 -0
- spring/web/health.py +399 -0
- spring/web/interceptor.py +91 -0
- spring/web/result.py +44 -0
- spring/web/swagger.py +601 -0
- spring/web/web_context.py +755 -0
- spring/websocket/__init__.py +86 -0
- spring/websocket/annotations.py +169 -0
- spring/websocket/broker.py +238 -0
- spring/websocket/exceptions.py +26 -0
- spring/websocket/handler.py +243 -0
- spring/websocket/router.py +526 -0
- spring/websocket/session.py +216 -0
- springbootai-1.8.0.dist-info/METADATA +2796 -0
- springbootai-1.8.0.dist-info/RECORD +175 -0
- springbootai-1.8.0.dist-info/WHEEL +5 -0
- springbootai-1.8.0.dist-info/entry_points.txt +2 -0
- springbootai-1.8.0.dist-info/licenses/LICENSE +7 -0
- springbootai-1.8.0.dist-info/top_level.txt +1 -0
spring/web/health.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""
|
|
2
|
+
健康检查模块
|
|
3
|
+
提供Actuator风格的健康检查端点
|
|
4
|
+
"""
|
|
5
|
+
import time
|
|
6
|
+
import logging
|
|
7
|
+
import threading
|
|
8
|
+
import queue
|
|
9
|
+
import platform
|
|
10
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
11
|
+
from fastapi import APIRouter
|
|
12
|
+
from fastapi.responses import JSONResponse, Response
|
|
13
|
+
from spring.utils.redis_client import redis_client
|
|
14
|
+
from spring.cloud.discovery import nacos_client
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from spring.orm.database import db_manager
|
|
18
|
+
except ImportError:
|
|
19
|
+
db_manager = None
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("Spring.Web.Health")
|
|
22
|
+
|
|
23
|
+
health_router = APIRouter()
|
|
24
|
+
_application_context = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def configure_health_checks(application_context) -> None:
|
|
28
|
+
global _application_context
|
|
29
|
+
_application_context = application_context
|
|
30
|
+
|
|
31
|
+
# 单个组件健康检查的最大耗时(秒)。
|
|
32
|
+
# 避免某个组件(如 Nacos/RabbitMQ 未配置但尝试连接默认地址)卡死整个 /actuator/health 端点,
|
|
33
|
+
# 进而拖垮 Docker HEALTHCHECK 与运维监控。
|
|
34
|
+
_CHECK_TIMEOUT_SECONDS = 2.0
|
|
35
|
+
|
|
36
|
+
_COMPONENT_CHECKS = {
|
|
37
|
+
'redis': lambda: _check_redis(),
|
|
38
|
+
'database': lambda: _check_database(),
|
|
39
|
+
'nacos': lambda: _check_nacos(),
|
|
40
|
+
'rabbitmq': lambda: _check_rabbitmq(),
|
|
41
|
+
'seata': lambda: _check_seata(),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _run_with_timeout(func, timeout: float = _CHECK_TIMEOUT_SECONDS):
|
|
46
|
+
"""
|
|
47
|
+
在独立 daemon 线程中执行健康检查,超时则立即返回 DOWN,不阻塞主请求。
|
|
48
|
+
|
|
49
|
+
使用 daemon 线程而非 concurrent.futures.ThreadPoolExecutor,
|
|
50
|
+
因为后者在 with 块退出时会 shutdown(wait=True) 阻塞等待卡住的任务完成,
|
|
51
|
+
反而会让超时机制失效。daemon 线程超时后主线程立即返回,
|
|
52
|
+
卡住的线程在后台继续运行但不影响响应,进程退出时自动清理。
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
func: 无参的可调用对象,返回状态字典
|
|
56
|
+
timeout: 最大等待秒数
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
检查结果字典;若超时则返回 DOWN + reason
|
|
60
|
+
"""
|
|
61
|
+
result_q: "queue.Queue" = queue.Queue()
|
|
62
|
+
|
|
63
|
+
def _worker():
|
|
64
|
+
try:
|
|
65
|
+
result_q.put(('ok', func()))
|
|
66
|
+
except Exception as e:
|
|
67
|
+
result_q.put(('err', e))
|
|
68
|
+
|
|
69
|
+
t = threading.Thread(target=_worker, daemon=True)
|
|
70
|
+
t.start()
|
|
71
|
+
t.join(timeout=timeout)
|
|
72
|
+
|
|
73
|
+
if t.is_alive():
|
|
74
|
+
# 超时:daemon 线程继续在后台跑,主线程立即返回
|
|
75
|
+
return {
|
|
76
|
+
'status': 'DOWN',
|
|
77
|
+
'enabled': True,
|
|
78
|
+
'reason': f'health check timeout after {timeout}s'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
kind, val = result_q.get_nowait()
|
|
83
|
+
if kind == 'ok':
|
|
84
|
+
return val
|
|
85
|
+
return {'status': 'DOWN', 'enabled': True, 'reason': str(val)}
|
|
86
|
+
except queue.Empty:
|
|
87
|
+
return {'status': 'DOWN', 'enabled': True, 'reason': 'health check returned no result'}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _collect_component_health() -> dict:
|
|
91
|
+
"""并发执行各组件检查,使探针耗时接近单个检查的最大超时。"""
|
|
92
|
+
with ThreadPoolExecutor(max_workers=len(_COMPONENT_CHECKS)) as executor:
|
|
93
|
+
futures = {
|
|
94
|
+
name: executor.submit(_run_with_timeout, check)
|
|
95
|
+
for name, check in _COMPONENT_CHECKS.items()
|
|
96
|
+
}
|
|
97
|
+
return {name: future.result() for name, future in futures.items()}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _enabled_down_components(components: dict) -> list:
|
|
101
|
+
return [
|
|
102
|
+
name for name, status in components.items()
|
|
103
|
+
if status.get('enabled', False) and status.get('status') == 'DOWN'
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@health_router.get('/health')
|
|
108
|
+
def health_check():
|
|
109
|
+
"""
|
|
110
|
+
健康检查端点
|
|
111
|
+
返回所有组件的健康状态
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
JSON格式的健康状态信息
|
|
115
|
+
"""
|
|
116
|
+
health_status = {
|
|
117
|
+
'status': 'UP',
|
|
118
|
+
'timestamp': time.time(),
|
|
119
|
+
'components': {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
health_status['components'] = _collect_component_health()
|
|
123
|
+
if _enabled_down_components(health_status['components']):
|
|
124
|
+
health_status['status'] = 'DEGRADED'
|
|
125
|
+
|
|
126
|
+
status_code = 200 if health_status['status'] == 'UP' else 503
|
|
127
|
+
return JSONResponse(content=health_status, status_code=status_code)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@health_router.get('/health/liveness')
|
|
131
|
+
def liveness_check():
|
|
132
|
+
"""
|
|
133
|
+
存活检查端点
|
|
134
|
+
检查应用是否正在运行
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
JSON格式的存活状态
|
|
138
|
+
"""
|
|
139
|
+
return JSONResponse(content={
|
|
140
|
+
'status': 'UP',
|
|
141
|
+
'timestamp': time.time()
|
|
142
|
+
}, status_code=200)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@health_router.get('/health/readiness')
|
|
146
|
+
def readiness_check():
|
|
147
|
+
"""
|
|
148
|
+
就绪检查端点
|
|
149
|
+
检查应用是否准备好处理请求
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
JSON格式的就绪状态
|
|
153
|
+
"""
|
|
154
|
+
components = _collect_component_health()
|
|
155
|
+
unavailable = _enabled_down_components(components)
|
|
156
|
+
if unavailable:
|
|
157
|
+
return JSONResponse(content={
|
|
158
|
+
'status': 'NOT_READY',
|
|
159
|
+
'timestamp': time.time(),
|
|
160
|
+
'reason': f"Required components unavailable: {', '.join(unavailable)}",
|
|
161
|
+
'components': components,
|
|
162
|
+
}, status_code=503)
|
|
163
|
+
|
|
164
|
+
return JSONResponse(content={
|
|
165
|
+
'status': 'READY',
|
|
166
|
+
'timestamp': time.time(),
|
|
167
|
+
'components': components,
|
|
168
|
+
}, status_code=200)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@health_router.get('/prometheus')
|
|
172
|
+
def prometheus_metrics():
|
|
173
|
+
config = _application_context.get_config() if _application_context is not None else {}
|
|
174
|
+
if not config.get('prometheus', {}).get('enabled', False):
|
|
175
|
+
return JSONResponse({'detail': 'Prometheus metrics are disabled'}, status_code=404)
|
|
176
|
+
from spring.monitoring.prometheus import CONTENT_TYPE_LATEST, prometheus_metrics as metrics
|
|
177
|
+
return Response(
|
|
178
|
+
content=metrics.generate_metrics_data(),
|
|
179
|
+
headers={'Content-Type': CONTENT_TYPE_LATEST},
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@health_router.get('/info')
|
|
184
|
+
def info_check():
|
|
185
|
+
"""返回不包含密钥和连接凭据的应用基本信息。"""
|
|
186
|
+
config = _application_context.get_config() if _application_context is not None else {}
|
|
187
|
+
spring_config = config.get('spring', {}) or {}
|
|
188
|
+
application_config = spring_config.get('application', {}) or {}
|
|
189
|
+
profile_config = spring_config.get('profiles', {}) or {}
|
|
190
|
+
try:
|
|
191
|
+
from spring import __version__ as spring_version
|
|
192
|
+
except ImportError:
|
|
193
|
+
spring_version = 'unknown'
|
|
194
|
+
|
|
195
|
+
return JSONResponse(content={
|
|
196
|
+
'application': {
|
|
197
|
+
'name': application_config.get('name', 'springpy-application'),
|
|
198
|
+
'profile': profile_config.get('active', 'default'),
|
|
199
|
+
},
|
|
200
|
+
'framework': {
|
|
201
|
+
'name': 'SpringBootAI',
|
|
202
|
+
'version': spring_version,
|
|
203
|
+
'python': platform.python_version(),
|
|
204
|
+
},
|
|
205
|
+
}, status_code=200)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _check_redis() -> dict:
|
|
209
|
+
"""检查Redis健康状态"""
|
|
210
|
+
try:
|
|
211
|
+
# 尊重 application.yml 的 redis.enabled 配置:
|
|
212
|
+
# 未启用时不尝试连接,直接返回 DISABLED,避免拖累整体健康状态
|
|
213
|
+
try:
|
|
214
|
+
redis_cfg = (
|
|
215
|
+
_application_context.get_config().get('redis', {})
|
|
216
|
+
if _application_context is not None else {}
|
|
217
|
+
) or {}
|
|
218
|
+
if not redis_cfg.get('enabled', False):
|
|
219
|
+
return {
|
|
220
|
+
'status': 'DISABLED',
|
|
221
|
+
'enabled': False,
|
|
222
|
+
'reason': 'Redis not configured (redis.enabled=false)'
|
|
223
|
+
}
|
|
224
|
+
except Exception:
|
|
225
|
+
# 配置读取失败时回退到原有行为(尝试连接)
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
client = redis_client.get_client()
|
|
229
|
+
if client:
|
|
230
|
+
client.ping()
|
|
231
|
+
info = client.info()
|
|
232
|
+
return {
|
|
233
|
+
'status': 'UP',
|
|
234
|
+
'enabled': True,
|
|
235
|
+
'version': info.get('redis_version', 'unknown'),
|
|
236
|
+
'used_memory': info.get('used_memory_human', 'unknown')
|
|
237
|
+
}
|
|
238
|
+
else:
|
|
239
|
+
return {
|
|
240
|
+
'status': 'DISABLED',
|
|
241
|
+
'enabled': False,
|
|
242
|
+
'reason': 'Redis not configured'
|
|
243
|
+
}
|
|
244
|
+
except Exception as e:
|
|
245
|
+
return {
|
|
246
|
+
'status': 'DOWN',
|
|
247
|
+
'enabled': True,
|
|
248
|
+
'reason': str(e)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _check_database() -> dict:
|
|
253
|
+
"""检查数据库健康状态"""
|
|
254
|
+
try:
|
|
255
|
+
if _application_context is not None:
|
|
256
|
+
database_config = (
|
|
257
|
+
_application_context.get_config().get('database', {}) or {}
|
|
258
|
+
)
|
|
259
|
+
if not database_config.get('enabled', False):
|
|
260
|
+
return {
|
|
261
|
+
'status': 'DISABLED',
|
|
262
|
+
'enabled': False,
|
|
263
|
+
'reason': 'Database not configured (database.enabled=false)',
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if _application_context is not None and _application_context.contains_bean(
|
|
267
|
+
'sqlSessionFactory'
|
|
268
|
+
):
|
|
269
|
+
factory = _application_context.get_bean('sqlSessionFactory')
|
|
270
|
+
pooled_connection = factory.connection_pool.get_connection()
|
|
271
|
+
factory.connection_pool.return_connection(pooled_connection)
|
|
272
|
+
return {
|
|
273
|
+
'status': 'UP',
|
|
274
|
+
'enabled': True,
|
|
275
|
+
'type': 'mybatis',
|
|
276
|
+
'pool': factory.connection_pool.get_pool_stats(),
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
engine = db_manager.get_engine() if db_manager is not None else None
|
|
280
|
+
if engine:
|
|
281
|
+
connection = engine.connect()
|
|
282
|
+
connection.close()
|
|
283
|
+
return {
|
|
284
|
+
'status': 'UP',
|
|
285
|
+
'enabled': True,
|
|
286
|
+
'url': db_manager.db_url
|
|
287
|
+
}
|
|
288
|
+
else:
|
|
289
|
+
return {
|
|
290
|
+
'status': 'DISABLED',
|
|
291
|
+
'enabled': False,
|
|
292
|
+
'reason': 'Database not configured'
|
|
293
|
+
}
|
|
294
|
+
except Exception as e:
|
|
295
|
+
return {
|
|
296
|
+
'status': 'DOWN',
|
|
297
|
+
'enabled': True,
|
|
298
|
+
'reason': str(e)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _check_nacos() -> dict:
|
|
303
|
+
"""检查Nacos健康状态"""
|
|
304
|
+
try:
|
|
305
|
+
config = _application_context.get_config() if _application_context is not None else {}
|
|
306
|
+
if not config.get('discovery', {}).get('enabled', False):
|
|
307
|
+
return {'status': 'DISABLED', 'enabled': False, 'reason': 'Nacos not configured'}
|
|
308
|
+
if nacos_client.is_healthy():
|
|
309
|
+
services = nacos_client.get_services()
|
|
310
|
+
return {
|
|
311
|
+
'status': 'UP',
|
|
312
|
+
'enabled': True,
|
|
313
|
+
'server_addr': nacos_client.server_addr,
|
|
314
|
+
'service_count': len(services) if services else 0
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
'status': 'DOWN',
|
|
318
|
+
'enabled': True,
|
|
319
|
+
'server_addr': nacos_client.server_addr,
|
|
320
|
+
'reason': 'Nacos liveness check failed'
|
|
321
|
+
}
|
|
322
|
+
except Exception as e:
|
|
323
|
+
return {
|
|
324
|
+
'status': 'DOWN',
|
|
325
|
+
'enabled': True,
|
|
326
|
+
'reason': str(e)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _check_rabbitmq() -> dict:
|
|
331
|
+
"""检查RabbitMQ健康状态"""
|
|
332
|
+
try:
|
|
333
|
+
config = _application_context.get_config() if _application_context is not None else {}
|
|
334
|
+
if not config.get('rabbitmq', {}).get('enabled', False):
|
|
335
|
+
return {'status': 'DISABLED', 'enabled': False, 'reason': 'RabbitMQ not configured'}
|
|
336
|
+
from spring.messaging.rabbitmq import rabbitmq_client
|
|
337
|
+
channel = rabbitmq_client._channel
|
|
338
|
+
if channel:
|
|
339
|
+
# 尝试声明一个临时队列
|
|
340
|
+
result = channel.queue_declare(queue='', exclusive=True)
|
|
341
|
+
channel.queue_delete(queue=result.method.queue)
|
|
342
|
+
return {
|
|
343
|
+
'status': 'UP',
|
|
344
|
+
'enabled': True,
|
|
345
|
+
'host': rabbitmq_client.host,
|
|
346
|
+
'port': rabbitmq_client.port
|
|
347
|
+
}
|
|
348
|
+
return {'status': 'DOWN', 'enabled': True, 'reason': 'RabbitMQ channel is unavailable'}
|
|
349
|
+
except ImportError:
|
|
350
|
+
return {
|
|
351
|
+
'status': 'DISABLED',
|
|
352
|
+
'enabled': False,
|
|
353
|
+
'reason': 'RabbitMQ not available (pika not installed)'
|
|
354
|
+
}
|
|
355
|
+
except Exception as e:
|
|
356
|
+
return {
|
|
357
|
+
'status': 'DOWN',
|
|
358
|
+
'enabled': True,
|
|
359
|
+
'reason': str(e)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _check_seata() -> dict:
|
|
364
|
+
"""检查Seata健康状态"""
|
|
365
|
+
from spring.cloud.seata import seata_manager
|
|
366
|
+
try:
|
|
367
|
+
config = _application_context.get_config() if _application_context is not None else {}
|
|
368
|
+
seata_config = config.get('seata', {}) or {}
|
|
369
|
+
if not seata_config.get('enabled', False):
|
|
370
|
+
return {'status': 'DISABLED', 'enabled': False, 'reason': 'Seata not configured'}
|
|
371
|
+
mode = str(seata_config.get('mode', 'local')).lower()
|
|
372
|
+
if mode == 'http':
|
|
373
|
+
return {
|
|
374
|
+
'status': 'DOWN', 'enabled': True,
|
|
375
|
+
'reason': 'Experimental HTTP compensation mode is not production-ready',
|
|
376
|
+
}
|
|
377
|
+
if mode == 'local':
|
|
378
|
+
return {
|
|
379
|
+
'status': 'DOWN', 'enabled': True,
|
|
380
|
+
'reason': 'Local mode does not provide distributed transaction guarantees',
|
|
381
|
+
}
|
|
382
|
+
if seata_manager._seata_client_initialized:
|
|
383
|
+
return {
|
|
384
|
+
'status': 'UP',
|
|
385
|
+
'enabled': True,
|
|
386
|
+
'server_addr': seata_manager.server_addr,
|
|
387
|
+
'application_id': seata_manager.application_id,
|
|
388
|
+
'transaction_group': seata_manager.transaction_group
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
'status': 'DOWN', 'enabled': True,
|
|
392
|
+
'reason': 'Distributed Seata client is not initialized',
|
|
393
|
+
}
|
|
394
|
+
except Exception as e:
|
|
395
|
+
return {
|
|
396
|
+
'status': 'DOWN',
|
|
397
|
+
'enabled': True,
|
|
398
|
+
'reason': str(e)
|
|
399
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import Callable, Dict, Any, List, Optional
|
|
2
|
+
from abc import ABC
|
|
3
|
+
import fnmatch
|
|
4
|
+
import inspect
|
|
5
|
+
from fastapi import Request, Response
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HandlerInterceptor(ABC):
|
|
9
|
+
def pre_handle(self, request: Request, handler: Callable) -> bool:
|
|
10
|
+
return True
|
|
11
|
+
|
|
12
|
+
def post_handle(self, request: Request, response: Response, handler: Callable) -> None:
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def after_completion(self, request: Request, response: Response, handler: Callable, exception: Optional[Exception] = None) -> None:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InterceptorRegistry:
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self._interceptors: List[HandlerInterceptor] = []
|
|
22
|
+
self._exclude_paths: List[str] = []
|
|
23
|
+
self._include_paths: List[str] = []
|
|
24
|
+
|
|
25
|
+
def add_interceptor(self, interceptor: HandlerInterceptor) -> 'InterceptorRegistry':
|
|
26
|
+
self._interceptors.append(interceptor)
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
def exclude_path_patterns(self, *patterns: str) -> 'InterceptorRegistry':
|
|
30
|
+
self._exclude_paths.extend(patterns)
|
|
31
|
+
return self
|
|
32
|
+
|
|
33
|
+
def include_path_patterns(self, *patterns: str) -> 'InterceptorRegistry':
|
|
34
|
+
self._include_paths.extend(patterns)
|
|
35
|
+
return self
|
|
36
|
+
|
|
37
|
+
def get_interceptors(self) -> List[HandlerInterceptor]:
|
|
38
|
+
return list(self._interceptors)
|
|
39
|
+
|
|
40
|
+
def should_intercept(self, path: str) -> bool:
|
|
41
|
+
if self._include_paths:
|
|
42
|
+
if not any(self._matches(path, p) for p in self._include_paths):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
if self._exclude_paths:
|
|
46
|
+
if any(self._matches(path, p) for p in self._exclude_paths):
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def _matches(path: str, pattern: str) -> bool:
|
|
53
|
+
"""Match Spring-style ``/**`` patterns and ordinary glob patterns."""
|
|
54
|
+
if pattern in {'/**', '**', '*'}:
|
|
55
|
+
return True
|
|
56
|
+
if pattern.endswith('/**'):
|
|
57
|
+
prefix = pattern[:-3].rstrip('/')
|
|
58
|
+
return path == prefix or path.startswith(prefix + '/')
|
|
59
|
+
return fnmatch.fnmatchcase(path, pattern) or path.startswith(pattern.rstrip('/') + '/')
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class InterceptorManager:
|
|
63
|
+
def __init__(self, registry: InterceptorRegistry):
|
|
64
|
+
self.registry = registry
|
|
65
|
+
|
|
66
|
+
async def apply_pre_handle(self, request: Request, handler: Callable) -> bool:
|
|
67
|
+
for interceptor in self.registry.get_interceptors():
|
|
68
|
+
if not self.registry.should_intercept(request.url.path):
|
|
69
|
+
continue
|
|
70
|
+
result = interceptor.pre_handle(request, handler)
|
|
71
|
+
if inspect.isawaitable(result):
|
|
72
|
+
result = await result
|
|
73
|
+
if not result:
|
|
74
|
+
return False
|
|
75
|
+
return True
|
|
76
|
+
|
|
77
|
+
async def apply_post_handle(self, request: Request, response: Response, handler: Callable) -> None:
|
|
78
|
+
for interceptor in self.registry.get_interceptors():
|
|
79
|
+
if not self.registry.should_intercept(request.url.path):
|
|
80
|
+
continue
|
|
81
|
+
result = interceptor.post_handle(request, response, handler)
|
|
82
|
+
if inspect.isawaitable(result):
|
|
83
|
+
await result
|
|
84
|
+
|
|
85
|
+
async def apply_after_completion(self, request: Request, response: Response, handler: Callable, exception: Optional[Exception] = None) -> None:
|
|
86
|
+
for interceptor in self.registry.get_interceptors():
|
|
87
|
+
if not self.registry.should_intercept(request.url.path):
|
|
88
|
+
continue
|
|
89
|
+
result = interceptor.after_completion(request, response, handler, exception)
|
|
90
|
+
if inspect.isawaitable(result):
|
|
91
|
+
await result
|
spring/web/result.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from typing import Generic, TypeVar, Optional, Any
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
T = TypeVar('T')
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Result(BaseModel, Generic[T]):
|
|
8
|
+
code: int
|
|
9
|
+
message: str
|
|
10
|
+
data: Optional[T] = None
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def success(cls, data: Optional[T] = None, message: str = "success") -> "Result[T]":
|
|
14
|
+
return cls(code=200, message=message, data=data)
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def error(cls, code: int = 500, message: str = "error") -> "Result[T]":
|
|
18
|
+
return cls(code=code, message=message, data=None)
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def bad_request(cls, message: str = "Bad Request") -> "Result[T]":
|
|
22
|
+
return cls(code=400, message=message, data=None)
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def unauthorized(cls, message: str = "Unauthorized") -> "Result[T]":
|
|
26
|
+
return cls(code=401, message=message, data=None)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def forbidden(cls, message: str = "Forbidden") -> "Result[T]":
|
|
30
|
+
return cls(code=403, message=message, data=None)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def not_found(cls, message: str = "Not Found") -> "Result[T]":
|
|
34
|
+
return cls(code=404, message=message, data=None)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def internal_error(cls, message: str = "Internal Server Error") -> "Result[T]":
|
|
38
|
+
return cls(code=500, message=message, data=None)
|
|
39
|
+
|
|
40
|
+
def is_success(self) -> bool:
|
|
41
|
+
return self.code == 200
|
|
42
|
+
|
|
43
|
+
def is_error(self) -> bool:
|
|
44
|
+
return self.code != 200
|