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/actuator.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Spring Boot Actuator 风格运维端点(扩展既有 ``/actuator/health``)。
|
|
2
|
+
|
|
3
|
+
在既有 health 端点(``spring.web.health``)基础上补齐标准 Actuator 端点:
|
|
4
|
+
``/actuator``(端点目录)、``/env``、``/loggers``、``/metrics``、``/beans``、
|
|
5
|
+
``/configprops``、``/mappings``、``/threaddump``。
|
|
6
|
+
|
|
7
|
+
设计:
|
|
8
|
+
- **纯函数 + 薄端点**:核心逻辑为接收 ``context`` 的纯函数,便于单测;路由函数仅做 HTTP 包装。
|
|
9
|
+
- **复用现有上下文**:与 ``health.py`` 共享 ``_application_context`` 全局(``configure_actuator`` 设置)。
|
|
10
|
+
- **脱敏**:``/env`` 自动屏蔽 password/secret/key/token/credential 等敏感键值(对齐 Spring Boot)。
|
|
11
|
+
- **loggers 动态调整**:GET 列出,POST ``/loggers/{name}`` 实时修改日志级别。
|
|
12
|
+
|
|
13
|
+
与 Java 差异:
|
|
14
|
+
- Python ``logging`` 无全局 logger 注册表,``/loggers`` 仅列出 root + ``Logger.manager.loggerDict`` 已实例化的 logger。
|
|
15
|
+
- ``/metrics`` 返回 JSON 指标名列表(Prometheus 文本格式仍由 ``/actuator/prometheus`` 提供)。
|
|
16
|
+
"""
|
|
17
|
+
import logging
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
20
|
+
import traceback
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
from fastapi import APIRouter, Body
|
|
24
|
+
from fastapi.responses import JSONResponse
|
|
25
|
+
|
|
26
|
+
actuator_router = APIRouter()
|
|
27
|
+
_application_context = None
|
|
28
|
+
|
|
29
|
+
# 敏感键关键词(命中即脱敏,对齐 Spring Boot env 脱敏)
|
|
30
|
+
_SENSITIVE_KEYS = ("password", "secret", "token", "credential", "passwd", "api_key", "apikey")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def configure_actuator(application_context) -> None:
|
|
34
|
+
"""注入应用上下文(与 ``health.configure_health_checks`` 平行调用)。"""
|
|
35
|
+
global _application_context
|
|
36
|
+
_application_context = application_context
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _get_context():
|
|
40
|
+
return _application_context
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ==================== 端点目录 ====================
|
|
44
|
+
|
|
45
|
+
def get_endpoint_directory() -> dict:
|
|
46
|
+
"""列出所有可用的 Actuator 端点(对齐 ``/actuator`` 根端点)。"""
|
|
47
|
+
endpoints = {
|
|
48
|
+
"health": {"href": "/actuator/health", "methods": ["GET"]},
|
|
49
|
+
"health-liveness": {"href": "/actuator/health/liveness", "methods": ["GET"]},
|
|
50
|
+
"health-readiness": {"href": "/actuator/health/readiness", "methods": ["GET"]},
|
|
51
|
+
"info": {"href": "/actuator/info", "methods": ["GET"]},
|
|
52
|
+
"env": {"href": "/actuator/env", "methods": ["GET"]},
|
|
53
|
+
"loggers": {"href": "/actuator/loggers", "methods": ["GET", "POST"]},
|
|
54
|
+
"metrics": {"href": "/actuator/metrics", "methods": ["GET"]},
|
|
55
|
+
"beans": {"href": "/actuator/beans", "methods": ["GET"]},
|
|
56
|
+
"configprops": {"href": "/actuator/configprops", "methods": ["GET"]},
|
|
57
|
+
"mappings": {"href": "/actuator/mappings", "methods": ["GET"]},
|
|
58
|
+
"threaddump": {"href": "/actuator/threaddump", "methods": ["GET"]},
|
|
59
|
+
"prometheus": {"href": "/actuator/prometheus", "methods": ["GET"]},
|
|
60
|
+
}
|
|
61
|
+
return {"_links": {k: v for k, v in endpoints.items()}}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ==================== /env 环境配置 ====================
|
|
65
|
+
|
|
66
|
+
def _sanitize(obj: Any) -> Any:
|
|
67
|
+
"""递归脱敏:键名命中敏感关键词的值替换为 ``******``。"""
|
|
68
|
+
if isinstance(obj, dict):
|
|
69
|
+
return {
|
|
70
|
+
k: ("******" if any(s in k.lower() for s in _SENSITIVE_KEYS) else _sanitize(v))
|
|
71
|
+
for k, v in obj.items()
|
|
72
|
+
}
|
|
73
|
+
if isinstance(obj, list):
|
|
74
|
+
return [_sanitize(item) for item in obj]
|
|
75
|
+
return obj
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_env_info(context) -> dict:
|
|
79
|
+
"""返回环境配置快照(脱敏)+ active profile。
|
|
80
|
+
|
|
81
|
+
对齐 Spring ``/actuator/env``:展示 property sources 与 active profile。
|
|
82
|
+
"""
|
|
83
|
+
result: Dict[str, Any] = {"activeProfiles": [], "propertySources": []}
|
|
84
|
+
if context is None:
|
|
85
|
+
return result
|
|
86
|
+
try:
|
|
87
|
+
active = context.config_loader.get_active_profile() if hasattr(context, "config_loader") else None
|
|
88
|
+
if active:
|
|
89
|
+
result["activeProfiles"] = [active]
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
try:
|
|
93
|
+
config = context.get_config() if hasattr(context, "get_config") else {}
|
|
94
|
+
result["propertySources"].append({
|
|
95
|
+
"name": "applicationConfig",
|
|
96
|
+
"properties": _sanitize(config) if isinstance(config, dict) else {},
|
|
97
|
+
})
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ==================== /loggers 日志级别 ====================
|
|
104
|
+
|
|
105
|
+
_LEVEL_NAMES = {
|
|
106
|
+
logging.DEBUG: "DEBUG", logging.INFO: "INFO", logging.WARNING: "WARNING",
|
|
107
|
+
logging.ERROR: "ERROR", logging.CRITICAL: "CRITICAL", logging.NOTSET: "NOTSET",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_loggers() -> dict:
|
|
112
|
+
"""列出 root + 已实例化的 logger 及其有效级别。"""
|
|
113
|
+
loggers: Dict[str, str] = {"ROOT": _LEVEL_NAMES.get(logging.getLogger().getEffectiveLevel(), "NOTSET")}
|
|
114
|
+
manager_dict = logging.Logger.manager.loggerDict
|
|
115
|
+
for name, logger in manager_dict.items():
|
|
116
|
+
if isinstance(logger, logging.Logger):
|
|
117
|
+
loggers[name] = _LEVEL_NAMES.get(logger.getEffectiveLevel(), "NOTSET")
|
|
118
|
+
return {"levels": ["TRACE", "DEBUG", "INFO", "WARNING", "ERROR", "FATAL", "OFF"],
|
|
119
|
+
"loggers": loggers}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_logger_level(name: str) -> Optional[dict]:
|
|
123
|
+
logger = logging.getLogger(name)
|
|
124
|
+
return {"configuredLevel": _LEVEL_NAMES.get(logger.level, "NOTSET") if logger.level else None,
|
|
125
|
+
"effectiveLevel": _LEVEL_NAMES.get(logger.getEffectiveLevel(), "NOTSET")}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def set_logger_level(name: str, level: str) -> dict:
|
|
129
|
+
"""动态修改 logger 级别。``name=root`` 修改 root logger。"""
|
|
130
|
+
target_name = "" if name.lower() == "root" else name
|
|
131
|
+
logger = logging.getLogger(target_name)
|
|
132
|
+
level_value = getattr(logging, level.upper(), None)
|
|
133
|
+
if level_value is None and level.upper() != "OFF":
|
|
134
|
+
raise ValueError(f"不支持的日志级别: {level}")
|
|
135
|
+
if level.upper() == "OFF":
|
|
136
|
+
logger.setLevel(logging.CRITICAL + 100)
|
|
137
|
+
else:
|
|
138
|
+
logger.setLevel(level_value)
|
|
139
|
+
return {"configuredLevel": level.upper()}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ==================== /metrics 指标列表 ====================
|
|
143
|
+
|
|
144
|
+
def get_metrics() -> dict:
|
|
145
|
+
"""返回可用指标名列表(JSON 视图;Prometheus 文本格式见 /actuator/prometheus)。"""
|
|
146
|
+
names: List[str] = []
|
|
147
|
+
try:
|
|
148
|
+
from spring.monitoring.prometheus import prometheus_metrics
|
|
149
|
+
metrics_map = prometheus_metrics.get_metrics()
|
|
150
|
+
names = list(metrics_map.keys())
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
return {"names": names}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ==================== /beans Bean 列表 ====================
|
|
157
|
+
|
|
158
|
+
def get_beans(context) -> dict:
|
|
159
|
+
"""列出 IoC 容器中所有 Bean 的元信息。"""
|
|
160
|
+
beans: Dict[str, Any] = {}
|
|
161
|
+
if context is not None:
|
|
162
|
+
try:
|
|
163
|
+
factory = context.bean_factory
|
|
164
|
+
except AttributeError:
|
|
165
|
+
factory = None
|
|
166
|
+
if factory is not None:
|
|
167
|
+
for name in factory.get_bean_names():
|
|
168
|
+
definition = factory.get_bean_definition(name)
|
|
169
|
+
if definition is None:
|
|
170
|
+
beans[name] = {"type": "unknown", "scope": "unknown"}
|
|
171
|
+
continue
|
|
172
|
+
bean_class = definition.bean_class
|
|
173
|
+
type_name = getattr(bean_class, "__name__", str(bean_class)) if bean_class else "unknown"
|
|
174
|
+
beans[name] = {
|
|
175
|
+
"type": type_name,
|
|
176
|
+
"scope": getattr(definition, "scope", "singleton"),
|
|
177
|
+
"singleton": getattr(definition, "is_singleton", True),
|
|
178
|
+
}
|
|
179
|
+
return {"contexts": {"application": {"beans": beans}}}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ==================== /configprops 配置属性绑定 ====================
|
|
183
|
+
|
|
184
|
+
def get_configprops(context) -> dict:
|
|
185
|
+
"""列出 ``@ConfigurationProperties`` 绑定的配置前缀与值(脱敏)。"""
|
|
186
|
+
result: Dict[str, Any] = {}
|
|
187
|
+
if context is None:
|
|
188
|
+
return result
|
|
189
|
+
try:
|
|
190
|
+
factory = context.bean_factory
|
|
191
|
+
except AttributeError:
|
|
192
|
+
return result
|
|
193
|
+
for name in factory.get_bean_names():
|
|
194
|
+
definition = factory.get_bean_definition(name)
|
|
195
|
+
if definition is None:
|
|
196
|
+
continue
|
|
197
|
+
props = definition.annotations.get("properties", []) if hasattr(definition, "annotations") else []
|
|
198
|
+
if not props:
|
|
199
|
+
continue
|
|
200
|
+
for prop_ann in props:
|
|
201
|
+
prefix = getattr(prop_ann, "prefix", None)
|
|
202
|
+
if not prefix:
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
config = context.get_config()
|
|
206
|
+
bound = context.config_loader.get_prefix_config(prefix) if hasattr(context, "config_loader") else {}
|
|
207
|
+
except Exception:
|
|
208
|
+
bound = {}
|
|
209
|
+
result[prefix] = {"prefix": prefix, "properties": _sanitize(bound)}
|
|
210
|
+
return {"contexts": {"application": {"beans": result}}}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ==================== /mappings 路由映射 ====================
|
|
214
|
+
|
|
215
|
+
def get_mappings(app) -> dict:
|
|
216
|
+
"""列出 FastAPI 应用的 HTTP 路由映射。"""
|
|
217
|
+
mappings: List[Dict[str, Any]] = []
|
|
218
|
+
if app is None:
|
|
219
|
+
return {"contexts": {"application": {"mappings": {"dispatcherServlets": {"dispatcherServlet": mappings}}}}}
|
|
220
|
+
for route in getattr(app, "routes", []):
|
|
221
|
+
path = getattr(route, "path", None)
|
|
222
|
+
methods = getattr(route, "methods", None)
|
|
223
|
+
if path is None:
|
|
224
|
+
continue
|
|
225
|
+
mappings.append({
|
|
226
|
+
"path": path,
|
|
227
|
+
"methods": sorted(list(methods)) if methods else ["GET"],
|
|
228
|
+
"name": getattr(route, "name", ""),
|
|
229
|
+
})
|
|
230
|
+
return {"contexts": {"application": {"mappings": {"dispatcherServlets": {"dispatcherServlet": mappings}}}}}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ==================== /threaddump 线程转储 ====================
|
|
234
|
+
|
|
235
|
+
def get_threaddump() -> dict:
|
|
236
|
+
"""返回当前进程所有线程的转储(id/name/alive/daemon/stack)。"""
|
|
237
|
+
import sys
|
|
238
|
+
frames_map = sys._current_frames()
|
|
239
|
+
threads = []
|
|
240
|
+
for thread in threading.enumerate():
|
|
241
|
+
frames = []
|
|
242
|
+
frame = frames_map.get(thread.ident)
|
|
243
|
+
if frame is not None:
|
|
244
|
+
try:
|
|
245
|
+
for filename, lineno, name, _line in traceback.extract_stack(frame):
|
|
246
|
+
frames.append({"file": filename, "line": lineno, "method": name})
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
threads.append({
|
|
250
|
+
"threadId": thread.ident,
|
|
251
|
+
"threadName": thread.name,
|
|
252
|
+
"threadState": "RUNNABLE" if thread.is_alive() else "TERMINATED",
|
|
253
|
+
"daemon": thread.daemon,
|
|
254
|
+
"stack": frames,
|
|
255
|
+
})
|
|
256
|
+
return {"threads": threads}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ==================== HTTP 端点(薄包装) ====================
|
|
260
|
+
|
|
261
|
+
@actuator_router.get('')
|
|
262
|
+
@actuator_router.get('/')
|
|
263
|
+
def actuator_root():
|
|
264
|
+
return JSONResponse(content=get_endpoint_directory(), status_code=200)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@actuator_router.get('/env')
|
|
268
|
+
def env_endpoint():
|
|
269
|
+
return JSONResponse(content=get_env_info(_get_context()), status_code=200)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@actuator_router.get('/loggers')
|
|
273
|
+
def loggers_endpoint():
|
|
274
|
+
return JSONResponse(content=get_loggers(), status_code=200)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@actuator_router.get('/loggers/{name}')
|
|
278
|
+
def logger_detail(name: str):
|
|
279
|
+
return JSONResponse(content=get_logger_level(name), status_code=200)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@actuator_router.post('/loggers/{name}')
|
|
283
|
+
def logger_update(name: str, body: dict = Body(default={})):
|
|
284
|
+
"""请求体 ``{"configuredLevel": "DEBUG"}`` 动态修改级别。"""
|
|
285
|
+
level = (body or {}).get("configuredLevel", "INFO")
|
|
286
|
+
try:
|
|
287
|
+
result = set_logger_level(name, level)
|
|
288
|
+
return JSONResponse(content=result, status_code=200)
|
|
289
|
+
except ValueError as e:
|
|
290
|
+
return JSONResponse(content={"error": str(e)}, status_code=400)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@actuator_router.get('/metrics')
|
|
294
|
+
def metrics_endpoint():
|
|
295
|
+
return JSONResponse(content=get_metrics(), status_code=200)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@actuator_router.get('/beans')
|
|
299
|
+
def beans_endpoint():
|
|
300
|
+
return JSONResponse(content=get_beans(_get_context()), status_code=200)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@actuator_router.get('/configprops')
|
|
304
|
+
def configprops_endpoint():
|
|
305
|
+
return JSONResponse(content=get_configprops(_get_context()), status_code=200)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
@actuator_router.get('/mappings')
|
|
309
|
+
def mappings_endpoint():
|
|
310
|
+
app = None
|
|
311
|
+
ctx = _get_context()
|
|
312
|
+
if ctx is not None and hasattr(ctx, "web_context"):
|
|
313
|
+
app = ctx.web_context.get_app()
|
|
314
|
+
return JSONResponse(content=get_mappings(app), status_code=200)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@actuator_router.get('/threaddump')
|
|
318
|
+
def threaddump_endpoint():
|
|
319
|
+
return JSONResponse(content=get_threaddump(), status_code=200)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Type, Callable, Dict, Any, Optional
|
|
2
|
+
from fastapi import Request
|
|
3
|
+
from spring.web.result import Result
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GlobalExceptionHandler:
|
|
8
|
+
def __init__(self, show_details: bool = False):
|
|
9
|
+
self._handlers: Dict[Type[Exception], Callable] = {}
|
|
10
|
+
self._show_details = show_details
|
|
11
|
+
self._logger = logging.getLogger("Spring.ExceptionHandler")
|
|
12
|
+
|
|
13
|
+
def add_exception_handler(self, exception_type: Type[Exception], handler: Callable) -> None:
|
|
14
|
+
self._handlers[exception_type] = handler
|
|
15
|
+
|
|
16
|
+
def get_handler(self, exception_type: Type[Exception]) -> Optional[Callable]:
|
|
17
|
+
if exception_type in self._handlers:
|
|
18
|
+
return self._handlers[exception_type]
|
|
19
|
+
|
|
20
|
+
for registered_type, handler in self._handlers.items():
|
|
21
|
+
if issubclass(exception_type, registered_type):
|
|
22
|
+
return handler
|
|
23
|
+
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
def handle(self, exception: Exception) -> Result:
|
|
27
|
+
handler = self.get_handler(type(exception))
|
|
28
|
+
if handler:
|
|
29
|
+
try:
|
|
30
|
+
result = handler(exception)
|
|
31
|
+
if isinstance(result, Result):
|
|
32
|
+
return result
|
|
33
|
+
return Result.error(message=str(result))
|
|
34
|
+
except Exception as e:
|
|
35
|
+
self._logger.error(f"Exception handler failed: {str(e)}")
|
|
36
|
+
return Result.error(message="Internal server error")
|
|
37
|
+
|
|
38
|
+
return self._default_handler(exception)
|
|
39
|
+
|
|
40
|
+
def _default_handler(self, exception: Exception) -> Result:
|
|
41
|
+
import traceback
|
|
42
|
+
self._logger.error(f"Unexpected error: {str(exception)}")
|
|
43
|
+
self._logger.error(traceback.format_exc())
|
|
44
|
+
|
|
45
|
+
if self._show_details:
|
|
46
|
+
return Result.error(message=f"Unexpected error: {str(exception)}")
|
|
47
|
+
return Result.error(message="Internal server error")
|
|
48
|
+
|
|
49
|
+
def register_default_handlers(self) -> None:
|
|
50
|
+
self.add_exception_handler(ValueError, self._handle_value_error)
|
|
51
|
+
self.add_exception_handler(TypeError, self._handle_type_error)
|
|
52
|
+
self.add_exception_handler(Exception, self._default_handler)
|
|
53
|
+
|
|
54
|
+
def _handle_value_error(self, exception: ValueError) -> Result:
|
|
55
|
+
return Result.bad_request(message=str(exception))
|
|
56
|
+
|
|
57
|
+
def _handle_type_error(self, exception: TypeError) -> Result:
|
|
58
|
+
return Result.bad_request(message=str(exception))
|
|
59
|
+
|
|
60
|
+
def set_show_details(self, show_details: bool) -> None:
|
|
61
|
+
self._show_details = show_details
|