aury-boot 0.0.4__py3-none-any.whl → 0.0.7__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.
Files changed (122) hide show
  1. aury/boot/__init__.py +2 -2
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +60 -36
  4. aury/boot/application/adapter/__init__.py +112 -0
  5. aury/boot/application/adapter/base.py +511 -0
  6. aury/boot/application/adapter/config.py +242 -0
  7. aury/boot/application/adapter/decorators.py +259 -0
  8. aury/boot/application/adapter/exceptions.py +202 -0
  9. aury/boot/application/adapter/http.py +325 -0
  10. aury/boot/application/app/__init__.py +12 -8
  11. aury/boot/application/app/base.py +12 -0
  12. aury/boot/application/app/components.py +137 -44
  13. aury/boot/application/app/middlewares.py +9 -4
  14. aury/boot/application/app/startup.py +249 -0
  15. aury/boot/application/config/__init__.py +36 -1
  16. aury/boot/application/config/multi_instance.py +216 -0
  17. aury/boot/application/config/settings.py +398 -149
  18. aury/boot/application/constants/components.py +6 -0
  19. aury/boot/application/errors/handlers.py +17 -3
  20. aury/boot/application/middleware/logging.py +21 -120
  21. aury/boot/application/rpc/__init__.py +2 -2
  22. aury/boot/commands/__init__.py +30 -10
  23. aury/boot/commands/app.py +131 -1
  24. aury/boot/commands/docs.py +104 -17
  25. aury/boot/commands/generate.py +22 -22
  26. aury/boot/commands/init.py +68 -17
  27. aury/boot/commands/server/app.py +2 -3
  28. aury/boot/commands/templates/project/AGENTS.md.tpl +221 -0
  29. aury/boot/commands/templates/project/README.md.tpl +2 -2
  30. aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
  31. aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +184 -0
  32. aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
  33. aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
  34. aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
  35. aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
  36. aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
  37. aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
  38. aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
  39. aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
  40. aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
  41. aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +131 -0
  42. aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
  43. aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +104 -0
  44. aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
  45. aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
  46. aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl +403 -0
  47. aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +19 -19
  48. aury/boot/commands/templates/project/config.py.tpl +10 -10
  49. aury/boot/commands/templates/project/env_templates/_header.tpl +10 -0
  50. aury/boot/commands/templates/project/env_templates/admin.tpl +49 -0
  51. aury/boot/commands/templates/project/env_templates/cache.tpl +14 -0
  52. aury/boot/commands/templates/project/env_templates/database.tpl +22 -0
  53. aury/boot/commands/templates/project/env_templates/log.tpl +18 -0
  54. aury/boot/commands/templates/project/env_templates/messaging.tpl +46 -0
  55. aury/boot/commands/templates/project/env_templates/rpc.tpl +28 -0
  56. aury/boot/commands/templates/project/env_templates/scheduler.tpl +18 -0
  57. aury/boot/commands/templates/project/env_templates/service.tpl +18 -0
  58. aury/boot/commands/templates/project/env_templates/storage.tpl +38 -0
  59. aury/boot/commands/templates/project/env_templates/third_party.tpl +43 -0
  60. aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
  61. aury/boot/common/logging/__init__.py +26 -674
  62. aury/boot/common/logging/context.py +132 -0
  63. aury/boot/common/logging/decorators.py +118 -0
  64. aury/boot/common/logging/format.py +315 -0
  65. aury/boot/common/logging/setup.py +214 -0
  66. aury/boot/contrib/admin_console/auth.py +2 -3
  67. aury/boot/contrib/admin_console/install.py +1 -1
  68. aury/boot/domain/models/mixins.py +48 -1
  69. aury/boot/domain/pagination/__init__.py +94 -0
  70. aury/boot/domain/repository/impl.py +1 -1
  71. aury/boot/domain/repository/interface.py +1 -1
  72. aury/boot/domain/transaction/__init__.py +8 -9
  73. aury/boot/infrastructure/__init__.py +86 -29
  74. aury/boot/infrastructure/cache/backends.py +102 -18
  75. aury/boot/infrastructure/cache/base.py +12 -0
  76. aury/boot/infrastructure/cache/manager.py +153 -91
  77. aury/boot/infrastructure/channel/__init__.py +24 -0
  78. aury/boot/infrastructure/channel/backends/__init__.py +9 -0
  79. aury/boot/infrastructure/channel/backends/memory.py +83 -0
  80. aury/boot/infrastructure/channel/backends/redis.py +88 -0
  81. aury/boot/infrastructure/channel/base.py +92 -0
  82. aury/boot/infrastructure/channel/manager.py +203 -0
  83. aury/boot/infrastructure/clients/__init__.py +22 -0
  84. aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
  85. aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
  86. aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
  87. aury/boot/infrastructure/clients/redis/__init__.py +28 -0
  88. aury/boot/infrastructure/clients/redis/config.py +51 -0
  89. aury/boot/infrastructure/clients/redis/manager.py +264 -0
  90. aury/boot/infrastructure/database/config.py +7 -16
  91. aury/boot/infrastructure/database/manager.py +16 -38
  92. aury/boot/infrastructure/events/__init__.py +18 -21
  93. aury/boot/infrastructure/events/backends/__init__.py +11 -0
  94. aury/boot/infrastructure/events/backends/memory.py +86 -0
  95. aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
  96. aury/boot/infrastructure/events/backends/redis.py +162 -0
  97. aury/boot/infrastructure/events/base.py +127 -0
  98. aury/boot/infrastructure/events/manager.py +224 -0
  99. aury/boot/infrastructure/mq/__init__.py +24 -0
  100. aury/boot/infrastructure/mq/backends/__init__.py +9 -0
  101. aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
  102. aury/boot/infrastructure/mq/backends/redis.py +167 -0
  103. aury/boot/infrastructure/mq/base.py +143 -0
  104. aury/boot/infrastructure/mq/manager.py +239 -0
  105. aury/boot/infrastructure/scheduler/manager.py +7 -3
  106. aury/boot/infrastructure/storage/__init__.py +9 -9
  107. aury/boot/infrastructure/storage/base.py +17 -5
  108. aury/boot/infrastructure/storage/factory.py +0 -1
  109. aury/boot/infrastructure/tasks/__init__.py +2 -2
  110. aury/boot/infrastructure/tasks/config.py +5 -13
  111. aury/boot/infrastructure/tasks/manager.py +55 -33
  112. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/METADATA +20 -2
  113. aury_boot-0.0.7.dist-info/RECORD +197 -0
  114. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
  115. aury/boot/commands/templates/project/env.example.tpl +0 -213
  116. aury/boot/infrastructure/events/bus.py +0 -362
  117. aury/boot/infrastructure/events/config.py +0 -52
  118. aury/boot/infrastructure/events/consumer.py +0 -134
  119. aury/boot/infrastructure/events/models.py +0 -63
  120. aury_boot-0.0.4.dist-info/RECORD +0 -137
  121. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/WHEEL +0 -0
  122. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/entry_points.txt +0 -0
@@ -5,6 +5,7 @@
5
5
  - 性能监控装饰器
6
6
  - 异常日志装饰器
7
7
  - 链路追踪 ID 支持
8
+ - 请求上下文注入(user_id 等)
8
9
  - 自定义日志 sink 注册 API
9
10
 
10
11
  日志文件:
@@ -18,690 +19,40 @@ application.middleware.logging
18
19
 
19
20
  from __future__ import annotations
20
21
 
21
- from collections.abc import Callable
22
- from contextvars import ContextVar
23
- from enum import Enum
24
- from functools import wraps
25
- import os
26
- import sys
27
- import time
28
- import traceback
29
- from typing import Any
30
- import uuid
31
-
32
22
  from loguru import logger
33
23
 
34
24
  # 移除默认配置,由setup_logging统一配置
35
25
  logger.remove()
36
26
 
37
- # ============================================================
38
- # 服务上下文(ContextVar)
39
- # ============================================================
40
-
41
- class ServiceContext(str, Enum):
42
- """日志用服务上下文常量(避免跨层依赖)。"""
43
- API = "api"
44
- SCHEDULER = "scheduler"
45
- WORKER = "worker"
46
-
47
- # 当前服务上下文(用于决定日志写入哪个文件)
48
- _service_context: ContextVar[ServiceContext] = ContextVar("service_context", default=ServiceContext.API)
49
-
50
- # 链路追踪 ID
51
- _trace_id_var: ContextVar[str] = ContextVar("trace_id", default="")
52
-
53
-
54
- def get_service_context() -> ServiceContext:
55
- """获取当前服务上下文。"""
56
- return _service_context.get()
57
-
58
-
59
- def _to_service_context(ctx: ServiceContext | str) -> ServiceContext:
60
- """将输入标准化为 ServiceContext。"""
61
- if isinstance(ctx, ServiceContext):
62
- return ctx
63
- val = str(ctx).strip().lower()
64
- if val == "app": # 兼容旧值
65
- val = ServiceContext.API.value
66
- try:
67
- return ServiceContext(val)
68
- except ValueError:
69
- return ServiceContext.API
70
-
71
-
72
- def set_service_context(context: ServiceContext | str) -> None:
73
- """设置当前服务上下文。
74
-
75
- 在调度器任务执行前调用 set_service_context("scheduler"),
76
- 后续该任务中的所有日志都会写入 scheduler_xxx.log。
77
-
78
- Args:
79
- context: 服务类型(api/scheduler/worker,或兼容 "app")
80
- """
81
- _service_context.set(_to_service_context(context))
82
-
83
-
84
- def get_trace_id() -> str:
85
- """获取当前链路追踪ID。
86
-
87
- 如果尚未设置,则生成一个新的随机 ID。
88
- """
89
- trace_id = _trace_id_var.get()
90
- if not trace_id:
91
- trace_id = str(uuid.uuid4())
92
- _trace_id_var.set(trace_id)
93
- return trace_id
94
-
95
-
96
- def set_trace_id(trace_id: str) -> None:
97
- """设置链路追踪ID。"""
98
- _trace_id_var.set(trace_id)
99
-
100
-
101
- # ============================================================
102
- # 日志配置
103
- # ============================================================
104
-
105
- # 全局日志配置状态
106
- _log_config: dict[str, Any] = {
107
- "log_dir": "logs",
108
- "rotation": "00:00",
109
- "retention_days": 7,
110
- "file_format": "",
111
- "initialized": False,
112
- }
113
-
114
- # 要过滤的内部模块(不显示在堆栈中)
115
- _INTERNAL_MODULES = {
116
- "asyncio", "runners", "base_events", "events", "tasks",
117
- "starlette", "uvicorn", "anyio", "httptools",
118
- }
119
-
120
-
121
- def _format_exception_compact(
122
- exc_type: type[BaseException],
123
- exc_value: BaseException,
124
- exc_tb: Any,
125
- ) -> str:
126
- """格式化异常为 Java 风格堆栈 + 参数摘要。"""
127
- import linecache
128
-
129
- lines = [f"{exc_type.__name__}: {exc_value}"]
130
-
131
- all_locals: dict[str, str] = {}
132
- seen_values: set[str] = set() # 用于去重
133
-
134
- tb = exc_tb
135
- while tb:
136
- frame = tb.tb_frame
137
- filename = frame.f_code.co_filename
138
- short_file = filename.split("/")[-1]
139
- func_name = frame.f_code.co_name
140
- lineno = tb.tb_lineno
141
-
142
- # 简化模块路径
143
- is_site_package = "site-packages/" in filename
144
- if is_site_package:
145
- module = filename.split("site-packages/")[-1].replace("/", ".").replace(".py", "")
146
- # 过滤内部模块
147
- module_root = module.split(".")[0]
148
- if module_root in _INTERNAL_MODULES:
149
- tb = tb.tb_next
150
- continue
151
- else:
152
- module = short_file.replace(".py", "")
153
-
154
- lines.append(f" at {module}.{func_name}({short_file}:{lineno})")
155
-
156
- # 对于用户代码(非 site-packages),显示具体代码行
157
- if not is_site_package:
158
- source_line = linecache.getline(filename, lineno).strip()
159
- if source_line:
160
- lines.append(f" >> {source_line}")
161
-
162
- # 收集局部变量(排除内部变量和 self)
163
- for k, v in frame.f_locals.items():
164
- if k.startswith("_") or k in ("self", "cls"):
165
- continue
166
- # 尝试获取变量的字符串表示
167
- try:
168
- # Pydantic 模型使用 model_dump
169
- if hasattr(v, "model_dump"):
170
- val_str = repr(v.model_dump())
171
- elif isinstance(v, str | int | float | bool | dict | list | tuple):
172
- val_str = repr(v)
173
- else:
174
- # 其他类型显示类名
175
- val_str = f"<{type(v).__name__}>"
176
- except Exception:
177
- val_str = f"<{type(v).__name__}>"
178
-
179
- # 截断过长的值(200 字符)
180
- if len(val_str) > 200:
181
- val_str = val_str[:200] + "..."
182
-
183
- # 去重:相同值的变量只保留第一个
184
- if val_str not in seen_values and k not in all_locals:
185
- all_locals[k] = val_str
186
- seen_values.add(val_str)
187
-
188
- tb = tb.tb_next
189
-
190
- # 输出参数
191
- if all_locals:
192
- lines.append(" Locals:")
193
- for k, v in list(all_locals.items())[:10]: # 最多 10 个
194
- lines.append(f" {k} = {v}")
195
-
196
- return "\n".join(lines)
197
-
198
-
199
- def _create_console_sink(colorize: bool = True):
200
- """创建控制台 sink(Java 风格异常格式)。"""
201
- import sys
202
-
203
- # ANSI 颜色码
204
- if colorize:
205
- GREEN = "\033[32m"
206
- CYAN = "\033[36m"
207
- YELLOW = "\033[33m"
208
- RED = "\033[31m"
209
- RESET = "\033[0m"
210
- BOLD = "\033[1m"
211
- else:
212
- GREEN = CYAN = YELLOW = RED = RESET = BOLD = ""
213
-
214
- LEVEL_COLORS = {
215
- "DEBUG": CYAN,
216
- "INFO": GREEN,
217
- "WARNING": YELLOW,
218
- "ERROR": RED,
219
- "CRITICAL": f"{BOLD}{RED}",
220
- }
221
-
222
- def sink(message):
223
- record = message.record
224
- exc = record.get("exception")
225
-
226
- time_str = record["time"].strftime("%Y-%m-%d %H:%M:%S")
227
- level = record["level"].name
228
- level_color = LEVEL_COLORS.get(level, "")
229
- service = record["extra"].get("service", "api")
230
- trace_id = record["extra"].get("trace_id", "")[:8]
231
- name = record["name"]
232
- func = record["function"]
233
- line = record["line"]
234
- msg = record["message"]
235
-
236
- # 基础日志行
237
- output = (
238
- f"{GREEN}{time_str}{RESET} | "
239
- f"{CYAN}[{service}]{RESET} | "
240
- f"{level_color}{level: <8}{RESET} | "
241
- f"{CYAN}{name}:{func}:{line}{RESET} | "
242
- f"{trace_id} - "
243
- f"{level_color}{msg}{RESET}\n"
244
- )
245
-
246
- # 异常堆栈
247
- if exc and exc.type:
248
- stack = _format_exception_compact(exc.type, exc.value, exc.traceback)
249
- output += f"{RED}{stack}{RESET}\n"
250
-
251
- sys.stderr.write(output)
252
-
253
- return sink
254
-
255
-
256
- def _escape_tags(s: str) -> str:
257
- """转义 loguru 格式特殊字符,避免解析错误。"""
258
- # 转义 { } 避免被当作 format 字段
259
- s = s.replace("{", "{{").replace("}", "}}")
260
- # 转义 < 避免被当作颜色标签
261
- return s.replace("<", r"\<")
262
-
263
-
264
- def _format_message(record: dict) -> str:
265
- """格式化日志消息(用于文件 sink)。"""
266
- exc = record.get("exception")
267
-
268
- time_str = record["time"].strftime("%Y-%m-%d %H:%M:%S")
269
- level_name = record["level"].name
270
- trace_id = record["extra"].get("trace_id", "")
271
- name = record["name"]
272
- func = _escape_tags(record["function"]) # 转义 <module> 等
273
- line = record["line"]
274
- msg = _escape_tags(record["message"]) # 转义消息中的 <
275
-
276
- # 基础日志行
277
- output = (
278
- f"{time_str} | {level_name: <8} | "
279
- f"{name}:{func}:{line} | "
280
- f"{trace_id} - {msg}\n"
281
- )
282
-
283
- # 异常堆栈
284
- if exc and exc.type:
285
- stack = _format_exception_compact(exc.type, exc.value, exc.traceback)
286
- output += f"{_escape_tags(stack)}\n"
287
-
288
- return output
289
-
290
-
291
- def register_log_sink(
292
- name: str,
293
- *,
294
- filter_key: str | None = None,
295
- level: str = "INFO",
296
- sink_format: str | None = None,
297
- ) -> None:
298
- """注册自定义日志 sink。
299
-
300
- 使用 logger.bind() 标记的日志会写入对应文件。
301
-
302
- Args:
303
- name: 日志文件名前缀(如 "access" -> access_2024-01-01.log)
304
- filter_key: 过滤键名,日志需要 logger.bind(key=True) 才会写入
305
- level: 日志级别
306
- sink_format: 自定义格式(默认使用简化格式)
307
-
308
- 使用示例:
309
- # 注册 access 日志
310
- register_log_sink("access", filter_key="access")
311
-
312
- # 写入 access 日志
313
- logger.bind(access=True).info("GET /api/users 200 0.05s")
314
- """
315
- if not _log_config["initialized"]:
316
- raise RuntimeError("请先调用 setup_logging() 初始化日志系统")
317
-
318
- log_dir = _log_config["log_dir"]
319
- rotation = _log_config["rotation"]
320
- retention_days = _log_config["retention_days"]
321
-
322
- default_format = (
323
- "{time:YYYY-MM-DD HH:mm:ss} | "
324
- "{extra[trace_id]} | "
325
- "{message}"
326
- )
327
-
328
- # 创建 filter
329
- if filter_key:
330
- def sink_filter(record, key=filter_key):
331
- return record["extra"].get(key, False)
332
- else:
333
- sink_filter = None
334
-
335
- logger.add(
336
- os.path.join(log_dir, f"{name}_{{time:YYYY-MM-DD}}.log"),
337
- rotation=rotation,
338
- retention=f"{retention_days} days",
339
- level=level,
340
- format=sink_format or default_format,
341
- encoding="utf-8",
342
- enqueue=True,
343
- delay=True,
344
- filter=sink_filter,
345
- )
346
-
347
- logger.debug(f"注册日志 sink: {name} (filter_key={filter_key})")
348
-
349
-
350
- def _parse_size(size_str: str) -> int:
351
- """解析大小字符串为字节数。"""
352
- size_str = size_str.strip().upper()
353
- units = {"B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3}
354
- for unit, multiplier in units.items():
355
- if size_str.endswith(unit):
356
- return int(float(size_str[:-len(unit)].strip()) * multiplier)
357
- return int(size_str)
358
-
359
-
360
- def setup_logging(
361
- log_level: str = "INFO",
362
- log_dir: str | None = None,
363
- service_type: ServiceContext | str = ServiceContext.API,
364
- enable_file_rotation: bool = True,
365
- rotation_time: str = "00:00",
366
- retention_days: int = 7,
367
- rotation_size: str = "50 MB",
368
- enable_console: bool = True,
369
- ) -> None:
370
- """设置日志配置。
371
-
372
- 日志文件按服务类型分离:
373
- - {service_type}_info_{date}.log - INFO/WARNING/DEBUG 日志
374
- - {service_type}_error_{date}.log - ERROR/CRITICAL 日志
375
-
376
- 轮转策略:
377
- - 文件名包含日期,每天自动创建新文件
378
- - 单文件超过大小限制时,会轮转产生 .1, .2 等后缀
379
-
380
- 可通过 register_log_sink() 注册额外的日志文件(如 access.log)。
381
-
382
- Args:
383
- log_level: 日志级别(DEBUG/INFO/WARNING/ERROR/CRITICAL)
384
- log_dir: 日志目录(默认:./logs)
385
- service_type: 服务类型(app/scheduler/worker)
386
- enable_file_rotation: 是否启用日志轮转
387
- rotation_time: 每日轮转时间(默认:00:00)
388
- retention_days: 日志保留天数(默认:7 天)
389
- rotation_size: 单文件大小上限(默认:100 MB)
390
- enable_console: 是否输出到控制台
391
- """
392
- log_level = log_level.upper()
393
- log_dir = log_dir or "logs"
394
- os.makedirs(log_dir, exist_ok=True)
395
-
396
- # 滚动策略:基于大小轮转(文件名已包含日期,每天自动新文件)
397
- rotation = rotation_size if enable_file_rotation else None
398
-
399
- # 标准化服务类型
400
- service_type_enum = _to_service_context(service_type)
401
-
402
- # 清理旧的 sink,避免重复日志(idempotent)
403
- logger.remove()
404
-
405
- # 保存全局配置(供 register_log_sink 使用)
406
- _log_config.update({
407
- "log_dir": log_dir,
408
- "rotation": rotation,
409
- "retention_days": retention_days,
410
- "initialized": True,
411
- })
412
-
413
- # 设置默认服务上下文
414
- set_service_context(service_type_enum)
415
-
416
- # 配置 patcher,确保每条日志都有 service 和 trace_id
417
- logger.configure(patcher=lambda record: (
418
- record["extra"].update({
419
- "trace_id": get_trace_id(),
420
- # 记录字符串值,便于过滤器比较
421
- "service": get_service_context().value,
422
- })
423
- ))
424
-
425
- # 控制台输出(使用 Java 风格堆栈)
426
- if enable_console:
427
- logger.add(
428
- _create_console_sink(),
429
- format="{message}", # 简单格式,避免解析 <module> 等函数名
430
- level=log_level,
431
- colorize=False, # 颜色在 sink 内处理
432
- )
433
-
434
- # 为 app 和 scheduler 分别创建日志文件(通过 ContextVar 区分)
435
- # API 模式下会同时运行嵌入式 scheduler,需要两个文件
436
- contexts_to_create: list[str] = [service_type_enum.value]
437
- # API 模式下也需要 scheduler 日志文件
438
- if service_type_enum is ServiceContext.API:
439
- contexts_to_create.append(ServiceContext.SCHEDULER.value)
440
-
441
- for ctx in contexts_to_create:
442
- # INFO 级别文件(使用 Java 风格堆栈)
443
- info_file = os.path.join(
444
- log_dir,
445
- f"{ctx}_info_{{time:YYYY-MM-DD}}.log" if enable_file_rotation else f"{ctx}_info.log"
446
- )
447
- logger.add(
448
- info_file,
449
- format=lambda record: _format_message(record),
450
- rotation=rotation,
451
- retention=f"{retention_days} days",
452
- level=log_level, # >= INFO 都写入(包含 WARNING/ERROR/CRITICAL)
453
- encoding="utf-8",
454
- enqueue=True,
455
- filter=lambda record, c=ctx: (
456
- record["extra"].get("service") == c
457
- and not record["extra"].get("access", False)
458
- ),
459
- )
460
-
461
- # ERROR 级别文件(使用 Java 风格堆栈)
462
- error_file = os.path.join(
463
- log_dir,
464
- f"{ctx}_error_{{time:YYYY-MM-DD}}.log" if enable_file_rotation else f"{ctx}_error.log"
465
- )
466
- logger.add(
467
- error_file,
468
- format=lambda record: _format_message(record),
469
- rotation=rotation,
470
- retention=f"{retention_days} days",
471
- level="ERROR",
472
- encoding="utf-8",
473
- enqueue=True,
474
- filter=lambda record, c=ctx: record["extra"].get("service") == c,
475
- )
476
-
477
- logger.info(f"日志系统初始化完成 | 服务: {service_type} | 级别: {log_level} | 目录: {log_dir}")
478
-
479
-
480
- def log_performance(threshold: float = 1.0) -> Callable:
481
- """性能监控装饰器。
482
-
483
- 记录函数执行时间,超过阈值时警告。
484
-
485
- Args:
486
- threshold: 警告阈值(秒)
487
-
488
- 使用示例:
489
- @log_performance(threshold=0.5)
490
- async def slow_operation():
491
- # 如果执行时间超过0.5秒,会记录警告
492
- pass
493
- """
494
- def decorator[T](func: Callable[..., T]) -> Callable[..., T]:
495
- @wraps(func)
496
- async def wrapper(*args, **kwargs) -> T:
497
- start_time = time.time()
498
- try:
499
- result = await func(*args, **kwargs)
500
- duration = time.time() - start_time
501
-
502
- if duration > threshold:
503
- logger.warning(
504
- f"性能警告: {func.__module__}.{func.__name__} 执行耗时 {duration:.3f}s "
505
- f"(阈值: {threshold}s)"
506
- )
507
- else:
508
- logger.debug(
509
- f"性能: {func.__module__}.{func.__name__} 执行耗时 {duration:.3f}s"
510
- )
511
-
512
- return result
513
- except Exception as exc:
514
- duration = time.time() - start_time
515
- logger.error(
516
- f"执行失败: {func.__module__}.{func.__name__} | "
517
- f"耗时: {duration:.3f}s | "
518
- f"异常: {type(exc).__name__}: {exc}"
519
- )
520
- raise
521
-
522
- return wrapper
523
- return decorator
524
-
525
-
526
- def log_exceptions[T](func: Callable[..., T]) -> Callable[..., T]:
527
- """异常日志装饰器。
528
-
529
- 自动记录函数抛出的异常。
530
-
531
- 使用示例:
532
- @log_exceptions
533
- async def risky_operation():
534
- # 如果抛出异常,会自动记录
535
- pass
536
- """
537
- @wraps(func)
538
- async def wrapper(*args, **kwargs) -> T:
539
- try:
540
- return await func(*args, **kwargs)
541
- except Exception as exc:
542
- logger.exception(
543
- f"异常捕获: {func.__module__}.{func.__name__} | "
544
- f"参数: args={args}, kwargs={kwargs} | "
545
- f"异常: {type(exc).__name__}: {exc}"
546
- )
547
- raise
548
-
549
- return wrapper
550
-
551
-
552
- def get_class_logger(obj: object) -> Any:
553
- """获取类专用的日志器(函数式工具函数)。
554
-
555
- 根据对象的类和模块名创建绑定的日志器。
556
-
557
- Args:
558
- obj: 对象实例或类
559
-
560
- Returns:
561
- 绑定的日志器实例
562
-
563
- 使用示例:
564
- class MyService:
565
- def do_something(self):
566
- log = get_class_logger(self)
567
- log.info("执行操作")
568
- """
569
- if isinstance(obj, type):
570
- class_name = obj.__name__
571
- module_name = obj.__module__
572
- else:
573
- class_name = obj.__class__.__name__
574
- module_name = obj.__class__.__module__
575
- return logger.bind(name=f"{module_name}.{class_name}")
576
-
577
-
578
- # ============================================================
579
- # Java 风格堆栈格式化
580
- # ============================================================
581
-
582
- def format_exception_java_style(
583
- exc_type: type[BaseException] | None = None,
584
- exc_value: BaseException | None = None,
585
- exc_tb: Any | None = None,
586
- *,
587
- max_frames: int = 20,
588
- skip_site_packages: bool = False,
589
- ) -> str:
590
- """将异常堆栈格式化为 Java 风格。
591
-
592
- 输出格式:
593
- ValueError: error message
594
- at module.function(file.py:42)
595
- at module.Class.method(file.py:100)
596
-
597
- Args:
598
- exc_type: 异常类型(默认从 sys.exc_info() 获取)
599
- exc_value: 异常值
600
- exc_tb: 异常 traceback
601
- max_frames: 最大堆栈帧数
602
- skip_site_packages: 是否跳过第三方库的堆栈帧
603
-
604
- Returns:
605
- Java 风格的堆栈字符串
606
-
607
- 使用示例:
608
- try:
609
- risky_operation()
610
- except Exception:
611
- logger.error(format_exception_java_style())
612
- """
613
- if exc_type is None:
614
- exc_type, exc_value, exc_tb = sys.exc_info()
615
-
616
- if exc_type is None or exc_value is None:
617
- return "No exception"
618
-
619
- lines = [f"{exc_type.__name__}: {exc_value}"]
620
-
621
- frames = traceback.extract_tb(exc_tb)
622
- if len(frames) > max_frames:
623
- frames = frames[-max_frames:]
624
- lines.append(f" ... ({len(traceback.extract_tb(exc_tb)) - max_frames} frames omitted)")
625
-
626
- for frame in frames:
627
- filename = frame.filename
628
-
629
- # 跳过第三方库
630
- if skip_site_packages and "site-packages" in filename:
631
- continue
632
-
633
- # 简化文件路径为模块风格
634
- short_file = filename.split("/")[-1]
635
-
636
- # 构建模块路径
637
- if "site-packages/" in filename:
638
- # 第三方库: 提取包名
639
- module_part = filename.split("site-packages/")[-1]
640
- module_path = module_part.replace("/", ".").replace(".py", "")
641
- else:
642
- # 项目代码: 使用文件名
643
- module_path = short_file.replace(".py", "")
644
-
645
- lines.append(f" at {module_path}.{frame.name}({short_file}:{frame.lineno})")
646
-
647
- return "\n".join(lines)
648
-
649
-
650
- def log_exception(
651
- message: str = "异常",
652
- *,
653
- exc_info: tuple | None = None,
654
- level: str = "ERROR",
655
- context: dict[str, Any] | None = None,
656
- max_frames: int = 20,
657
- ) -> None:
658
- """记录异常日志(Java 风格堆栈)。
659
-
660
- 相比 logger.exception(),输出更简洁的堆栈信息。
661
-
662
- Args:
663
- message: 日志消息
664
- exc_info: 异常信息元组 (type, value, tb),默认从 sys.exc_info() 获取
665
- level: 日志级别
666
- context: 额外上下文信息(如请求参数)
667
- max_frames: 最大堆栈帧数
668
-
669
- 使用示例:
670
- try:
671
- user_service.create(data)
672
- except Exception:
673
- log_exception(
674
- "创建用户失败",
675
- context={"user_data": data.model_dump()}
676
- )
677
- raise
678
- """
679
- if exc_info is None:
680
- exc_info = sys.exc_info()
681
-
682
- exc_type, exc_value, exc_tb = exc_info
683
-
684
- # 构建日志消息
685
- parts = [message]
686
-
687
- # 添加上下文
688
- if context:
689
- ctx_str = " | ".join(f"{k}={v}" for k, v in context.items())
690
- parts.append(f"上下文: {ctx_str}")
691
-
692
- # 添加堆栈
693
- stack = format_exception_java_style(exc_type, exc_value, exc_tb, max_frames=max_frames)
694
- parts.append(f"\n{stack}")
695
-
696
- full_message = " | ".join(parts[:2]) + parts[2] if len(parts) > 2 else " | ".join(parts)
697
-
698
- logger.opt(depth=1).log(level, full_message)
699
-
27
+ # 从子模块导入
28
+ from aury.boot.common.logging.context import (
29
+ ServiceContext,
30
+ get_request_contexts,
31
+ get_service_context,
32
+ get_trace_id,
33
+ register_request_context,
34
+ set_service_context,
35
+ set_trace_id,
36
+ )
37
+ from aury.boot.common.logging.decorators import (
38
+ get_class_logger,
39
+ log_exceptions,
40
+ log_performance,
41
+ )
42
+ from aury.boot.common.logging.format import (
43
+ format_exception_java_style,
44
+ log_exception,
45
+ )
46
+ from aury.boot.common.logging.setup import (
47
+ register_log_sink,
48
+ setup_logging,
49
+ )
700
50
 
701
51
  __all__ = [
702
52
  "ServiceContext",
703
53
  "format_exception_java_style",
704
54
  "get_class_logger",
55
+ "get_request_contexts",
705
56
  "get_service_context",
706
57
  "get_trace_id",
707
58
  "log_exception",
@@ -709,6 +60,7 @@ __all__ = [
709
60
  "log_performance",
710
61
  "logger",
711
62
  "register_log_sink",
63
+ "register_request_context",
712
64
  "set_service_context",
713
65
  "set_trace_id",
714
66
  "setup_logging",