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/cloud/tracer.py
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""
|
|
2
|
+
原生 OpenTelemetry 兼容分布式追踪 (Native OpenTelemetry-compatible Tracer)
|
|
3
|
+
|
|
4
|
+
实现 W3C Trace Context 标准的分布式追踪,无需外部 SkyWalking agent:
|
|
5
|
+
- 生成/传播 traceId, spanId (W3C traceparent header)
|
|
6
|
+
- 支持 HTTP 自动注入/提取
|
|
7
|
+
- 支持 Feign 跨服务调用追踪
|
|
8
|
+
- 支持方法级 @Trace 注解
|
|
9
|
+
- 支持导出到日志/控制台,兼容 Zipkin/Jaeger OTLP 格式(可选)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
import random
|
|
15
|
+
import threading
|
|
16
|
+
import logging
|
|
17
|
+
import functools
|
|
18
|
+
from typing import Dict, List, Optional, Any, Callable
|
|
19
|
+
from contextvars import ContextVar
|
|
20
|
+
from enum import Enum
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("Spring.Cloud.Tracer")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SpanKind(Enum):
|
|
26
|
+
INTERNAL = "INTERNAL"
|
|
27
|
+
SERVER = "SERVER"
|
|
28
|
+
CLIENT = "CLIENT"
|
|
29
|
+
PRODUCER = "PRODUCER"
|
|
30
|
+
CONSUMER = "CONSUMER"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SpanStatus(Enum):
|
|
34
|
+
OK = "OK"
|
|
35
|
+
ERROR = "ERROR"
|
|
36
|
+
UNSET = "UNSET"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Span:
|
|
40
|
+
"""追踪 Span"""
|
|
41
|
+
__slots__ = (
|
|
42
|
+
'trace_id', 'span_id', 'parent_span_id', 'name', 'kind',
|
|
43
|
+
'start_time_ns', 'end_time_ns', 'attributes', 'events',
|
|
44
|
+
'status', 'status_description', 'service_name', '_ended'
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __init__(self, trace_id: str, span_id: str, parent_span_id: Optional[str],
|
|
48
|
+
name: str, kind: SpanKind, service_name: str = "unknown"):
|
|
49
|
+
self.trace_id = trace_id
|
|
50
|
+
self.span_id = span_id
|
|
51
|
+
self.parent_span_id = parent_span_id
|
|
52
|
+
self.name = name
|
|
53
|
+
self.kind = kind
|
|
54
|
+
self.start_time_ns = time.time_ns()
|
|
55
|
+
self.end_time_ns = 0
|
|
56
|
+
self.attributes: Dict[str, Any] = {}
|
|
57
|
+
self.events: List[Dict[str, Any]] = []
|
|
58
|
+
self.status = SpanStatus.UNSET
|
|
59
|
+
self.status_description = ""
|
|
60
|
+
self.service_name = service_name
|
|
61
|
+
self._ended = False
|
|
62
|
+
|
|
63
|
+
def set_attribute(self, key: str, value: Any):
|
|
64
|
+
self.attributes[key] = value
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def set_status(self, status: SpanStatus, description: str = ""):
|
|
68
|
+
self.status = status
|
|
69
|
+
self.status_description = description
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def add_event(self, name: str, attributes: Dict[str, Any] = None):
|
|
73
|
+
self.events.append({
|
|
74
|
+
'name': name,
|
|
75
|
+
'timestamp_ns': time.time_ns(),
|
|
76
|
+
'attributes': attributes or {},
|
|
77
|
+
})
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def record_exception(self, exception: Exception):
|
|
81
|
+
self.add_event("exception", {
|
|
82
|
+
'exception.type': type(exception).__name__,
|
|
83
|
+
'exception.message': str(exception),
|
|
84
|
+
})
|
|
85
|
+
self.set_status(SpanStatus.ERROR, str(exception))
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
def end(self):
|
|
89
|
+
if not self._ended:
|
|
90
|
+
self.end_time_ns = time.time_ns()
|
|
91
|
+
if self.status == SpanStatus.UNSET:
|
|
92
|
+
self.status = SpanStatus.OK
|
|
93
|
+
self._ended = True
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def duration_ms(self) -> float:
|
|
97
|
+
end = self.end_time_ns or time.time_ns()
|
|
98
|
+
return (end - self.start_time_ns) / 1_000_000
|
|
99
|
+
|
|
100
|
+
def to_dict(self) -> dict:
|
|
101
|
+
return {
|
|
102
|
+
'traceId': self.trace_id,
|
|
103
|
+
'id': self.span_id,
|
|
104
|
+
'parentId': self.parent_span_id or '',
|
|
105
|
+
'name': self.name,
|
|
106
|
+
'kind': self.kind.value,
|
|
107
|
+
'timestamp': self.start_time_ns // 1000, # microseconds for OTLP
|
|
108
|
+
'duration': (self.end_time_ns - self.start_time_ns) // 1000 if self.end_time_ns else 0,
|
|
109
|
+
'localEndpoint': {'serviceName': self.service_name},
|
|
110
|
+
'tags': self.attributes,
|
|
111
|
+
'annotations': [{'timestamp': e['timestamp_ns'] // 1000, 'value': e['name']} for e in self.events],
|
|
112
|
+
'tags': {**self.attributes, **{
|
|
113
|
+
'otel.status_code': self.status.value,
|
|
114
|
+
'error': self.status_description if self.status == SpanStatus.ERROR else '',
|
|
115
|
+
}},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _generate_trace_id() -> str:
|
|
120
|
+
"""生成 W3C 兼容 32-hex traceId"""
|
|
121
|
+
return uuid.uuid4().hex
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _generate_span_id() -> str:
|
|
125
|
+
"""生成 16-hex spanId"""
|
|
126
|
+
return '%016x' % random.getrandbits(64)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _parse_traceparent(header: str) -> Optional[tuple]:
|
|
130
|
+
"""解析 W3C traceparent header: 00-traceid-spanid-flags"""
|
|
131
|
+
try:
|
|
132
|
+
parts = header.strip().split('-')
|
|
133
|
+
if len(parts) >= 4:
|
|
134
|
+
version = parts[0]
|
|
135
|
+
trace_id = parts[1]
|
|
136
|
+
span_id = parts[2]
|
|
137
|
+
flags = parts[3]
|
|
138
|
+
if len(trace_id) == 32 and len(span_id) == 16:
|
|
139
|
+
return trace_id, span_id, flags
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _build_traceparent(trace_id: str, span_id: str, sampled: bool = True) -> str:
|
|
146
|
+
flags = '01' if sampled else '00'
|
|
147
|
+
return f"00-{trace_id}-{span_id}-{flags}"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Tracer:
|
|
151
|
+
"""
|
|
152
|
+
OpenTelemetry 兼容追踪器
|
|
153
|
+
|
|
154
|
+
Usage:
|
|
155
|
+
tracer = Tracer("my-service")
|
|
156
|
+
with tracer.span("my-operation") as span:
|
|
157
|
+
span.set_attribute("http.method", "GET")
|
|
158
|
+
...
|
|
159
|
+
"""
|
|
160
|
+
def __init__(self, service_name: str = "springpy-app", enabled: bool = True,
|
|
161
|
+
sample_rate: float = 1.0, export_to_log: bool = True):
|
|
162
|
+
self.service_name = service_name
|
|
163
|
+
self.enabled = enabled
|
|
164
|
+
self.sample_rate = sample_rate
|
|
165
|
+
self.export_to_log = export_to_log
|
|
166
|
+
self._spans: List[Span] = []
|
|
167
|
+
self._lock = threading.Lock()
|
|
168
|
+
self._span_stack: ContextVar[List[Span]] = ContextVar('span_stack', default=[])
|
|
169
|
+
|
|
170
|
+
def _should_sample(self) -> bool:
|
|
171
|
+
if self.sample_rate >= 1.0:
|
|
172
|
+
return True
|
|
173
|
+
if self.sample_rate <= 0:
|
|
174
|
+
return False
|
|
175
|
+
return random.random() < self.sample_rate
|
|
176
|
+
|
|
177
|
+
def start_span(self, name: str, kind: SpanKind = SpanKind.INTERNAL,
|
|
178
|
+
attributes: Dict[str, Any] = None,
|
|
179
|
+
traceparent: str = None) -> Span:
|
|
180
|
+
if not self.enabled or not self._should_sample():
|
|
181
|
+
# 返回一个空span(disabled)
|
|
182
|
+
span = Span("0" * 32, "0" * 16, None, name, kind, self.service_name)
|
|
183
|
+
span._ended = True
|
|
184
|
+
return span
|
|
185
|
+
|
|
186
|
+
# 从context或traceparent获取父span
|
|
187
|
+
stack = self._span_stack.get()
|
|
188
|
+
parent_trace_id = None
|
|
189
|
+
parent_span_id = None
|
|
190
|
+
|
|
191
|
+
if traceparent:
|
|
192
|
+
parsed = _parse_traceparent(traceparent)
|
|
193
|
+
if parsed:
|
|
194
|
+
parent_trace_id, parent_span_id, _ = parsed
|
|
195
|
+
|
|
196
|
+
if parent_trace_id is None:
|
|
197
|
+
if stack:
|
|
198
|
+
parent = stack[-1]
|
|
199
|
+
parent_trace_id = parent.trace_id
|
|
200
|
+
parent_span_id = parent.span_id
|
|
201
|
+
else:
|
|
202
|
+
parent_trace_id = _generate_trace_id()
|
|
203
|
+
|
|
204
|
+
span_id = _generate_span_id()
|
|
205
|
+
span = Span(parent_trace_id, span_id, parent_span_id, name, kind, self.service_name)
|
|
206
|
+
if attributes:
|
|
207
|
+
span.attributes.update(attributes)
|
|
208
|
+
|
|
209
|
+
new_stack = list(stack) + [span]
|
|
210
|
+
self._span_stack.set(new_stack)
|
|
211
|
+
return span
|
|
212
|
+
|
|
213
|
+
def end_span(self, span: Span):
|
|
214
|
+
if span._ended or span.trace_id == "0" * 32:
|
|
215
|
+
return
|
|
216
|
+
span.end()
|
|
217
|
+
# 从栈中移除
|
|
218
|
+
stack = self._span_stack.get()
|
|
219
|
+
if stack and stack[-1] is span:
|
|
220
|
+
self._span_stack.set(stack[:-1])
|
|
221
|
+
with self._lock:
|
|
222
|
+
self._spans.append(span)
|
|
223
|
+
if self.export_to_log and span.status == SpanStatus.ERROR:
|
|
224
|
+
logger.error(f"[Trace] {span.trace_id[:16]}... {span.name} "
|
|
225
|
+
f"status={span.status.value} duration={span.duration_ms:.2f}ms error={span.status_description}")
|
|
226
|
+
elif self.export_to_log:
|
|
227
|
+
logger.debug(f"[Trace] {span.trace_id[:16]}... {span.name} "
|
|
228
|
+
f"duration={span.duration_ms:.2f}ms")
|
|
229
|
+
|
|
230
|
+
def span(self, name: str, kind: SpanKind = SpanKind.INTERNAL,
|
|
231
|
+
attributes: Dict[str, Any] = None, traceparent: str = None):
|
|
232
|
+
return _SpanContext(self, name, kind, attributes, traceparent)
|
|
233
|
+
|
|
234
|
+
def get_current_span(self) -> Optional[Span]:
|
|
235
|
+
stack = self._span_stack.get()
|
|
236
|
+
return stack[-1] if stack else None
|
|
237
|
+
|
|
238
|
+
def get_traceparent_header(self) -> str:
|
|
239
|
+
span = self.get_current_span()
|
|
240
|
+
if span and span.trace_id != "0" * 32:
|
|
241
|
+
return _build_traceparent(span.trace_id, span.span_id)
|
|
242
|
+
return ""
|
|
243
|
+
|
|
244
|
+
def inject_headers(self, headers: Dict[str, str]) -> Dict[str, str]:
|
|
245
|
+
"""将traceparent注入HTTP headers(用于Feign跨服务调用)"""
|
|
246
|
+
tp = self.get_traceparent_header()
|
|
247
|
+
if tp:
|
|
248
|
+
headers['traceparent'] = tp
|
|
249
|
+
headers['X-B3-TraceId'] = self.get_current_span().trace_id
|
|
250
|
+
headers['X-B3-SpanId'] = self.get_current_span().span_id
|
|
251
|
+
return headers
|
|
252
|
+
|
|
253
|
+
def extract_from_headers(self, headers: Dict[str, str]) -> Optional[str]:
|
|
254
|
+
"""从HTTP headers提取traceparent"""
|
|
255
|
+
if not headers:
|
|
256
|
+
return None
|
|
257
|
+
tp = headers.get('traceparent') or headers.get('Traceparent')
|
|
258
|
+
if not tp:
|
|
259
|
+
# 尝试 B3 格式
|
|
260
|
+
tid = headers.get('X-B3-TraceId')
|
|
261
|
+
sid = headers.get('X-B3-SpanId')
|
|
262
|
+
if tid and sid:
|
|
263
|
+
tp = f"00-{tid}-sid-01"
|
|
264
|
+
return tp
|
|
265
|
+
|
|
266
|
+
def get_spans(self, trace_id: str = None) -> List[Span]:
|
|
267
|
+
with self._lock:
|
|
268
|
+
if trace_id:
|
|
269
|
+
return [s for s in self._spans if s.trace_id == trace_id]
|
|
270
|
+
return list(self._spans)
|
|
271
|
+
|
|
272
|
+
def clear(self):
|
|
273
|
+
with self._lock:
|
|
274
|
+
self._spans.clear()
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class _SpanContext:
|
|
278
|
+
"""Span 上下文管理器"""
|
|
279
|
+
def __init__(self, tracer: Tracer, name: str, kind: SpanKind,
|
|
280
|
+
attributes: Dict[str, Any], traceparent: str):
|
|
281
|
+
self.tracer = tracer
|
|
282
|
+
self.name = name
|
|
283
|
+
self.kind = kind
|
|
284
|
+
self.attributes = attributes
|
|
285
|
+
self.traceparent = traceparent
|
|
286
|
+
self.span = None
|
|
287
|
+
|
|
288
|
+
def __enter__(self):
|
|
289
|
+
self.span = self.tracer.start_span(self.name, self.kind,
|
|
290
|
+
self.attributes, self.traceparent)
|
|
291
|
+
return self.span
|
|
292
|
+
|
|
293
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
294
|
+
if exc_val is not None:
|
|
295
|
+
self.span.record_exception(exc_val)
|
|
296
|
+
self.tracer.end_span(self.span)
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# 全局Tracer实例
|
|
301
|
+
_tracer_instance: Optional[Tracer] = None
|
|
302
|
+
_tracer_lock = threading.Lock()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def get_tracer(service_name: str = "springpy-app", **kwargs) -> Tracer:
|
|
306
|
+
global _tracer_instance
|
|
307
|
+
if _tracer_instance is None:
|
|
308
|
+
with _tracer_lock:
|
|
309
|
+
if _tracer_instance is None:
|
|
310
|
+
_tracer_instance = Tracer(service_name, **kwargs)
|
|
311
|
+
return _tracer_instance
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def trace_span(name: str = "", kind: SpanKind = SpanKind.INTERNAL):
|
|
315
|
+
"""
|
|
316
|
+
@Trace 方法级追踪注解装饰器
|
|
317
|
+
|
|
318
|
+
Usage:
|
|
319
|
+
@trace_span("my-method")
|
|
320
|
+
def my_method():
|
|
321
|
+
...
|
|
322
|
+
"""
|
|
323
|
+
def decorator(func: Callable) -> Callable:
|
|
324
|
+
span_name = name or f"{func.__module__}.{func.__qualname__}"
|
|
325
|
+
|
|
326
|
+
@functools.wraps(func)
|
|
327
|
+
def wrapper(*args, **kwargs):
|
|
328
|
+
tracer = get_tracer()
|
|
329
|
+
with tracer.span(span_name, kind) as span:
|
|
330
|
+
try:
|
|
331
|
+
result = func(*args, **kwargs)
|
|
332
|
+
return result
|
|
333
|
+
except Exception as e:
|
|
334
|
+
span.record_exception(e)
|
|
335
|
+
raise
|
|
336
|
+
return wrapper
|
|
337
|
+
return decorator
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
配置模块
|
|
3
|
+
提供配置加载和管理功能
|
|
4
|
+
"""
|
|
5
|
+
from .config_loader import (
|
|
6
|
+
ConfigLoader,
|
|
7
|
+
ConfigurationError,
|
|
8
|
+
config_loader,
|
|
9
|
+
set_global_config_loader,
|
|
10
|
+
get_config,
|
|
11
|
+
get_config_value,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'ConfigLoader',
|
|
16
|
+
'ConfigurationError',
|
|
17
|
+
'config_loader',
|
|
18
|
+
'set_global_config_loader',
|
|
19
|
+
'get_config',
|
|
20
|
+
'get_config_value',
|
|
21
|
+
]
|
spring/config/binding.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""配置属性松散绑定与校验(对齐 Spring Boot ``@ConfigurationProperties`` 松散绑定 +
|
|
2
|
+
``@NestedConfigurationProperties`` + ``@Validated``)。
|
|
3
|
+
|
|
4
|
+
能力:
|
|
5
|
+
- **松散绑定**:``kebab-case`` / ``camelCase`` / ``snake_case`` / ``SCREAMING_SNAKE`` 等命名
|
|
6
|
+
规范化后等价匹配(对齐 Spring ``RelaxedNames``)。如 ``max-connections``、``maxConnections``、
|
|
7
|
+
``max_connections`` 均绑定到属性 ``max_connections``。
|
|
8
|
+
- **嵌套绑定**:属性类型标注为 ``@NestedConfigurationProperties`` 类时,递归绑定子字典到该类实例。
|
|
9
|
+
- **类型强转**:标量值按属性类型注解强转(``int``/``float``/``bool``/``str``),避免 YAML 字符串误绑。
|
|
10
|
+
- **校验**:类上标注 ``@Validated`` 时,绑定后调用 ``BeanValidator.validate_or_raise``,
|
|
11
|
+
违反约束抛 ``ValidationError``(对齐 Spring ``@Validated`` + Hibernate Validator)。
|
|
12
|
+
|
|
13
|
+
设计:
|
|
14
|
+
- **复用既有范式**:``@NestedConfigurationProperties`` 继承 ``SpringAnnotation``;校验复用
|
|
15
|
+
``spring.validation.BeanValidator``,不重复造轮子。
|
|
16
|
+
- **独立可测**:``ConfigurationPropertiesBinder.bind`` 为静态方法,无需 IoC 容器即可使用。
|
|
17
|
+
- **集成点**:``ApplicationContext._apply_configuration_properties`` 调用本绑定器替代原扁平绑定。
|
|
18
|
+
|
|
19
|
+
与 Java 的差异:
|
|
20
|
+
- Spring 用 ``Binder`` + ``BeanBinder``;本实现用反射 + ``get_type_hints``,简化但覆盖常见场景。
|
|
21
|
+
- 不支持 ``Duration``/``DataSize`` 等专用转换器(可按需扩展 ``_coerce``)。
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
import re
|
|
27
|
+
from typing import Any, Dict, Optional, Type, get_type_hints
|
|
28
|
+
|
|
29
|
+
from spring.annotations.core import SpringAnnotation, Validated
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger("Spring.Config.Binding")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NestedConfigurationProperties(SpringAnnotation):
|
|
35
|
+
"""``@NestedConfigurationProperties`` 标记一个类可作为嵌套配置属性持有者。
|
|
36
|
+
|
|
37
|
+
标注后,父 ``@ConfigurationProperties`` 绑定时遇到该类型的属性会递归绑定子字典。
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
_annotation_type = "nested_properties"
|
|
41
|
+
|
|
42
|
+
def __init__(self, prefix: str = ""):
|
|
43
|
+
super().__init__(prefix=prefix)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# 匹配非字母数字字符(kebab/camel/snake 分隔符)用于规范化
|
|
47
|
+
_NON_ALNUM = re.compile(r'[^0-9a-zA-Z]+')
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _normalize(name: str) -> str:
|
|
51
|
+
"""规范化命名:去除分隔符并小写,用于松散匹配。
|
|
52
|
+
|
|
53
|
+
``max-connections`` / ``maxConnections`` / ``max_connections`` / ``MAX_CONNECTIONS``
|
|
54
|
+
→ ``maxconnections``。
|
|
55
|
+
"""
|
|
56
|
+
if not isinstance(name, str):
|
|
57
|
+
return str(name).lower()
|
|
58
|
+
return _NON_ALNUM.sub('', name).lower()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _is_nested_config_class(cls: Optional[type]) -> bool:
|
|
62
|
+
"""判断类是否标注 ``@NestedConfigurationProperties``。"""
|
|
63
|
+
if cls is None or not isinstance(cls, type):
|
|
64
|
+
return False
|
|
65
|
+
annotations = getattr(cls, '__spring_annotations__', [])
|
|
66
|
+
return any(isinstance(a, NestedConfigurationProperties) for a in annotations)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _coerce(value: Any, target_type: Optional[type]) -> Any:
|
|
70
|
+
"""按目标类型强转标量值;无法强转则原样返回(交由校验器报错)。"""
|
|
71
|
+
if target_type is None or value is None:
|
|
72
|
+
return value
|
|
73
|
+
# 已经是目标类型,直接返回
|
|
74
|
+
if isinstance(value, target_type):
|
|
75
|
+
return value
|
|
76
|
+
try:
|
|
77
|
+
if target_type is bool:
|
|
78
|
+
# YAML 已把 true/false 解析为 bool;字符串场景兜底
|
|
79
|
+
if isinstance(value, str):
|
|
80
|
+
return value.strip().lower() in ('true', '1', 'yes', 'on')
|
|
81
|
+
return bool(value)
|
|
82
|
+
if target_type is int:
|
|
83
|
+
return int(value)
|
|
84
|
+
if target_type is float:
|
|
85
|
+
return float(value)
|
|
86
|
+
if target_type is str:
|
|
87
|
+
return str(value)
|
|
88
|
+
except (ValueError, TypeError):
|
|
89
|
+
return value # 强转失败保留原值,由校验器/使用方处理
|
|
90
|
+
return value
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ConfigurationPropertiesBinder:
|
|
94
|
+
"""配置属性松散绑定器(静态方法风格,无状态)。"""
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _resolve_attr_name(instance: Any, config_key: str, candidates: Dict[str, str]) -> Optional[str]:
|
|
98
|
+
"""松散匹配:把 config_key 规范化后在属性候选表中查找。
|
|
99
|
+
|
|
100
|
+
``candidates`` 为 ``{normalized_attr_name: actual_attr_name}``。
|
|
101
|
+
"""
|
|
102
|
+
normalized = _normalize(config_key)
|
|
103
|
+
return candidates.get(normalized)
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _attr_candidates(instance: Any) -> Dict[str, str]:
|
|
107
|
+
"""构造属性候选表:``{规范化属性名: 实际属性名}``。
|
|
108
|
+
|
|
109
|
+
综合三类来源,确保 ``__init__`` 赋值的实例属性、类级注解属性、继承属性均可命中:
|
|
110
|
+
1. 类级类型注解(``get_type_hints``,最可靠,含继承)
|
|
111
|
+
2. 实例 ``__dict__``(``__init__`` 赋值的属性)
|
|
112
|
+
3. ``dir(cls)`` 类属性兜底
|
|
113
|
+
"""
|
|
114
|
+
candidates: Dict[str, str] = {}
|
|
115
|
+
cls = type(instance)
|
|
116
|
+
# 1. 类型注解(含继承,最可靠的属性名来源)
|
|
117
|
+
try:
|
|
118
|
+
for attr in get_type_hints(cls):
|
|
119
|
+
if not attr.startswith('_'):
|
|
120
|
+
candidates[_normalize(attr)] = attr
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
# 2. 实例属性(__init__ 赋值)
|
|
124
|
+
for attr in vars(instance):
|
|
125
|
+
if not attr.startswith('_'):
|
|
126
|
+
candidates[_normalize(attr)] = attr
|
|
127
|
+
# 3. 类属性兜底
|
|
128
|
+
for attr in dir(cls):
|
|
129
|
+
if attr.startswith('_'):
|
|
130
|
+
continue
|
|
131
|
+
candidates.setdefault(_normalize(attr), attr)
|
|
132
|
+
return candidates
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def bind(instance: Any, config: Dict[str, Any]) -> Any:
|
|
136
|
+
"""递归松散绑定 ``config`` 字典到 ``instance`` 的属性。
|
|
137
|
+
|
|
138
|
+
- 嵌套字典 + 属性类型为 ``@NestedConfigurationProperties`` 类 → 递归绑定。
|
|
139
|
+
- 标量按属性类型注解强转。
|
|
140
|
+
- 未匹配的键跳过(不报错,对齐 Spring 宽松绑定)。
|
|
141
|
+
"""
|
|
142
|
+
if not isinstance(config, dict):
|
|
143
|
+
return instance
|
|
144
|
+
try:
|
|
145
|
+
type_hints = get_type_hints(type(instance))
|
|
146
|
+
except Exception:
|
|
147
|
+
type_hints = {}
|
|
148
|
+
candidates = ConfigurationPropertiesBinder._attr_candidates(instance)
|
|
149
|
+
|
|
150
|
+
for key, value in config.items():
|
|
151
|
+
attr_name = ConfigurationPropertiesBinder._resolve_attr_name(
|
|
152
|
+
instance, key, candidates
|
|
153
|
+
)
|
|
154
|
+
if attr_name is None:
|
|
155
|
+
logger.debug("配置键 '%s' 未匹配到属性,跳过", key)
|
|
156
|
+
continue
|
|
157
|
+
attr_type = type_hints.get(attr_name)
|
|
158
|
+
# 嵌套配置:值是字典且属性类型为 @NestedConfigurationProperties 类
|
|
159
|
+
if isinstance(value, dict) and _is_nested_config_class(attr_type):
|
|
160
|
+
nested_instance = ConfigurationPropertiesBinder._build_nested(attr_type)
|
|
161
|
+
if nested_instance is not None:
|
|
162
|
+
ConfigurationPropertiesBinder.bind(nested_instance, value)
|
|
163
|
+
setattr(instance, attr_name, nested_instance)
|
|
164
|
+
continue
|
|
165
|
+
# 嵌套字典但属性类型非嵌套注解类:若属性类型是普通类,尝试递归绑定(容错)
|
|
166
|
+
if isinstance(value, dict) and isinstance(attr_type, type) and attr_type not in (dict,):
|
|
167
|
+
nested_instance = ConfigurationPropertiesBinder._build_nested(attr_type)
|
|
168
|
+
if nested_instance is not None and _is_nested_config_class(attr_type):
|
|
169
|
+
ConfigurationPropertiesBinder.bind(nested_instance, value)
|
|
170
|
+
setattr(instance, attr_name, nested_instance)
|
|
171
|
+
continue
|
|
172
|
+
# 标量/列表/字典:按类型强转后赋值
|
|
173
|
+
setattr(instance, attr_name, _coerce(value, attr_type))
|
|
174
|
+
return instance
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _build_nested(cls: type) -> Optional[Any]:
|
|
178
|
+
"""构造嵌套配置类实例;构造失败返回 None。"""
|
|
179
|
+
try:
|
|
180
|
+
return cls()
|
|
181
|
+
except Exception as exc:
|
|
182
|
+
logger.warning("构造嵌套配置类 %s 失败: %s", cls.__name__, exc)
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def validate_configuration_properties(instance: Any) -> None:
|
|
187
|
+
"""若 ``instance`` 类标注了 ``@Validated``,运行 BeanValidator 校验,违反则抛错。
|
|
188
|
+
|
|
189
|
+
复用 ``spring.validation.BeanValidator``,未启用 validation 模块时静默跳过。
|
|
190
|
+
"""
|
|
191
|
+
annotations = getattr(type(instance), '__spring_annotations__', [])
|
|
192
|
+
if not any(isinstance(a, Validated) for a in annotations):
|
|
193
|
+
return
|
|
194
|
+
try:
|
|
195
|
+
from spring.validation import BeanValidator
|
|
196
|
+
except ImportError: # pragma: no cover - validation 为内置模块
|
|
197
|
+
logger.debug("spring.validation 未安装,跳过配置属性校验")
|
|
198
|
+
return
|
|
199
|
+
BeanValidator.validate_or_raise(instance)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
__all__ = [
|
|
203
|
+
"NestedConfigurationProperties",
|
|
204
|
+
"ConfigurationPropertiesBinder",
|
|
205
|
+
"validate_configuration_properties",
|
|
206
|
+
]
|