aury-boot 0.0.5__py3-none-any.whl → 0.0.8__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 (49) hide show
  1. aury/boot/_version.py +2 -2
  2. aury/boot/application/__init__.py +15 -0
  3. aury/boot/application/adapter/__init__.py +112 -0
  4. aury/boot/application/adapter/base.py +511 -0
  5. aury/boot/application/adapter/config.py +242 -0
  6. aury/boot/application/adapter/decorators.py +259 -0
  7. aury/boot/application/adapter/exceptions.py +202 -0
  8. aury/boot/application/adapter/http.py +325 -0
  9. aury/boot/application/app/middlewares.py +7 -4
  10. aury/boot/application/config/multi_instance.py +42 -26
  11. aury/boot/application/config/settings.py +111 -191
  12. aury/boot/application/middleware/logging.py +14 -1
  13. aury/boot/commands/generate.py +22 -22
  14. aury/boot/commands/init.py +41 -9
  15. aury/boot/commands/templates/project/AGENTS.md.tpl +8 -4
  16. aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +17 -16
  17. aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +82 -43
  18. aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +14 -14
  19. aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +40 -28
  20. aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +9 -9
  21. aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +8 -8
  22. aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl +403 -0
  23. aury/boot/commands/templates/project/aury_docs/99-cli.md.tpl +19 -19
  24. aury/boot/commands/templates/project/config.py.tpl +10 -10
  25. aury/boot/commands/templates/project/env_templates/_header.tpl +10 -0
  26. aury/boot/commands/templates/project/env_templates/admin.tpl +49 -0
  27. aury/boot/commands/templates/project/env_templates/cache.tpl +14 -0
  28. aury/boot/commands/templates/project/env_templates/database.tpl +22 -0
  29. aury/boot/commands/templates/project/env_templates/log.tpl +18 -0
  30. aury/boot/commands/templates/project/env_templates/messaging.tpl +46 -0
  31. aury/boot/commands/templates/project/env_templates/rpc.tpl +28 -0
  32. aury/boot/commands/templates/project/env_templates/scheduler.tpl +18 -0
  33. aury/boot/commands/templates/project/env_templates/service.tpl +18 -0
  34. aury/boot/commands/templates/project/env_templates/storage.tpl +38 -0
  35. aury/boot/commands/templates/project/env_templates/third_party.tpl +43 -0
  36. aury/boot/common/logging/__init__.py +26 -674
  37. aury/boot/common/logging/context.py +132 -0
  38. aury/boot/common/logging/decorators.py +118 -0
  39. aury/boot/common/logging/format.py +315 -0
  40. aury/boot/common/logging/setup.py +214 -0
  41. aury/boot/infrastructure/database/config.py +6 -14
  42. aury/boot/infrastructure/tasks/config.py +5 -13
  43. aury/boot/infrastructure/tasks/manager.py +8 -4
  44. aury/boot/testing/base.py +2 -2
  45. {aury_boot-0.0.5.dist-info → aury_boot-0.0.8.dist-info}/METADATA +2 -1
  46. {aury_boot-0.0.5.dist-info → aury_boot-0.0.8.dist-info}/RECORD +48 -27
  47. aury/boot/commands/templates/project/env.example.tpl +0 -281
  48. {aury_boot-0.0.5.dist-info → aury_boot-0.0.8.dist-info}/WHEEL +0 -0
  49. {aury_boot-0.0.5.dist-info → aury_boot-0.0.8.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,132 @@
1
+ """日志上下文管理。
2
+
3
+ 提供链路追踪 ID、服务上下文、请求上下文的管理。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Callable
9
+ from contextvars import ContextVar
10
+ from enum import Enum
11
+ import uuid
12
+
13
+
14
+ class ServiceContext(str, Enum):
15
+ """日志用服务上下文常量(避免跨层依赖)。"""
16
+ API = "api"
17
+ SCHEDULER = "scheduler"
18
+ WORKER = "worker"
19
+
20
+
21
+ # 当前服务上下文(用于决定日志写入哪个文件)
22
+ _service_context: ContextVar[ServiceContext] = ContextVar("service_context", default=ServiceContext.API)
23
+
24
+ # 链路追踪 ID
25
+ _trace_id_var: ContextVar[str] = ContextVar("trace_id", default="")
26
+
27
+ # 请求上下文字段注册表(用户可注册自定义字段,如 user_id, tenant_id)
28
+ _request_context_getters: dict[str, Callable[[], str]] = {}
29
+
30
+
31
+ def get_service_context() -> ServiceContext:
32
+ """获取当前服务上下文。"""
33
+ return _service_context.get()
34
+
35
+
36
+ def _to_service_context(ctx: ServiceContext | str) -> ServiceContext:
37
+ """将输入标准化为 ServiceContext。"""
38
+ if isinstance(ctx, ServiceContext):
39
+ return ctx
40
+ val = str(ctx).strip().lower()
41
+ if val == "app": # 兼容旧值
42
+ val = ServiceContext.API.value
43
+ try:
44
+ return ServiceContext(val)
45
+ except ValueError:
46
+ return ServiceContext.API
47
+
48
+
49
+ def set_service_context(context: ServiceContext | str) -> None:
50
+ """设置当前服务上下文。
51
+
52
+ 在调度器任务执行前调用 set_service_context("scheduler"),
53
+ 后续该任务中的所有日志都会写入 scheduler_xxx.log。
54
+
55
+ Args:
56
+ context: 服务类型(api/scheduler/worker,或兼容 "app")
57
+ """
58
+ _service_context.set(_to_service_context(context))
59
+
60
+
61
+ def get_trace_id() -> str:
62
+ """获取当前链路追踪ID。
63
+
64
+ 如果尚未设置,则生成一个新的随机 ID。
65
+ """
66
+ trace_id = _trace_id_var.get()
67
+ if not trace_id:
68
+ trace_id = str(uuid.uuid4())
69
+ _trace_id_var.set(trace_id)
70
+ return trace_id
71
+
72
+
73
+ def set_trace_id(trace_id: str) -> None:
74
+ """设置链路追踪ID。"""
75
+ _trace_id_var.set(trace_id)
76
+
77
+
78
+ def register_request_context(name: str, getter: Callable[[], str]) -> None:
79
+ """注册请求上下文字段。
80
+
81
+ 注册后,该字段会在每个请求结束时记录一次(与 trace_id 关联)。
82
+ 适用于 user_id、tenant_id 等需要关联到请求但不需要每行日志都记录的信息。
83
+
84
+ Args:
85
+ name: 字段名(如 "user_id", "tenant_id")
86
+ getter: 获取当前值的函数(通常从 ContextVar 读取)
87
+
88
+ 使用示例:
89
+ from contextvars import ContextVar
90
+ from aury.boot.common.logging import register_request_context
91
+
92
+ # 定义上下文变量
93
+ _user_id: ContextVar[str] = ContextVar("user_id", default="")
94
+
95
+ def set_user_id(uid: str):
96
+ _user_id.set(uid)
97
+
98
+ # 启动时注册(一次)
99
+ register_request_context("user_id", _user_id.get)
100
+
101
+ # Auth 中间件中设置(每次请求)
102
+ set_user_id(str(user.id))
103
+ """
104
+ _request_context_getters[name] = getter
105
+
106
+
107
+ def get_request_contexts() -> dict[str, str]:
108
+ """获取所有已注册的请求上下文当前值。
109
+
110
+ Returns:
111
+ 字段名到值的字典(仅包含非空值)
112
+ """
113
+ result = {}
114
+ for name, getter in _request_context_getters.items():
115
+ try:
116
+ value = getter()
117
+ if value: # 只包含非空值
118
+ result[name] = value
119
+ except Exception:
120
+ pass # 忽略获取失败的字段
121
+ return result
122
+
123
+
124
+ __all__ = [
125
+ "ServiceContext",
126
+ "get_request_contexts",
127
+ "get_service_context",
128
+ "get_trace_id",
129
+ "register_request_context",
130
+ "set_service_context",
131
+ "set_trace_id",
132
+ ]
@@ -0,0 +1,118 @@
1
+ """日志装饰器。
2
+
3
+ 提供性能监控和异常日志装饰器。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Callable
9
+ from functools import wraps
10
+ import time
11
+ from typing import Any
12
+
13
+ from loguru import logger
14
+
15
+
16
+ def log_performance(threshold: float = 1.0) -> Callable:
17
+ """性能监控装饰器。
18
+
19
+ 记录函数执行时间,超过阈值时警告。
20
+
21
+ Args:
22
+ threshold: 警告阈值(秒)
23
+
24
+ 使用示例:
25
+ @log_performance(threshold=0.5)
26
+ async def slow_operation():
27
+ # 如果执行时间超过0.5秒,会记录警告
28
+ pass
29
+ """
30
+ def decorator[T](func: Callable[..., T]) -> Callable[..., T]:
31
+ @wraps(func)
32
+ async def wrapper(*args, **kwargs) -> T:
33
+ start_time = time.time()
34
+ try:
35
+ result = await func(*args, **kwargs)
36
+ duration = time.time() - start_time
37
+
38
+ if duration > threshold:
39
+ logger.warning(
40
+ f"性能警告: {func.__module__}.{func.__name__} 执行耗时 {duration:.3f}s "
41
+ f"(阈值: {threshold}s)"
42
+ )
43
+ else:
44
+ logger.debug(
45
+ f"性能: {func.__module__}.{func.__name__} 执行耗时 {duration:.3f}s"
46
+ )
47
+
48
+ return result
49
+ except Exception as exc:
50
+ duration = time.time() - start_time
51
+ logger.error(
52
+ f"执行失败: {func.__module__}.{func.__name__} | "
53
+ f"耗时: {duration:.3f}s | "
54
+ f"异常: {type(exc).__name__}: {exc}"
55
+ )
56
+ raise
57
+
58
+ return wrapper
59
+ return decorator
60
+
61
+
62
+ def log_exceptions[T](func: Callable[..., T]) -> Callable[..., T]:
63
+ """异常日志装饰器。
64
+
65
+ 自动记录函数抛出的异常。
66
+
67
+ 使用示例:
68
+ @log_exceptions
69
+ async def risky_operation():
70
+ # 如果抛出异常,会自动记录
71
+ pass
72
+ """
73
+ @wraps(func)
74
+ async def wrapper(*args, **kwargs) -> T:
75
+ try:
76
+ return await func(*args, **kwargs)
77
+ except Exception as exc:
78
+ logger.exception(
79
+ f"异常捕获: {func.__module__}.{func.__name__} | "
80
+ f"参数: args={args}, kwargs={kwargs} | "
81
+ f"异常: {type(exc).__name__}: {exc}"
82
+ )
83
+ raise
84
+
85
+ return wrapper
86
+
87
+
88
+ def get_class_logger(obj: object) -> Any:
89
+ """获取类专用的日志器(函数式工具函数)。
90
+
91
+ 根据对象的类和模块名创建绑定的日志器。
92
+
93
+ Args:
94
+ obj: 对象实例或类
95
+
96
+ Returns:
97
+ 绑定的日志器实例
98
+
99
+ 使用示例:
100
+ class MyService:
101
+ def do_something(self):
102
+ log = get_class_logger(self)
103
+ log.info("执行操作")
104
+ """
105
+ if isinstance(obj, type):
106
+ class_name = obj.__name__
107
+ module_name = obj.__module__
108
+ else:
109
+ class_name = obj.__class__.__name__
110
+ module_name = obj.__class__.__module__
111
+ return logger.bind(name=f"{module_name}.{class_name}")
112
+
113
+
114
+ __all__ = [
115
+ "get_class_logger",
116
+ "log_exceptions",
117
+ "log_performance",
118
+ ]
@@ -0,0 +1,315 @@
1
+ """日志格式化函数。
2
+
3
+ 提供 Java 风格堆栈格式化等功能。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ import traceback
10
+ from typing import Any
11
+
12
+ from loguru import logger
13
+
14
+ from aury.boot.common.logging.context import get_service_context, get_trace_id
15
+
16
+ # 要过滤的内部模块(不显示在堆栈中)
17
+ _INTERNAL_MODULES = {
18
+ "asyncio", "runners", "base_events", "events", "tasks",
19
+ "starlette", "uvicorn", "anyio", "httptools",
20
+ }
21
+
22
+
23
+ def _format_exception_compact(
24
+ exc_type: type[BaseException],
25
+ exc_value: BaseException,
26
+ exc_tb: Any,
27
+ ) -> str:
28
+ """格式化异常为 Java 风格堆栈 + 参数摘要。"""
29
+ import linecache
30
+
31
+ lines = [f"{exc_type.__name__}: {exc_value}"]
32
+
33
+ all_locals: dict[str, str] = {}
34
+ seen_values: set[str] = set() # 用于去重
35
+
36
+ tb = exc_tb
37
+ while tb:
38
+ frame = tb.tb_frame
39
+ filename = frame.f_code.co_filename
40
+ short_file = filename.split("/")[-1]
41
+ func_name = frame.f_code.co_name
42
+ lineno = tb.tb_lineno
43
+
44
+ # 简化模块路径
45
+ is_site_package = "site-packages/" in filename
46
+ if is_site_package:
47
+ module = filename.split("site-packages/")[-1].replace("/", ".").replace(".py", "")
48
+ # 过滤内部模块
49
+ module_root = module.split(".")[0]
50
+ if module_root in _INTERNAL_MODULES:
51
+ tb = tb.tb_next
52
+ continue
53
+ else:
54
+ module = short_file.replace(".py", "")
55
+
56
+ lines.append(f" at {module}.{func_name}({short_file}:{lineno})")
57
+
58
+ # 对于用户代码(非 site-packages),显示具体代码行
59
+ if not is_site_package:
60
+ source_line = linecache.getline(filename, lineno).strip()
61
+ if source_line:
62
+ lines.append(f" >> {source_line}")
63
+
64
+ # 收集局部变量(排除内部变量和 self)
65
+ for k, v in frame.f_locals.items():
66
+ if k.startswith("_") or k in ("self", "cls"):
67
+ continue
68
+ # 尝试获取变量的字符串表示
69
+ try:
70
+ # Pydantic 模型使用 model_dump
71
+ if hasattr(v, "model_dump"):
72
+ val_str = repr(v.model_dump())
73
+ elif isinstance(v, str | int | float | bool | dict | list | tuple):
74
+ val_str = repr(v)
75
+ else:
76
+ # 其他类型显示类名
77
+ val_str = f"<{type(v).__name__}>"
78
+ except Exception:
79
+ val_str = f"<{type(v).__name__}>"
80
+
81
+ # 截断过长的值(200 字符)
82
+ if len(val_str) > 200:
83
+ val_str = val_str[:200] + "..."
84
+
85
+ # 去重:相同值的变量只保留第一个
86
+ if val_str not in seen_values and k not in all_locals:
87
+ all_locals[k] = val_str
88
+ seen_values.add(val_str)
89
+
90
+ tb = tb.tb_next
91
+
92
+ # 输出参数
93
+ if all_locals:
94
+ lines.append(" Locals:")
95
+ for k, v in list(all_locals.items())[:10]: # 最多 10 个
96
+ lines.append(f" {k} = {v}")
97
+
98
+ return "\n".join(lines)
99
+
100
+
101
+ def create_console_sink(colorize: bool = True):
102
+ """创建控制台 sink(Java 风格异常格式)。"""
103
+ # ANSI 颜色码
104
+ if colorize:
105
+ GREEN = "\033[32m"
106
+ CYAN = "\033[36m"
107
+ YELLOW = "\033[33m"
108
+ RED = "\033[31m"
109
+ RESET = "\033[0m"
110
+ BOLD = "\033[1m"
111
+ else:
112
+ GREEN = CYAN = YELLOW = RED = RESET = BOLD = ""
113
+
114
+ LEVEL_COLORS = {
115
+ "DEBUG": CYAN,
116
+ "INFO": GREEN,
117
+ "WARNING": YELLOW,
118
+ "ERROR": RED,
119
+ "CRITICAL": f"{BOLD}{RED}",
120
+ }
121
+
122
+ def sink(message):
123
+ record = message.record
124
+ exc = record.get("exception")
125
+
126
+ time_str = record["time"].strftime("%Y-%m-%d %H:%M:%S")
127
+ level = record["level"].name
128
+ level_color = LEVEL_COLORS.get(level, "")
129
+ service = record["extra"].get("service", "api")
130
+ trace_id = record["extra"].get("trace_id", "")[:8]
131
+ name = record["name"]
132
+ func = record["function"]
133
+ line = record["line"]
134
+ msg = record["message"]
135
+
136
+ # 基础日志行
137
+ output = (
138
+ f"{GREEN}{time_str}{RESET} | "
139
+ f"{CYAN}[{service}]{RESET} | "
140
+ f"{level_color}{level: <8}{RESET} | "
141
+ f"{CYAN}{name}:{func}:{line}{RESET} | "
142
+ f"{trace_id} - "
143
+ f"{level_color}{msg}{RESET}\n"
144
+ )
145
+
146
+ # 异常堆栈
147
+ if exc and exc.type:
148
+ stack = _format_exception_compact(exc.type, exc.value, exc.traceback)
149
+ output += f"{RED}{stack}{RESET}\n"
150
+
151
+ sys.stderr.write(output)
152
+
153
+ return sink
154
+
155
+
156
+ def _escape_tags(s: str) -> str:
157
+ """转义 loguru 格式特殊字符,避免解析错误。"""
158
+ # 转义 { } 避免被当作 format 字段
159
+ s = s.replace("{", "{{").replace("}", "}}")
160
+ # 转义 < 避免被当作颜色标签
161
+ return s.replace("<", r"\<")
162
+
163
+
164
+ def format_message(record: dict) -> str:
165
+ """格式化日志消息(用于文件 sink)。"""
166
+ exc = record.get("exception")
167
+
168
+ time_str = record["time"].strftime("%Y-%m-%d %H:%M:%S")
169
+ level_name = record["level"].name
170
+ trace_id = record["extra"].get("trace_id", "")
171
+ name = record["name"]
172
+ func = _escape_tags(record["function"]) # 转义 <module> 等
173
+ line = record["line"]
174
+ msg = _escape_tags(record["message"]) # 转义消息中的 <
175
+
176
+ # 基础日志行
177
+ output = (
178
+ f"{time_str} | {level_name: <8} | "
179
+ f"{name}:{func}:{line} | "
180
+ f"{trace_id} - {msg}\n"
181
+ )
182
+
183
+ # 异常堆栈
184
+ if exc and exc.type:
185
+ stack = _format_exception_compact(exc.type, exc.value, exc.traceback)
186
+ output += f"{_escape_tags(stack)}\n"
187
+
188
+ return output
189
+
190
+
191
+ def format_exception_java_style(
192
+ exc_type: type[BaseException] | None = None,
193
+ exc_value: BaseException | None = None,
194
+ exc_tb: Any | None = None,
195
+ *,
196
+ max_frames: int = 20,
197
+ skip_site_packages: bool = False,
198
+ ) -> str:
199
+ """将异常堆栈格式化为 Java 风格。
200
+
201
+ 输出格式:
202
+ ValueError: error message
203
+ at module.function(file.py:42)
204
+ at module.Class.method(file.py:100)
205
+
206
+ Args:
207
+ exc_type: 异常类型(默认从 sys.exc_info() 获取)
208
+ exc_value: 异常值
209
+ exc_tb: 异常 traceback
210
+ max_frames: 最大堆栈帧数
211
+ skip_site_packages: 是否跳过第三方库的堆栈帧
212
+
213
+ Returns:
214
+ Java 风格的堆栈字符串
215
+
216
+ 使用示例:
217
+ try:
218
+ risky_operation()
219
+ except Exception:
220
+ logger.error(format_exception_java_style())
221
+ """
222
+ if exc_type is None:
223
+ exc_type, exc_value, exc_tb = sys.exc_info()
224
+
225
+ if exc_type is None or exc_value is None:
226
+ return "No exception"
227
+
228
+ lines = [f"{exc_type.__name__}: {exc_value}"]
229
+
230
+ frames = traceback.extract_tb(exc_tb)
231
+ if len(frames) > max_frames:
232
+ frames = frames[-max_frames:]
233
+ lines.append(f" ... ({len(traceback.extract_tb(exc_tb)) - max_frames} frames omitted)")
234
+
235
+ for frame in frames:
236
+ filename = frame.filename
237
+
238
+ # 跳过第三方库
239
+ if skip_site_packages and "site-packages" in filename:
240
+ continue
241
+
242
+ # 简化文件路径为模块风格
243
+ short_file = filename.split("/")[-1]
244
+
245
+ # 构建模块路径
246
+ if "site-packages/" in filename:
247
+ # 第三方库: 提取包名
248
+ module_part = filename.split("site-packages/")[-1]
249
+ module_path = module_part.replace("/", ".").replace(".py", "")
250
+ else:
251
+ # 项目代码: 使用文件名
252
+ module_path = short_file.replace(".py", "")
253
+
254
+ lines.append(f" at {module_path}.{frame.name}({short_file}:{frame.lineno})")
255
+
256
+ return "\n".join(lines)
257
+
258
+
259
+ def log_exception(
260
+ message: str = "异常",
261
+ *,
262
+ exc_info: tuple | None = None,
263
+ level: str = "ERROR",
264
+ context: dict[str, Any] | None = None,
265
+ max_frames: int = 20,
266
+ ) -> None:
267
+ """记录异常日志(Java 风格堆栈)。
268
+
269
+ 相比 logger.exception(),输出更简洁的堆栈信息。
270
+
271
+ Args:
272
+ message: 日志消息
273
+ exc_info: 异常信息元组 (type, value, tb),默认从 sys.exc_info() 获取
274
+ level: 日志级别
275
+ context: 额外上下文信息(如请求参数)
276
+ max_frames: 最大堆栈帧数
277
+
278
+ 使用示例:
279
+ try:
280
+ user_service.create(data)
281
+ except Exception:
282
+ log_exception(
283
+ "创建用户失败",
284
+ context={"user_data": data.model_dump()}
285
+ )
286
+ raise
287
+ """
288
+ if exc_info is None:
289
+ exc_info = sys.exc_info()
290
+
291
+ exc_type, exc_value, exc_tb = exc_info
292
+
293
+ # 构建日志消息
294
+ parts = [message]
295
+
296
+ # 添加上下文
297
+ if context:
298
+ ctx_str = " | ".join(f"{k}={v}" for k, v in context.items())
299
+ parts.append(f"上下文: {ctx_str}")
300
+
301
+ # 添加堆栈
302
+ stack = format_exception_java_style(exc_type, exc_value, exc_tb, max_frames=max_frames)
303
+ parts.append(f"\n{stack}")
304
+
305
+ full_message = " | ".join(parts[:2]) + parts[2] if len(parts) > 2 else " | ".join(parts)
306
+
307
+ logger.opt(depth=1).log(level, full_message)
308
+
309
+
310
+ __all__ = [
311
+ "create_console_sink",
312
+ "format_exception_java_style",
313
+ "format_message",
314
+ "log_exception",
315
+ ]