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
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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",
|