aury-boot 0.0.2__py3-none-any.whl → 0.0.4__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.
- aury/boot/__init__.py +66 -0
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +120 -0
- aury/boot/application/app/__init__.py +39 -0
- aury/boot/application/app/base.py +511 -0
- aury/boot/application/app/components.py +434 -0
- aury/boot/application/app/middlewares.py +101 -0
- aury/boot/application/config/__init__.py +44 -0
- aury/boot/application/config/settings.py +663 -0
- aury/boot/application/constants/__init__.py +19 -0
- aury/boot/application/constants/components.py +50 -0
- aury/boot/application/constants/scheduler.py +28 -0
- aury/boot/application/constants/service.py +29 -0
- aury/boot/application/errors/__init__.py +55 -0
- aury/boot/application/errors/chain.py +80 -0
- aury/boot/application/errors/codes.py +67 -0
- aury/boot/application/errors/exceptions.py +238 -0
- aury/boot/application/errors/handlers.py +320 -0
- aury/boot/application/errors/response.py +120 -0
- aury/boot/application/interfaces/__init__.py +76 -0
- aury/boot/application/interfaces/egress.py +224 -0
- aury/boot/application/interfaces/ingress.py +98 -0
- aury/boot/application/middleware/__init__.py +22 -0
- aury/boot/application/middleware/logging.py +451 -0
- aury/boot/application/migrations/__init__.py +13 -0
- aury/boot/application/migrations/manager.py +685 -0
- aury/boot/application/migrations/setup.py +237 -0
- aury/boot/application/rpc/__init__.py +63 -0
- aury/boot/application/rpc/base.py +108 -0
- aury/boot/application/rpc/client.py +294 -0
- aury/boot/application/rpc/discovery.py +218 -0
- aury/boot/application/scheduler/__init__.py +13 -0
- aury/boot/application/scheduler/runner.py +123 -0
- aury/boot/application/server/__init__.py +296 -0
- aury/boot/commands/__init__.py +30 -0
- aury/boot/commands/add.py +76 -0
- aury/boot/commands/app.py +105 -0
- aury/boot/commands/config.py +177 -0
- aury/boot/commands/docker.py +367 -0
- aury/boot/commands/docs.py +284 -0
- aury/boot/commands/generate.py +1277 -0
- aury/boot/commands/init.py +892 -0
- aury/boot/commands/migrate/__init__.py +37 -0
- aury/boot/commands/migrate/app.py +54 -0
- aury/boot/commands/migrate/commands.py +303 -0
- aury/boot/commands/scheduler.py +124 -0
- aury/boot/commands/server/__init__.py +21 -0
- aury/boot/commands/server/app.py +541 -0
- aury/boot/commands/templates/generate/api.py.tpl +105 -0
- aury/boot/commands/templates/generate/model.py.tpl +17 -0
- aury/boot/commands/templates/generate/repository.py.tpl +19 -0
- aury/boot/commands/templates/generate/schema.py.tpl +29 -0
- aury/boot/commands/templates/generate/service.py.tpl +48 -0
- aury/boot/commands/templates/project/CLI.md.tpl +92 -0
- aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
- aury/boot/commands/templates/project/README.md.tpl +111 -0
- aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
- aury/boot/commands/templates/project/config.py.tpl +30 -0
- aury/boot/commands/templates/project/conftest.py.tpl +26 -0
- aury/boot/commands/templates/project/env.example.tpl +213 -0
- aury/boot/commands/templates/project/gitignore.tpl +128 -0
- aury/boot/commands/templates/project/main.py.tpl +41 -0
- aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
- aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
- aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
- aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
- aury/boot/commands/worker.py +143 -0
- aury/boot/common/__init__.py +35 -0
- aury/boot/common/exceptions/__init__.py +114 -0
- aury/boot/common/i18n/__init__.py +16 -0
- aury/boot/common/i18n/translator.py +272 -0
- aury/boot/common/logging/__init__.py +716 -0
- aury/boot/contrib/__init__.py +10 -0
- aury/boot/contrib/admin_console/__init__.py +18 -0
- aury/boot/contrib/admin_console/auth.py +137 -0
- aury/boot/contrib/admin_console/discovery.py +69 -0
- aury/boot/contrib/admin_console/install.py +172 -0
- aury/boot/contrib/admin_console/utils.py +44 -0
- aury/boot/domain/__init__.py +79 -0
- aury/boot/domain/exceptions/__init__.py +132 -0
- aury/boot/domain/models/__init__.py +51 -0
- aury/boot/domain/models/base.py +69 -0
- aury/boot/domain/models/mixins.py +135 -0
- aury/boot/domain/models/models.py +96 -0
- aury/boot/domain/pagination/__init__.py +279 -0
- aury/boot/domain/repository/__init__.py +23 -0
- aury/boot/domain/repository/impl.py +423 -0
- aury/boot/domain/repository/interceptors.py +47 -0
- aury/boot/domain/repository/interface.py +106 -0
- aury/boot/domain/repository/query_builder.py +348 -0
- aury/boot/domain/service/__init__.py +11 -0
- aury/boot/domain/service/base.py +73 -0
- aury/boot/domain/transaction/__init__.py +404 -0
- aury/boot/infrastructure/__init__.py +104 -0
- aury/boot/infrastructure/cache/__init__.py +31 -0
- aury/boot/infrastructure/cache/backends.py +348 -0
- aury/boot/infrastructure/cache/base.py +68 -0
- aury/boot/infrastructure/cache/exceptions.py +37 -0
- aury/boot/infrastructure/cache/factory.py +94 -0
- aury/boot/infrastructure/cache/manager.py +274 -0
- aury/boot/infrastructure/database/__init__.py +39 -0
- aury/boot/infrastructure/database/config.py +71 -0
- aury/boot/infrastructure/database/exceptions.py +44 -0
- aury/boot/infrastructure/database/manager.py +317 -0
- aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
- aury/boot/infrastructure/database/strategies/__init__.py +198 -0
- aury/boot/infrastructure/di/__init__.py +15 -0
- aury/boot/infrastructure/di/container.py +393 -0
- aury/boot/infrastructure/events/__init__.py +33 -0
- aury/boot/infrastructure/events/bus.py +362 -0
- aury/boot/infrastructure/events/config.py +52 -0
- aury/boot/infrastructure/events/consumer.py +134 -0
- aury/boot/infrastructure/events/middleware.py +51 -0
- aury/boot/infrastructure/events/models.py +63 -0
- aury/boot/infrastructure/monitoring/__init__.py +529 -0
- aury/boot/infrastructure/scheduler/__init__.py +19 -0
- aury/boot/infrastructure/scheduler/exceptions.py +37 -0
- aury/boot/infrastructure/scheduler/manager.py +478 -0
- aury/boot/infrastructure/storage/__init__.py +38 -0
- aury/boot/infrastructure/storage/base.py +164 -0
- aury/boot/infrastructure/storage/exceptions.py +37 -0
- aury/boot/infrastructure/storage/factory.py +88 -0
- aury/boot/infrastructure/tasks/__init__.py +24 -0
- aury/boot/infrastructure/tasks/config.py +45 -0
- aury/boot/infrastructure/tasks/constants.py +37 -0
- aury/boot/infrastructure/tasks/exceptions.py +37 -0
- aury/boot/infrastructure/tasks/manager.py +490 -0
- aury/boot/testing/__init__.py +24 -0
- aury/boot/testing/base.py +122 -0
- aury/boot/testing/client.py +163 -0
- aury/boot/testing/factory.py +154 -0
- aury/boot/toolkit/__init__.py +21 -0
- aury/boot/toolkit/http/__init__.py +367 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/METADATA +3 -2
- aury_boot-0.0.4.dist-info/RECORD +137 -0
- aury_boot-0.0.2.dist-info/RECORD +0 -5
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.2.dist-info → aury_boot-0.0.4.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""HTTP 请求日志中间件。
|
|
2
|
+
|
|
3
|
+
提供 HTTP 相关的日志功能,包括:
|
|
4
|
+
- 请求日志中间件(支持链路追踪)
|
|
5
|
+
- 请求日志装饰器
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from functools import wraps
|
|
12
|
+
import time
|
|
13
|
+
import uuid
|
|
14
|
+
|
|
15
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
16
|
+
from starlette.requests import Request
|
|
17
|
+
from starlette.responses import Response
|
|
18
|
+
|
|
19
|
+
from aury.boot.common.logging import get_trace_id, logger, set_trace_id
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def log_request[T](func: Callable[..., T]) -> Callable[..., T]:
|
|
23
|
+
"""请求日志装饰器。
|
|
24
|
+
|
|
25
|
+
记录请求的详细信息。
|
|
26
|
+
|
|
27
|
+
使用示例:
|
|
28
|
+
@router.get("/users")
|
|
29
|
+
@log_request
|
|
30
|
+
async def get_users(request: Request):
|
|
31
|
+
return {"users": []}
|
|
32
|
+
"""
|
|
33
|
+
@wraps(func)
|
|
34
|
+
async def wrapper(*args, **kwargs) -> T:
|
|
35
|
+
request: Request | None = None
|
|
36
|
+
|
|
37
|
+
# 查找Request对象
|
|
38
|
+
for arg in args:
|
|
39
|
+
if isinstance(arg, Request):
|
|
40
|
+
request = arg
|
|
41
|
+
break
|
|
42
|
+
|
|
43
|
+
if not request:
|
|
44
|
+
request = kwargs.get("request")
|
|
45
|
+
|
|
46
|
+
# 记录请求信息
|
|
47
|
+
if request:
|
|
48
|
+
logger.info(
|
|
49
|
+
f"请求: {request.method} {request.url.path} | "
|
|
50
|
+
f"客户端: {request.client.host if request.client else 'unknown'} | "
|
|
51
|
+
f"查询参数: {dict(request.query_params)}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
# 执行函数
|
|
56
|
+
start_time = time.time()
|
|
57
|
+
response = await func(*args, **kwargs)
|
|
58
|
+
duration = time.time() - start_time
|
|
59
|
+
|
|
60
|
+
# 记录响应信息
|
|
61
|
+
if request:
|
|
62
|
+
logger.info(
|
|
63
|
+
f"响应: {request.method} {request.url.path} | "
|
|
64
|
+
f"耗时: {duration:.3f}s"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return response
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
# 记录错误
|
|
70
|
+
if request:
|
|
71
|
+
logger.error(
|
|
72
|
+
f"错误: {request.method} {request.url.path} | "
|
|
73
|
+
f"异常: {type(exc).__name__}: {exc}"
|
|
74
|
+
)
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
return wrapper
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# 请求/响应体最大记录长度
|
|
81
|
+
MAX_BODY_LOG_SIZE = 2000
|
|
82
|
+
# 不记录 body 的 Content-Type
|
|
83
|
+
SKIP_BODY_CONTENT_TYPES = (
|
|
84
|
+
"multipart/form-data",
|
|
85
|
+
"application/octet-stream",
|
|
86
|
+
"image/",
|
|
87
|
+
"audio/",
|
|
88
|
+
"video/",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _truncate_body(body: bytes | None, max_size: int = MAX_BODY_LOG_SIZE) -> str | None:
|
|
93
|
+
"""截取请求/响应体用于日志记录。"""
|
|
94
|
+
if not body:
|
|
95
|
+
return None
|
|
96
|
+
try:
|
|
97
|
+
text = body.decode("utf-8")
|
|
98
|
+
if len(text) > max_size:
|
|
99
|
+
return text[:max_size] + f"...(截取,总长{len(text)})"
|
|
100
|
+
return text
|
|
101
|
+
except UnicodeDecodeError:
|
|
102
|
+
return f"<二进制数据 {len(body)} bytes>"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _should_log_body(content_type: str | None) -> bool:
|
|
106
|
+
"""判断是否应该记录 body。"""
|
|
107
|
+
if not content_type:
|
|
108
|
+
return True
|
|
109
|
+
content_type = content_type.lower()
|
|
110
|
+
for skip_type in SKIP_BODY_CONTENT_TYPES:
|
|
111
|
+
if skip_type in content_type:
|
|
112
|
+
return False
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
117
|
+
"""请求日志中间件(支持链路追踪)。
|
|
118
|
+
|
|
119
|
+
自动记录所有HTTP请求的详细信息,包括:
|
|
120
|
+
- 请求方法、路径、查询参数、请求体
|
|
121
|
+
- 客户端IP、User-Agent
|
|
122
|
+
- 响应状态码、耗时、响应体
|
|
123
|
+
- 链路追踪 ID(X-Trace-ID / X-Request-ID)
|
|
124
|
+
|
|
125
|
+
注意:文件上传、二进制数据等不会记录 body 内容。
|
|
126
|
+
|
|
127
|
+
使用示例:
|
|
128
|
+
from aury.boot.application.middleware.logging import RequestLoggingMiddleware
|
|
129
|
+
|
|
130
|
+
app.add_middleware(RequestLoggingMiddleware)
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
134
|
+
"""处理请求并记录日志。"""
|
|
135
|
+
start_time = time.time()
|
|
136
|
+
|
|
137
|
+
# 从请求头获取或生成链路追踪 ID
|
|
138
|
+
trace_id = (
|
|
139
|
+
request.headers.get("x-trace-id") or
|
|
140
|
+
request.headers.get("x-request-id") or
|
|
141
|
+
str(uuid.uuid4())
|
|
142
|
+
)
|
|
143
|
+
set_trace_id(trace_id)
|
|
144
|
+
|
|
145
|
+
# 获取客户端信息
|
|
146
|
+
client_host = request.client.host if request.client else "unknown"
|
|
147
|
+
content_type = request.headers.get("content-type", "")
|
|
148
|
+
|
|
149
|
+
# 读取请求体
|
|
150
|
+
request_body: bytes | None = None
|
|
151
|
+
request_body_log: str | None = None
|
|
152
|
+
if request.method in ("POST", "PUT", "PATCH", "DELETE"):
|
|
153
|
+
try:
|
|
154
|
+
request_body = await request.body()
|
|
155
|
+
if _should_log_body(content_type):
|
|
156
|
+
request_body_log = _truncate_body(request_body)
|
|
157
|
+
else:
|
|
158
|
+
request_body_log = f"<{content_type}, {len(request_body)} bytes>"
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
# 构建请求日志
|
|
163
|
+
request_log = f"→ {request.method} {request.url.path}"
|
|
164
|
+
if request.query_params:
|
|
165
|
+
request_log += f" | 参数: {dict(request.query_params)}"
|
|
166
|
+
if request_body_log:
|
|
167
|
+
request_log += f" | Body: {request_body_log}"
|
|
168
|
+
request_log += f" | 客户端: {client_host} | Trace-ID: {trace_id}"
|
|
169
|
+
|
|
170
|
+
logger.info(request_log)
|
|
171
|
+
|
|
172
|
+
# 执行请求
|
|
173
|
+
try:
|
|
174
|
+
response = await call_next(request)
|
|
175
|
+
duration = time.time() - start_time
|
|
176
|
+
|
|
177
|
+
# 在响应头中添加追踪 ID
|
|
178
|
+
response.headers["x-trace-id"] = trace_id
|
|
179
|
+
|
|
180
|
+
# 记录响应信息
|
|
181
|
+
status_code = response.status_code
|
|
182
|
+
log_level = "error" if status_code >= 500 else "warning" if status_code >= 400 else "info"
|
|
183
|
+
|
|
184
|
+
response_log = (
|
|
185
|
+
f"← {request.method} {request.url.path} | "
|
|
186
|
+
f"状态: {status_code} | "
|
|
187
|
+
f"耗时: {duration:.3f}s | "
|
|
188
|
+
f"Trace-ID: {trace_id}"
|
|
189
|
+
)
|
|
190
|
+
logger.log(log_level.upper(), response_log)
|
|
191
|
+
|
|
192
|
+
# 写入 access 日志(简洁格式)
|
|
193
|
+
logger.bind(access=True).info(
|
|
194
|
+
f"{request.method} {request.url.path} {status_code} {duration:.3f}s"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# 慢请求警告
|
|
198
|
+
if duration > 1.0:
|
|
199
|
+
logger.warning(
|
|
200
|
+
f"慢请求: {request.method} {request.url.path} | "
|
|
201
|
+
f"耗时: {duration:.3f}s (超过1秒) | "
|
|
202
|
+
f"Trace-ID: {trace_id}"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
return response
|
|
206
|
+
|
|
207
|
+
except Exception as exc:
|
|
208
|
+
duration = time.time() - start_time
|
|
209
|
+
# diagnose=True 会自动记录局部变量(request_body_log, client_host, trace_id 等)
|
|
210
|
+
logger.exception(
|
|
211
|
+
f"请求处理失败: {request.method} {request.url.path} | "
|
|
212
|
+
f"耗时: {duration:.3f}s | Trace-ID: {trace_id}"
|
|
213
|
+
)
|
|
214
|
+
raise
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
class WebSocketLoggingMiddleware:
|
|
218
|
+
"""WebSocket 日志中间件。
|
|
219
|
+
|
|
220
|
+
记录 WebSocket 连接生命周期和消息收发(可选)。
|
|
221
|
+
|
|
222
|
+
使用示例:
|
|
223
|
+
from aury.boot.application.middleware.logging import WebSocketLoggingMiddleware
|
|
224
|
+
|
|
225
|
+
app.add_middleware(WebSocketLoggingMiddleware, log_messages=True)
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
def __init__(
|
|
229
|
+
self,
|
|
230
|
+
app,
|
|
231
|
+
*,
|
|
232
|
+
log_messages: bool = False,
|
|
233
|
+
max_message_length: int = 500,
|
|
234
|
+
) -> None:
|
|
235
|
+
"""初始化 WebSocket 日志中间件。
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
app: ASGI 应用
|
|
239
|
+
log_messages: 是否记录消息内容(默认 False,注意性能和敏感数据)
|
|
240
|
+
max_message_length: 消息内容最大记录长度
|
|
241
|
+
"""
|
|
242
|
+
self.app = app
|
|
243
|
+
self.log_messages = log_messages
|
|
244
|
+
self.max_message_length = max_message_length
|
|
245
|
+
|
|
246
|
+
async def __call__(self, scope, receive, send) -> None:
|
|
247
|
+
if scope["type"] != "websocket":
|
|
248
|
+
await self.app(scope, receive, send)
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
# 获取或生成 trace_id
|
|
252
|
+
headers = dict(scope.get("headers", []))
|
|
253
|
+
trace_id = (
|
|
254
|
+
headers.get(b"x-trace-id", b"").decode() or
|
|
255
|
+
headers.get(b"x-request-id", b"").decode() or
|
|
256
|
+
str(uuid.uuid4())
|
|
257
|
+
)
|
|
258
|
+
set_trace_id(trace_id)
|
|
259
|
+
|
|
260
|
+
path = scope.get("path", "/")
|
|
261
|
+
client = scope.get("client")
|
|
262
|
+
client_host = f"{client[0]}:{client[1]}" if client else "unknown"
|
|
263
|
+
|
|
264
|
+
start_time = time.time()
|
|
265
|
+
message_count = {"sent": 0, "received": 0}
|
|
266
|
+
|
|
267
|
+
async def logging_receive():
|
|
268
|
+
message = await receive()
|
|
269
|
+
msg_type = message.get("type", "")
|
|
270
|
+
|
|
271
|
+
if msg_type == "websocket.connect":
|
|
272
|
+
logger.info(
|
|
273
|
+
f"WS → 连接建立: {path} | "
|
|
274
|
+
f"客户端: {client_host} | Trace-ID: {trace_id}"
|
|
275
|
+
)
|
|
276
|
+
elif msg_type == "websocket.disconnect":
|
|
277
|
+
duration = time.time() - start_time
|
|
278
|
+
logger.info(
|
|
279
|
+
f"WS ← 连接关闭: {path} | "
|
|
280
|
+
f"时长: {duration:.1f}s | "
|
|
281
|
+
f"收/发: {message_count['received']}/{message_count['sent']} | "
|
|
282
|
+
f"Trace-ID: {trace_id}"
|
|
283
|
+
)
|
|
284
|
+
elif msg_type == "websocket.receive":
|
|
285
|
+
message_count["received"] += 1
|
|
286
|
+
if self.log_messages:
|
|
287
|
+
text = message.get("text") or message.get("bytes", b"").decode("utf-8", errors="replace")
|
|
288
|
+
if len(text) > self.max_message_length:
|
|
289
|
+
text = text[:self.max_message_length] + "..."
|
|
290
|
+
logger.debug(f"WS → 收到: {path} | {text}")
|
|
291
|
+
|
|
292
|
+
return message
|
|
293
|
+
|
|
294
|
+
async def logging_send(message):
|
|
295
|
+
msg_type = message.get("type", "")
|
|
296
|
+
|
|
297
|
+
if msg_type == "websocket.send":
|
|
298
|
+
message_count["sent"] += 1
|
|
299
|
+
if self.log_messages:
|
|
300
|
+
text = message.get("text") or message.get("bytes", b"").decode("utf-8", errors="replace")
|
|
301
|
+
if len(text) > self.max_message_length:
|
|
302
|
+
text = text[:self.max_message_length] + "..."
|
|
303
|
+
logger.debug(f"WS ← 发送: {path} | {text}")
|
|
304
|
+
elif msg_type == "websocket.close":
|
|
305
|
+
code = message.get("code", 1000)
|
|
306
|
+
reason = message.get("reason", "")
|
|
307
|
+
duration = time.time() - start_time
|
|
308
|
+
log_level = "warning" if code != 1000 else "info"
|
|
309
|
+
logger.log(
|
|
310
|
+
log_level.upper(),
|
|
311
|
+
f"WS ← 服务关闭: {path} | "
|
|
312
|
+
f"Code: {code} | 原因: {reason or '正常'} | "
|
|
313
|
+
f"时长: {duration:.1f}s | Trace-ID: {trace_id}"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
await send(message)
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
await self.app(scope, logging_receive, logging_send)
|
|
320
|
+
except Exception as exc:
|
|
321
|
+
duration = time.time() - start_time
|
|
322
|
+
logger.exception(
|
|
323
|
+
f"WS ✖ 异常: {path} | "
|
|
324
|
+
f"时长: {duration:.1f}s | "
|
|
325
|
+
f"收/发: {message_count['received']}/{message_count['sent']} | "
|
|
326
|
+
f"Trace-ID: {trace_id}"
|
|
327
|
+
)
|
|
328
|
+
raise
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class WebSocketLoggingMiddleware:
|
|
332
|
+
"""WebSocket 日志中间件。
|
|
333
|
+
|
|
334
|
+
记录 WebSocket 连接生命周期和消息收发(可选)。
|
|
335
|
+
|
|
336
|
+
使用示例:
|
|
337
|
+
from aury.boot.application.middleware.logging import WebSocketLoggingMiddleware
|
|
338
|
+
|
|
339
|
+
app.add_middleware(WebSocketLoggingMiddleware, log_messages=True)
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
def __init__(
|
|
343
|
+
self,
|
|
344
|
+
app,
|
|
345
|
+
*,
|
|
346
|
+
log_messages: bool = False,
|
|
347
|
+
max_message_length: int = 500,
|
|
348
|
+
) -> None:
|
|
349
|
+
"""初始化 WebSocket 日志中间件。
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
app: ASGI 应用
|
|
353
|
+
log_messages: 是否记录消息内容(默认 False,注意性能和敏感数据)
|
|
354
|
+
max_message_length: 消息内容最大记录长度
|
|
355
|
+
"""
|
|
356
|
+
self.app = app
|
|
357
|
+
self.log_messages = log_messages
|
|
358
|
+
self.max_message_length = max_message_length
|
|
359
|
+
|
|
360
|
+
async def __call__(self, scope, receive, send) -> None:
|
|
361
|
+
if scope["type"] != "websocket":
|
|
362
|
+
await self.app(scope, receive, send)
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
# 获取或生成 trace_id
|
|
366
|
+
headers = dict(scope.get("headers", []))
|
|
367
|
+
trace_id = (
|
|
368
|
+
headers.get(b"x-trace-id", b"").decode() or
|
|
369
|
+
headers.get(b"x-request-id", b"").decode() or
|
|
370
|
+
str(uuid.uuid4())
|
|
371
|
+
)
|
|
372
|
+
set_trace_id(trace_id)
|
|
373
|
+
|
|
374
|
+
path = scope.get("path", "/")
|
|
375
|
+
client = scope.get("client")
|
|
376
|
+
client_host = f"{client[0]}:{client[1]}" if client else "unknown"
|
|
377
|
+
|
|
378
|
+
start_time = time.time()
|
|
379
|
+
message_count = {"sent": 0, "received": 0}
|
|
380
|
+
|
|
381
|
+
async def logging_receive():
|
|
382
|
+
message = await receive()
|
|
383
|
+
msg_type = message.get("type", "")
|
|
384
|
+
|
|
385
|
+
if msg_type == "websocket.connect":
|
|
386
|
+
logger.info(
|
|
387
|
+
f"WS → 连接: {path} | "
|
|
388
|
+
f"客户端: {client_host} | Trace-ID: {trace_id}"
|
|
389
|
+
)
|
|
390
|
+
elif msg_type == "websocket.disconnect":
|
|
391
|
+
duration = time.time() - start_time
|
|
392
|
+
logger.info(
|
|
393
|
+
f"WS ← 断开: {path} | "
|
|
394
|
+
f"时长: {duration:.1f}s | "
|
|
395
|
+
f"收/发: {message_count['received']}/{message_count['sent']} | "
|
|
396
|
+
f"Trace-ID: {trace_id}"
|
|
397
|
+
)
|
|
398
|
+
elif msg_type == "websocket.receive":
|
|
399
|
+
message_count["received"] += 1
|
|
400
|
+
if self.log_messages:
|
|
401
|
+
text = message.get("text") or message.get("bytes", b"").decode("utf-8", errors="replace")
|
|
402
|
+
if len(text) > self.max_message_length:
|
|
403
|
+
text = text[:self.max_message_length] + "..."
|
|
404
|
+
logger.debug(f"WS → 收: {path} | {text}")
|
|
405
|
+
|
|
406
|
+
return message
|
|
407
|
+
|
|
408
|
+
async def logging_send(message):
|
|
409
|
+
msg_type = message.get("type", "")
|
|
410
|
+
|
|
411
|
+
if msg_type == "websocket.send":
|
|
412
|
+
message_count["sent"] += 1
|
|
413
|
+
if self.log_messages:
|
|
414
|
+
text = message.get("text") or message.get("bytes", b"").decode("utf-8", errors="replace")
|
|
415
|
+
if len(text) > self.max_message_length:
|
|
416
|
+
text = text[:self.max_message_length] + "..."
|
|
417
|
+
logger.debug(f"WS ← 发: {path} | {text}")
|
|
418
|
+
elif msg_type == "websocket.close":
|
|
419
|
+
code = message.get("code", 1000)
|
|
420
|
+
reason = message.get("reason", "")
|
|
421
|
+
duration = time.time() - start_time
|
|
422
|
+
log_level = "warning" if code != 1000 else "info"
|
|
423
|
+
logger.log(
|
|
424
|
+
log_level.upper(),
|
|
425
|
+
f"WS × 关闭: {path} | "
|
|
426
|
+
f"Code: {code}{' | 原因: ' + reason if reason else ''} | "
|
|
427
|
+
f"时长: {duration:.1f}s | Trace-ID: {trace_id}"
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
await send(message)
|
|
431
|
+
|
|
432
|
+
try:
|
|
433
|
+
await self.app(scope, logging_receive, logging_send)
|
|
434
|
+
except Exception as exc:
|
|
435
|
+
duration = time.time() - start_time
|
|
436
|
+
logger.exception(
|
|
437
|
+
f"WS ✖ 异常: {path} | "
|
|
438
|
+
f"时长: {duration:.1f}s | "
|
|
439
|
+
f"收/发: {message_count['received']}/{message_count['sent']} | "
|
|
440
|
+
f"Trace-ID: {trace_id}"
|
|
441
|
+
)
|
|
442
|
+
raise
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
__all__ = [
|
|
446
|
+
"RequestLoggingMiddleware",
|
|
447
|
+
"WebSocketLoggingMiddleware",
|
|
448
|
+
"log_request",
|
|
449
|
+
]
|
|
450
|
+
|
|
451
|
+
|