aury-boot 0.0.5__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.
- aury/boot/_version.py +2 -2
- aury/boot/application/__init__.py +15 -0
- aury/boot/application/adapter/__init__.py +112 -0
- aury/boot/application/adapter/base.py +511 -0
- aury/boot/application/adapter/config.py +242 -0
- aury/boot/application/adapter/decorators.py +259 -0
- aury/boot/application/adapter/exceptions.py +202 -0
- aury/boot/application/adapter/http.py +325 -0
- aury/boot/application/app/middlewares.py +7 -4
- aury/boot/application/config/multi_instance.py +42 -26
- aury/boot/application/config/settings.py +111 -191
- aury/boot/application/middleware/logging.py +14 -1
- aury/boot/commands/generate.py +22 -22
- aury/boot/commands/init.py +41 -9
- aury/boot/commands/templates/project/AGENTS.md.tpl +8 -4
- aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +17 -16
- aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +82 -43
- aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +14 -14
- aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +40 -28
- aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +9 -9
- aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +8 -8
- aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl +403 -0
- aury/boot/commands/templates/project/aury_docs/99-cli.md.tpl +19 -19
- aury/boot/commands/templates/project/config.py.tpl +10 -10
- aury/boot/commands/templates/project/env_templates/_header.tpl +10 -0
- aury/boot/commands/templates/project/env_templates/admin.tpl +49 -0
- aury/boot/commands/templates/project/env_templates/cache.tpl +14 -0
- aury/boot/commands/templates/project/env_templates/database.tpl +22 -0
- aury/boot/commands/templates/project/env_templates/log.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/messaging.tpl +46 -0
- aury/boot/commands/templates/project/env_templates/rpc.tpl +28 -0
- aury/boot/commands/templates/project/env_templates/scheduler.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/service.tpl +18 -0
- aury/boot/commands/templates/project/env_templates/storage.tpl +38 -0
- aury/boot/commands/templates/project/env_templates/third_party.tpl +43 -0
- aury/boot/common/logging/__init__.py +26 -674
- aury/boot/common/logging/context.py +132 -0
- aury/boot/common/logging/decorators.py +118 -0
- aury/boot/common/logging/format.py +315 -0
- aury/boot/common/logging/setup.py +214 -0
- aury/boot/infrastructure/database/config.py +6 -14
- aury/boot/infrastructure/tasks/config.py +5 -13
- aury/boot/infrastructure/tasks/manager.py +8 -4
- aury/boot/testing/base.py +2 -2
- {aury_boot-0.0.5.dist-info → aury_boot-0.0.7.dist-info}/METADATA +2 -1
- {aury_boot-0.0.5.dist-info → aury_boot-0.0.7.dist-info}/RECORD +48 -27
- aury/boot/commands/templates/project/env.example.tpl +0 -281
- {aury_boot-0.0.5.dist-info → aury_boot-0.0.7.dist-info}/WHEEL +0 -0
- {aury_boot-0.0.5.dist-info → aury_boot-0.0.7.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
|
+
]
|