xmi-logger 0.0.1__tar.gz
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.
- xmi_logger-0.0.1/PKG-INFO +221 -0
- xmi_logger-0.0.1/README.md +195 -0
- xmi_logger-0.0.1/setup.cfg +4 -0
- xmi_logger-0.0.1/setup.py +41 -0
- xmi_logger-0.0.1/xmi_logger/__init__.py +16 -0
- xmi_logger-0.0.1/xmi_logger/xmi_logger.py +612 -0
- xmi_logger-0.0.1/xmi_logger.egg-info/PKG-INFO +221 -0
- xmi_logger-0.0.1/xmi_logger.egg-info/SOURCES.txt +9 -0
- xmi_logger-0.0.1/xmi_logger.egg-info/dependency_links.txt +1 -0
- xmi_logger-0.0.1/xmi_logger.egg-info/requires.txt +2 -0
- xmi_logger-0.0.1/xmi_logger.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xmi_logger
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: An enhanced logger based on Loguru
|
|
5
|
+
Home-page: https://github.com/wang-zhibo/xmi_logger
|
|
6
|
+
Author: gm.zhibo.wang
|
|
7
|
+
Author-email: gm.zhibo.wang@gmail.com
|
|
8
|
+
Project-URL: Bug Reports, https://github.com/wang-zhibo/xmi_logger/issues
|
|
9
|
+
Project-URL: Source, https://github.com/wang-zhibo/xmi_logger
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.6
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: loguru==0.7.3
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: project-url
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
26
|
+
|
|
27
|
+
## Enhanced Logger
|
|
28
|
+
|
|
29
|
+
这是一个基于 [Loguru](https://github.com/Delgan/loguru) 的扩展日志记录器,提供了一系列增强特性,包括:
|
|
30
|
+
|
|
31
|
+
- 自定义日志格式
|
|
32
|
+
- 日志轮转和保留策略
|
|
33
|
+
- 上下文信息管理(如 `request_id`)
|
|
34
|
+
- 远程日志收集(使用线程池防止阻塞)
|
|
35
|
+
- 装饰器用于记录函数调用和执行时间,支持同步/异步函数
|
|
36
|
+
- 自定义日志级别(避免与 Loguru 预定义的冲突)
|
|
37
|
+
- 统一异常处理
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### 功能概述
|
|
42
|
+
|
|
43
|
+
1. **自定义日志格式**
|
|
44
|
+
可自由配置字段,如时间、进程/线程 ID、日志级别、请求 ID、所在文件、函数、行号等。
|
|
45
|
+
|
|
46
|
+
2. **日志轮转与保留**
|
|
47
|
+
- 支持按照文件大小、时间或文件数量进行滚动,并可自动删除过期日志。
|
|
48
|
+
- 默认使用大小轮转:单个文件超过 `max_size` MB 时自动滚动。
|
|
49
|
+
- 默认保留策略 `retention='9 days'`,可根据需要自定义。
|
|
50
|
+
|
|
51
|
+
3. **上下文管理**
|
|
52
|
+
- 使用 `ContextVar` 储存 `request_id`,可在异步环境中区分不同请求来源的日志。
|
|
53
|
+
|
|
54
|
+
4. **远程日志收集**
|
|
55
|
+
- 通过自定义处理器,使用线程池的方式将日志上报到远程服务,避免主线程阻塞。
|
|
56
|
+
- 默认仅收集 `ERROR` 及以上等级的日志。可在 `_configure_remote_logging()` 方法中自行配置。
|
|
57
|
+
|
|
58
|
+
5. **装饰器**
|
|
59
|
+
- `log_decorator` 可装饰任意同步或异步函数,自动记录:
|
|
60
|
+
- 函数调用开始
|
|
61
|
+
- 参数、返回值
|
|
62
|
+
- 函数执行耗时
|
|
63
|
+
- 异常信息(可选择是否抛出异常)
|
|
64
|
+
|
|
65
|
+
6. **自定义日志级别**
|
|
66
|
+
- 通过 `add_custom_level` 方法添加额外的日志级别(如 `AUDIT`, `SECURITY` 等),避免与已有日志级别冲突。
|
|
67
|
+
|
|
68
|
+
7. **统一异常处理**
|
|
69
|
+
- 注册全局异常处理 (`sys.excepthook`),捕获任何未处理的异常并记录。
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### 目录结构
|
|
74
|
+
|
|
75
|
+
. ├── logs/ # 日志存放目录(默认) ├── my_logger.py # MyLogger 类源码 ├── README.md # 使用说明 └── requirements.txt # Python依赖(如有)
|
|
76
|
+
|
|
77
|
+
yaml
|
|
78
|
+
复制代码
|
|
79
|
+
|
|
80
|
+
> 其中 `logs/` 是默认日志目录,可以通过初始化时的 `log_dir` 参数修改。
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
### 安装
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install xdeek-logger
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
### 使用示例
|
|
93
|
+
example/main.py
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
### 导入并使用
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
from xdeek_logger import MyLogger
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
初始化日志记录器
|
|
103
|
+
可自定义:
|
|
104
|
+
- 主日志文件名 (e.g., "app_log")
|
|
105
|
+
- 日志目录 log_dir (默认 "logs")
|
|
106
|
+
- 单个日志文件体积最大值 max_size (MB)
|
|
107
|
+
- 日志保留策略 retention (e.g., "7 days")
|
|
108
|
+
- 远程日志收集地址 remote_log_url (默认 None)
|
|
109
|
+
- 线程池最大工作线程数 max_workers (默认 5)
|
|
110
|
+
"""
|
|
111
|
+
logger = MyLogger(
|
|
112
|
+
file_name="app_log",
|
|
113
|
+
log_dir="logs",
|
|
114
|
+
max_size=50,
|
|
115
|
+
retention="7 days",
|
|
116
|
+
remote_log_url=None,
|
|
117
|
+
max_workers=5,
|
|
118
|
+
language='zh' # 新增:语言选项,默认为中文
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 调用日志方法
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
"""直接使用 Loguru 的常见日志方法"""
|
|
127
|
+
logger.info("This is an info message.")
|
|
128
|
+
logger.debug("Debug details here.")
|
|
129
|
+
logger.warning("Be cautious!")
|
|
130
|
+
logger.error("An error occurred.")
|
|
131
|
+
logger.critical("Critical issue!")
|
|
132
|
+
logger.trace("This is a trace message - only if Loguru TRACE level is enabled.")
|
|
133
|
+
|
|
134
|
+
logger.log("CUSTOM_LEVEL", "A special custom message.")
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 使用装饰器记录函数调用
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
@logger.log_decorator("A division error occurred.")
|
|
142
|
+
def divide(a, b):
|
|
143
|
+
return a / b
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
result = divide(10, 0)
|
|
147
|
+
"""# 将触发 ZeroDivisionError"""
|
|
148
|
+
except ZeroDivisionError:
|
|
149
|
+
logger.exception("Handled ZeroDivisionError.")
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
- 此装饰器会自动在函数开始和结束时分别记录函数名、参数、返回值以及耗时。
|
|
153
|
+
- 如果出现异常,则记录 traceback 并打印自定义提示信息。
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
### 记录异步函数调用
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
import asyncio
|
|
160
|
+
|
|
161
|
+
@logger.log_decorator("Async function error.")
|
|
162
|
+
async def async_task():
|
|
163
|
+
await asyncio.sleep(1)
|
|
164
|
+
return "Async result"
|
|
165
|
+
|
|
166
|
+
async def main():
|
|
167
|
+
result = await async_task()
|
|
168
|
+
logger.info(f"Result: {result}")
|
|
169
|
+
|
|
170
|
+
asyncio.run(main())
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 设置和重置 request_id
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
"""# 设置某个上下文的 request_id"""
|
|
178
|
+
token = logger.request_id_var.set("12345")
|
|
179
|
+
|
|
180
|
+
"""# ...执行与你的请求相关的操作,所有日志都带上 request_id=12345"""
|
|
181
|
+
|
|
182
|
+
"""# 结束后重置"""
|
|
183
|
+
logger.request_id_var.reset(token)
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
### 远程日志收集
|
|
189
|
+
|
|
190
|
+
- 在初始化 MyLogger 时,指定 remote_log_url 即可启用远程日志上报功能:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
logger = MyLogger(
|
|
194
|
+
file_name="app_log",
|
|
195
|
+
remote_log_url="https://your-logging-endpoint.com/logs"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
### 常见问题
|
|
205
|
+
|
|
206
|
+
#### 1. 如何关闭日志多文件策略?
|
|
207
|
+
- 如果仅需要一个主日志文件,可去掉或注释掉 `_get_level_log_path()` 相关的 `logger.add(...)` 调用。
|
|
208
|
+
- 如果希望“只按级别分文件、不需要主日志文件”,可以删除对应的添加主日志文件的 `add` 调用。
|
|
209
|
+
|
|
210
|
+
#### 2. 如何自定义轮转策略(按天、按小时等)?
|
|
211
|
+
- 将 `rotation=f"{self.max_size} MB"` 改为 `rotation="1 day"`、`rotation="00:00"` 等,即可使用 Loguru 的时间轮转功能。
|
|
212
|
+
|
|
213
|
+
#### 3. 如何自定义日志输出格式?
|
|
214
|
+
- 修改 `custom_format` 变量,或在 `logger.add()` 中使用你喜欢的格式,如 **JSON** 格式、单行简洁格式等。
|
|
215
|
+
|
|
216
|
+
#### 4. 如何在函数装饰器中抛出异常?
|
|
217
|
+
- 在装饰器里捕获异常后,如果希望装饰器内不“吞掉”异常,可在 `except` 块里添加 `raise`,这样异常会继续向上传递。
|
|
218
|
+
|
|
219
|
+
#### 5. 如何在远程收集中添加鉴权信息?
|
|
220
|
+
- 在 `_send_to_remote` 方法里,可在 `headers` 中添加 `Authorization` token 或其他自定义请求头。
|
|
221
|
+
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
## Enhanced Logger
|
|
2
|
+
|
|
3
|
+
这是一个基于 [Loguru](https://github.com/Delgan/loguru) 的扩展日志记录器,提供了一系列增强特性,包括:
|
|
4
|
+
|
|
5
|
+
- 自定义日志格式
|
|
6
|
+
- 日志轮转和保留策略
|
|
7
|
+
- 上下文信息管理(如 `request_id`)
|
|
8
|
+
- 远程日志收集(使用线程池防止阻塞)
|
|
9
|
+
- 装饰器用于记录函数调用和执行时间,支持同步/异步函数
|
|
10
|
+
- 自定义日志级别(避免与 Loguru 预定义的冲突)
|
|
11
|
+
- 统一异常处理
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
### 功能概述
|
|
16
|
+
|
|
17
|
+
1. **自定义日志格式**
|
|
18
|
+
可自由配置字段,如时间、进程/线程 ID、日志级别、请求 ID、所在文件、函数、行号等。
|
|
19
|
+
|
|
20
|
+
2. **日志轮转与保留**
|
|
21
|
+
- 支持按照文件大小、时间或文件数量进行滚动,并可自动删除过期日志。
|
|
22
|
+
- 默认使用大小轮转:单个文件超过 `max_size` MB 时自动滚动。
|
|
23
|
+
- 默认保留策略 `retention='9 days'`,可根据需要自定义。
|
|
24
|
+
|
|
25
|
+
3. **上下文管理**
|
|
26
|
+
- 使用 `ContextVar` 储存 `request_id`,可在异步环境中区分不同请求来源的日志。
|
|
27
|
+
|
|
28
|
+
4. **远程日志收集**
|
|
29
|
+
- 通过自定义处理器,使用线程池的方式将日志上报到远程服务,避免主线程阻塞。
|
|
30
|
+
- 默认仅收集 `ERROR` 及以上等级的日志。可在 `_configure_remote_logging()` 方法中自行配置。
|
|
31
|
+
|
|
32
|
+
5. **装饰器**
|
|
33
|
+
- `log_decorator` 可装饰任意同步或异步函数,自动记录:
|
|
34
|
+
- 函数调用开始
|
|
35
|
+
- 参数、返回值
|
|
36
|
+
- 函数执行耗时
|
|
37
|
+
- 异常信息(可选择是否抛出异常)
|
|
38
|
+
|
|
39
|
+
6. **自定义日志级别**
|
|
40
|
+
- 通过 `add_custom_level` 方法添加额外的日志级别(如 `AUDIT`, `SECURITY` 等),避免与已有日志级别冲突。
|
|
41
|
+
|
|
42
|
+
7. **统一异常处理**
|
|
43
|
+
- 注册全局异常处理 (`sys.excepthook`),捕获任何未处理的异常并记录。
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### 目录结构
|
|
48
|
+
|
|
49
|
+
. ├── logs/ # 日志存放目录(默认) ├── my_logger.py # MyLogger 类源码 ├── README.md # 使用说明 └── requirements.txt # Python依赖(如有)
|
|
50
|
+
|
|
51
|
+
yaml
|
|
52
|
+
复制代码
|
|
53
|
+
|
|
54
|
+
> 其中 `logs/` 是默认日志目录,可以通过初始化时的 `log_dir` 参数修改。
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
### 安装
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install xdeek-logger
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
### 使用示例
|
|
67
|
+
example/main.py
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
### 导入并使用
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
from xdeek_logger import MyLogger
|
|
74
|
+
|
|
75
|
+
"""
|
|
76
|
+
初始化日志记录器
|
|
77
|
+
可自定义:
|
|
78
|
+
- 主日志文件名 (e.g., "app_log")
|
|
79
|
+
- 日志目录 log_dir (默认 "logs")
|
|
80
|
+
- 单个日志文件体积最大值 max_size (MB)
|
|
81
|
+
- 日志保留策略 retention (e.g., "7 days")
|
|
82
|
+
- 远程日志收集地址 remote_log_url (默认 None)
|
|
83
|
+
- 线程池最大工作线程数 max_workers (默认 5)
|
|
84
|
+
"""
|
|
85
|
+
logger = MyLogger(
|
|
86
|
+
file_name="app_log",
|
|
87
|
+
log_dir="logs",
|
|
88
|
+
max_size=50,
|
|
89
|
+
retention="7 days",
|
|
90
|
+
remote_log_url=None,
|
|
91
|
+
max_workers=5,
|
|
92
|
+
language='zh' # 新增:语言选项,默认为中文
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 调用日志方法
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
"""直接使用 Loguru 的常见日志方法"""
|
|
101
|
+
logger.info("This is an info message.")
|
|
102
|
+
logger.debug("Debug details here.")
|
|
103
|
+
logger.warning("Be cautious!")
|
|
104
|
+
logger.error("An error occurred.")
|
|
105
|
+
logger.critical("Critical issue!")
|
|
106
|
+
logger.trace("This is a trace message - only if Loguru TRACE level is enabled.")
|
|
107
|
+
|
|
108
|
+
logger.log("CUSTOM_LEVEL", "A special custom message.")
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 使用装饰器记录函数调用
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
@logger.log_decorator("A division error occurred.")
|
|
116
|
+
def divide(a, b):
|
|
117
|
+
return a / b
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
result = divide(10, 0)
|
|
121
|
+
"""# 将触发 ZeroDivisionError"""
|
|
122
|
+
except ZeroDivisionError:
|
|
123
|
+
logger.exception("Handled ZeroDivisionError.")
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
- 此装饰器会自动在函数开始和结束时分别记录函数名、参数、返回值以及耗时。
|
|
127
|
+
- 如果出现异常,则记录 traceback 并打印自定义提示信息。
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
### 记录异步函数调用
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
import asyncio
|
|
134
|
+
|
|
135
|
+
@logger.log_decorator("Async function error.")
|
|
136
|
+
async def async_task():
|
|
137
|
+
await asyncio.sleep(1)
|
|
138
|
+
return "Async result"
|
|
139
|
+
|
|
140
|
+
async def main():
|
|
141
|
+
result = await async_task()
|
|
142
|
+
logger.info(f"Result: {result}")
|
|
143
|
+
|
|
144
|
+
asyncio.run(main())
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### 设置和重置 request_id
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
"""# 设置某个上下文的 request_id"""
|
|
152
|
+
token = logger.request_id_var.set("12345")
|
|
153
|
+
|
|
154
|
+
"""# ...执行与你的请求相关的操作,所有日志都带上 request_id=12345"""
|
|
155
|
+
|
|
156
|
+
"""# 结束后重置"""
|
|
157
|
+
logger.request_id_var.reset(token)
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
### 远程日志收集
|
|
163
|
+
|
|
164
|
+
- 在初始化 MyLogger 时,指定 remote_log_url 即可启用远程日志上报功能:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
logger = MyLogger(
|
|
168
|
+
file_name="app_log",
|
|
169
|
+
remote_log_url="https://your-logging-endpoint.com/logs"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
### 常见问题
|
|
179
|
+
|
|
180
|
+
#### 1. 如何关闭日志多文件策略?
|
|
181
|
+
- 如果仅需要一个主日志文件,可去掉或注释掉 `_get_level_log_path()` 相关的 `logger.add(...)` 调用。
|
|
182
|
+
- 如果希望“只按级别分文件、不需要主日志文件”,可以删除对应的添加主日志文件的 `add` 调用。
|
|
183
|
+
|
|
184
|
+
#### 2. 如何自定义轮转策略(按天、按小时等)?
|
|
185
|
+
- 将 `rotation=f"{self.max_size} MB"` 改为 `rotation="1 day"`、`rotation="00:00"` 等,即可使用 Loguru 的时间轮转功能。
|
|
186
|
+
|
|
187
|
+
#### 3. 如何自定义日志输出格式?
|
|
188
|
+
- 修改 `custom_format` 变量,或在 `logger.add()` 中使用你喜欢的格式,如 **JSON** 格式、单行简洁格式等。
|
|
189
|
+
|
|
190
|
+
#### 4. 如何在函数装饰器中抛出异常?
|
|
191
|
+
- 在装饰器里捕获异常后,如果希望装饰器内不“吞掉”异常,可在 `except` 块里添加 `raise`,这样异常会继续向上传递。
|
|
192
|
+
|
|
193
|
+
#### 5. 如何在远程收集中添加鉴权信息?
|
|
194
|
+
- 在 `_send_to_remote` 方法里,可在 `headers` 中添加 `Authorization` token 或其他自定义请求头。
|
|
195
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
# Author: gm.zhibo.wang
|
|
5
|
+
# E-mail: gm.zhibo.wang@gmail.com
|
|
6
|
+
# Date :
|
|
7
|
+
# Desc :
|
|
8
|
+
|
|
9
|
+
from setuptools import setup, find_packages
|
|
10
|
+
|
|
11
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
12
|
+
long_description = fh.read()
|
|
13
|
+
|
|
14
|
+
setup(
|
|
15
|
+
name='xmi_logger',
|
|
16
|
+
version='0.0.1',
|
|
17
|
+
author='gm.zhibo.wang',
|
|
18
|
+
author_email='gm.zhibo.wang@gmail.com',
|
|
19
|
+
description='An enhanced logger based on Loguru',
|
|
20
|
+
long_description=long_description,
|
|
21
|
+
long_description_content_type="text/markdown",
|
|
22
|
+
url='https://github.com/wang-zhibo/xmi_logger',
|
|
23
|
+
packages=find_packages(),
|
|
24
|
+
include_package_data=True,
|
|
25
|
+
python_requires='>=3.6',
|
|
26
|
+
classifiers=[
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
],
|
|
30
|
+
install_requires=[
|
|
31
|
+
'loguru==0.7.3',
|
|
32
|
+
'requests'
|
|
33
|
+
],
|
|
34
|
+
project_urls={
|
|
35
|
+
"Bug Reports": "https://github.com/wang-zhibo/xmi_logger/issues",
|
|
36
|
+
"Source": "https://github.com/wang-zhibo/xmi_logger",
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
# Author: zhibo.wang
|
|
5
|
+
# E-mail: gm.zhibo.wang@gmail.com
|
|
6
|
+
# Date : 2025-01-03
|
|
7
|
+
# Desc : Enhanced Logger with Loguru (with async support) + Language Option
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import inspect
|
|
12
|
+
import requests
|
|
13
|
+
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from functools import wraps
|
|
17
|
+
from time import perf_counter
|
|
18
|
+
from contextvars import ContextVar
|
|
19
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
20
|
+
|
|
21
|
+
from loguru import logger
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class XmiLogger:
|
|
25
|
+
"""
|
|
26
|
+
基于 Loguru 的增强日志记录器,具有以下功能:
|
|
27
|
+
- 自定义日志格式
|
|
28
|
+
- 日志轮转和保留策略
|
|
29
|
+
- 上下文信息管理(如 request_id)
|
|
30
|
+
- 远程日志收集(使用线程池防止阻塞)
|
|
31
|
+
- 装饰器用于记录函数调用和执行时间,支持同步/异步函数
|
|
32
|
+
- 自定义日志级别(避免与 Loguru 预定义的冲突)
|
|
33
|
+
- 统一异常处理
|
|
34
|
+
|
|
35
|
+
新增:
|
|
36
|
+
- 可指定语言(中文/英文),默认中文
|
|
37
|
+
- 支持按时间轮转日志
|
|
38
|
+
- 支持自定义日志格式
|
|
39
|
+
- 支持日志级别过滤
|
|
40
|
+
- 支持自定义压缩格式
|
|
41
|
+
- 支持自定义文件命名模式
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
# 在 _LANG_MAP 中添加新的语言项
|
|
45
|
+
_LANG_MAP = {
|
|
46
|
+
'zh': {
|
|
47
|
+
'LOG_STATS': "日志统计: 总计 {total} 条, 错误 {error} 条, 警告 {warning} 条, 信息 {info} 条",
|
|
48
|
+
'LOG_TAGGED': "[{tag}] {message}",
|
|
49
|
+
'LOG_CATEGORY': "分类: {category} - {message}",
|
|
50
|
+
'UNHANDLED_EXCEPTION': "未处理的异常",
|
|
51
|
+
'FAILED_REMOTE': "远程日志发送失败: {error}",
|
|
52
|
+
'START_FUNCTION_CALL': "开始函数调用",
|
|
53
|
+
'END_FUNCTION_CALL': "结束函数调用",
|
|
54
|
+
'START_ASYNC_FUNCTION_CALL': "开始异步函数调用",
|
|
55
|
+
'END_ASYNC_FUNCTION_CALL': "结束异步函数调用",
|
|
56
|
+
'CALLING_FUNCTION': "调用函数: {func},参数: {args},关键字参数: {kwargs}",
|
|
57
|
+
'CALLING_ASYNC_FUNCTION': "调用异步函数: {func},参数: {args},关键字参数: {kwargs}",
|
|
58
|
+
'FUNCTION_RETURNED': "函数 {func} 返回结果: {result},耗时: {duration:.6f}秒",
|
|
59
|
+
'ASYNC_FUNCTION_RETURNED': "异步函数 {func} 返回结果: {result},耗时: {duration:.6f}秒",
|
|
60
|
+
},
|
|
61
|
+
'en': {
|
|
62
|
+
'LOG_STATS': "Log statistics: Total {total}, Errors {error}, Warnings {warning}, Info {info}",
|
|
63
|
+
'LOG_TAGGED': "[{tag}] {message}",
|
|
64
|
+
'LOG_CATEGORY': "Category: {category} - {message}",
|
|
65
|
+
'UNHANDLED_EXCEPTION': "Unhandled exception",
|
|
66
|
+
'FAILED_REMOTE': "Remote logging failed: {error}",
|
|
67
|
+
'START_FUNCTION_CALL': "Starting function call",
|
|
68
|
+
'END_FUNCTION_CALL': "Ending function call",
|
|
69
|
+
'START_ASYNC_FUNCTION_CALL': "Starting async function call",
|
|
70
|
+
'END_ASYNC_FUNCTION_CALL': "Ending async function call",
|
|
71
|
+
'CALLING_FUNCTION': "Calling function: {func}, args: {args}, kwargs: {kwargs}",
|
|
72
|
+
'CALLING_ASYNC_FUNCTION': "Calling async function: {func}, args: {args}, kwargs: {kwargs}",
|
|
73
|
+
'FUNCTION_RETURNED': "Function {func} returned: {result}, duration: {duration:.6f}s",
|
|
74
|
+
'ASYNC_FUNCTION_RETURNED': "Async function {func} returned: {result}, duration: {duration:.6f}s",
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# 在 __init__ 方法中添加新的参数
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
file_name: str,
|
|
82
|
+
log_dir: str = 'logs',
|
|
83
|
+
max_size: int = 14, # 单位:MB
|
|
84
|
+
retention: str = '7 days',
|
|
85
|
+
remote_log_url: Optional[str] = None,
|
|
86
|
+
max_workers: int = 3,
|
|
87
|
+
work_type: bool = False,
|
|
88
|
+
language: str = 'zh', # 语言选项,默认为中文
|
|
89
|
+
rotation_time: Optional[str] = None, # 新增:按时间轮转,如 "1 day", "1 week"
|
|
90
|
+
custom_format: Optional[str] = None, # 新增:自定义日志格式
|
|
91
|
+
filter_level: str = "DEBUG", # 新增:日志过滤级别
|
|
92
|
+
compression: str = "zip", # 新增:压缩格式,支持 zip, gz, tar
|
|
93
|
+
file_pattern: str = "{time:YYYY-MM-DD}", # 新增:文件命名模式
|
|
94
|
+
enable_stats: bool = False, # 新增:是否启用日志统计
|
|
95
|
+
categories: Optional[list] = None, # 新增:日志分类列表
|
|
96
|
+
) -> None:
|
|
97
|
+
"""
|
|
98
|
+
初始化日志记录器。
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
file_name (str): 日志文件名称(主日志文件前缀)。
|
|
102
|
+
log_dir (str): 日志文件目录。
|
|
103
|
+
max_size (int): 日志文件大小(MB)超过时进行轮转。
|
|
104
|
+
retention (str): 日志保留策略。
|
|
105
|
+
remote_log_url (str, optional): 远程日志收集的URL。如果提供,将启用远程日志收集。
|
|
106
|
+
max_workers (int): 线程池的最大工作线程数。
|
|
107
|
+
work_type (bool): False 测试环境
|
|
108
|
+
language (str): 'zh' 或 'en',表示日志输出语言,默认为中文。
|
|
109
|
+
"""
|
|
110
|
+
self.file_name = file_name
|
|
111
|
+
self.log_dir = log_dir
|
|
112
|
+
self.max_size = max_size
|
|
113
|
+
self.retention = retention
|
|
114
|
+
self.remote_log_url = remote_log_url
|
|
115
|
+
|
|
116
|
+
# 保存新增的参数为实例属性
|
|
117
|
+
self.rotation_time = rotation_time
|
|
118
|
+
self.custom_format = custom_format
|
|
119
|
+
self.filter_level = filter_level
|
|
120
|
+
self.compression = compression
|
|
121
|
+
self.file_pattern = file_pattern
|
|
122
|
+
self.enable_stats = enable_stats
|
|
123
|
+
self.categories = categories or []
|
|
124
|
+
|
|
125
|
+
# 语言选项
|
|
126
|
+
self.language = language if language in ('zh', 'en') else 'zh'
|
|
127
|
+
|
|
128
|
+
# 定义上下文变量,用于存储 request_id
|
|
129
|
+
self.request_id_var = ContextVar("request_id", default="no-request-id")
|
|
130
|
+
|
|
131
|
+
# 使用 patch 确保每条日志记录都包含 'request_id'
|
|
132
|
+
self.logger = logger.patch(
|
|
133
|
+
lambda record: record["extra"].update(
|
|
134
|
+
request_id=self.request_id_var.get() or "no-request-id"
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
if work_type:
|
|
138
|
+
self.enqueue = False
|
|
139
|
+
self.diagnose = False
|
|
140
|
+
self.backtrace = False
|
|
141
|
+
else:
|
|
142
|
+
self.enqueue = True
|
|
143
|
+
self.diagnose = True
|
|
144
|
+
self.backtrace = True
|
|
145
|
+
|
|
146
|
+
# 用于远程日志发送的线程池
|
|
147
|
+
self._executor = ThreadPoolExecutor(max_workers=max_workers)
|
|
148
|
+
|
|
149
|
+
# 初始化 Logger 配置
|
|
150
|
+
self.configure_logger()
|
|
151
|
+
|
|
152
|
+
def _msg(self, key: str, **kwargs) -> str:
|
|
153
|
+
"""
|
|
154
|
+
根据当前语言,从 _LANG_MAP 中获取对应文本。
|
|
155
|
+
可使用 kwargs 替换字符串中的占位符。
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
key: Message key from _LANG_MAP
|
|
159
|
+
**kwargs: Format arguments for the message
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Formatted message string
|
|
163
|
+
"""
|
|
164
|
+
# 安全获取文本,如果键不存在则返回键名
|
|
165
|
+
text = self._LANG_MAP.get(self.language, {}).get(key, key)
|
|
166
|
+
try:
|
|
167
|
+
return text.format(**kwargs)
|
|
168
|
+
except KeyError as e:
|
|
169
|
+
# 如果格式化失败,返回原始文本并记录警告
|
|
170
|
+
return f"{text} (格式化错误: 缺少参数 {e})"
|
|
171
|
+
|
|
172
|
+
def configure_logger(self) -> None:
|
|
173
|
+
"""Configure logger with console, file and remote handlers.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
OSError: If log directory cannot be created
|
|
177
|
+
ValueError: If invalid configuration values are provided
|
|
178
|
+
"""
|
|
179
|
+
"""
|
|
180
|
+
配置 Loguru 日志记录器:控制台输出、文件输出、远程日志收集、自定义日志级别。
|
|
181
|
+
"""
|
|
182
|
+
# 移除所有现有的处理器,重新添加
|
|
183
|
+
self.logger.remove()
|
|
184
|
+
|
|
185
|
+
# 定义日志格式:可根据需要自由增减字段
|
|
186
|
+
# 包含时间、进程 ID、线程 ID、日志级别、request_id、调用位置等
|
|
187
|
+
# 目前去除进程 ID、线程 ID
|
|
188
|
+
"""
|
|
189
|
+
custom_format = (
|
|
190
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
|
191
|
+
"<cyan>PID:{process}</cyan>/<cyan>TID:{thread}</cyan> | "
|
|
192
|
+
"<level>{level: <8}</level> | "
|
|
193
|
+
"ReqID:{extra[request_id]} | "
|
|
194
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
|
195
|
+
"<level>{message}</level>"
|
|
196
|
+
)
|
|
197
|
+
"""
|
|
198
|
+
custom_format = (
|
|
199
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
|
200
|
+
"<level>{level: <8}</level> | "
|
|
201
|
+
"ReqID:{extra[request_id]} | "
|
|
202
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
|
203
|
+
"<level>{message}</level>"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# 使用自定义格式或默认格式
|
|
207
|
+
log_format = self.custom_format or custom_format
|
|
208
|
+
|
|
209
|
+
# 添加控制台处理器
|
|
210
|
+
self.logger.add(
|
|
211
|
+
sys.stdout,
|
|
212
|
+
format=log_format,
|
|
213
|
+
level=self.filter_level,
|
|
214
|
+
enqueue=True,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# 确保日志目录存在
|
|
218
|
+
try:
|
|
219
|
+
os.makedirs(self.log_dir, exist_ok=True)
|
|
220
|
+
except OSError as e:
|
|
221
|
+
self.logger.error(f"Failed to create log directory: {e}")
|
|
222
|
+
raise
|
|
223
|
+
|
|
224
|
+
# 设置日志轮转策略
|
|
225
|
+
rotation = self.rotation_time or f"{self.max_size} MB"
|
|
226
|
+
|
|
227
|
+
# 添加主日志文件
|
|
228
|
+
self.logger.add(
|
|
229
|
+
os.path.join(self.log_dir, f"{self.file_name}_{self.file_pattern}.log"),
|
|
230
|
+
format=log_format,
|
|
231
|
+
level=self.filter_level,
|
|
232
|
+
rotation=rotation,
|
|
233
|
+
retention=self.retention,
|
|
234
|
+
compression=self.compression,
|
|
235
|
+
encoding='utf-8',
|
|
236
|
+
enqueue=True,
|
|
237
|
+
diagnose=True,
|
|
238
|
+
backtrace=True,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# 仅示例演示:为 ERROR 级别单独输出到文件
|
|
242
|
+
self.logger.add(
|
|
243
|
+
self._get_level_log_path("error"),
|
|
244
|
+
format=custom_format,
|
|
245
|
+
level="ERROR",
|
|
246
|
+
rotation=f"{self.max_size} MB",
|
|
247
|
+
retention=self.retention,
|
|
248
|
+
compression="zip",
|
|
249
|
+
encoding='utf-8',
|
|
250
|
+
enqueue=self.enqueue,
|
|
251
|
+
diagnose=self.diagnose,
|
|
252
|
+
backtrace=self.backtrace,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# 远程日志收集
|
|
256
|
+
if self.remote_log_url:
|
|
257
|
+
self._configure_remote_logging()
|
|
258
|
+
|
|
259
|
+
# 设置统一异常处理
|
|
260
|
+
self.setup_exception_handler()
|
|
261
|
+
|
|
262
|
+
def _configure_remote_logging(self):
|
|
263
|
+
"""
|
|
264
|
+
配置远程日志收集。
|
|
265
|
+
"""
|
|
266
|
+
# 当远程日志收集启用时,只发送 ERROR 及以上级别的日志
|
|
267
|
+
self.logger.add(
|
|
268
|
+
self.remote_sink,
|
|
269
|
+
level="ERROR",
|
|
270
|
+
enqueue=self.enqueue,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
def log_with_tag(self, level: str, message: str, tag: str):
|
|
274
|
+
"""
|
|
275
|
+
使用标签记录日志消息。
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
level: 日志级别 (info, debug, warning, error, critical)
|
|
279
|
+
message: 日志消息
|
|
280
|
+
tag: 标签名称
|
|
281
|
+
"""
|
|
282
|
+
log_method = getattr(self.logger, level.lower(), self.logger.info)
|
|
283
|
+
tagged_message = self._msg('LOG_TAGGED', tag=tag, message=message)
|
|
284
|
+
log_method(tagged_message)
|
|
285
|
+
|
|
286
|
+
def log_with_category(self, level: str, message: str, category: str):
|
|
287
|
+
"""
|
|
288
|
+
使用分类记录日志消息。
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
level: 日志级别 (info, debug, warning, error, critical)
|
|
292
|
+
message: 日志消息
|
|
293
|
+
category: 分类名称
|
|
294
|
+
"""
|
|
295
|
+
log_method = getattr(self.logger, level.lower(), self.logger.info)
|
|
296
|
+
categorized_message = self._msg('LOG_CATEGORY', category=category, message=message)
|
|
297
|
+
log_method(categorized_message)
|
|
298
|
+
|
|
299
|
+
def setup_exception_handler(self):
|
|
300
|
+
"""
|
|
301
|
+
设置统一的异常处理函数,将未处理的异常记录到日志。
|
|
302
|
+
"""
|
|
303
|
+
def exception_handler(exc_type, exc_value, exc_traceback):
|
|
304
|
+
if issubclass(exc_type, KeyboardInterrupt):
|
|
305
|
+
# 允许程序被 Ctrl+C 中断
|
|
306
|
+
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
# 修复:不直接传递 traceback 对象,而是将异常信息格式化为字符串
|
|
310
|
+
error_msg = self._msg('UNHANDLED_EXCEPTION') if 'UNHANDLED_EXCEPTION' in self._LANG_MAP[self.language] else "未处理的异常"
|
|
311
|
+
self.logger.opt(exception=True).error(
|
|
312
|
+
f"{error_msg}: {exc_type.__name__}: {exc_value}"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
sys.excepthook = exception_handler
|
|
316
|
+
|
|
317
|
+
def _get_level_log_path(self, level_name):
|
|
318
|
+
"""
|
|
319
|
+
获取不同级别日志文件的路径。
|
|
320
|
+
"""
|
|
321
|
+
return os.path.join(self.log_dir, f"{self.file_name}_{level_name}.log")
|
|
322
|
+
|
|
323
|
+
def get_log_path(self, message):
|
|
324
|
+
"""
|
|
325
|
+
如果需要将所有日志按照级别分文件时,可使用此方法。
|
|
326
|
+
"""
|
|
327
|
+
log_level = message.record["level"].name.lower()
|
|
328
|
+
log_file = f"{log_level}.log"
|
|
329
|
+
return os.path.join(self.log_dir, log_file)
|
|
330
|
+
|
|
331
|
+
def remote_sink(self, message):
|
|
332
|
+
"""
|
|
333
|
+
自定义的远程日志处理器,将日志发送到远程服务器(使用线程池防止阻塞)。
|
|
334
|
+
"""
|
|
335
|
+
self._executor.submit(self._send_to_remote, message)
|
|
336
|
+
|
|
337
|
+
def _send_to_remote(self, message) -> None:
|
|
338
|
+
"""Send log message to remote server with retry logic.
|
|
339
|
+
|
|
340
|
+
Args:
|
|
341
|
+
message: Log message to send
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
None
|
|
345
|
+
"""
|
|
346
|
+
"""
|
|
347
|
+
线程池中实际执行的远程日志发送逻辑。
|
|
348
|
+
"""
|
|
349
|
+
log_entry = message.record
|
|
350
|
+
payload = {
|
|
351
|
+
"time": log_entry["time"].strftime("%Y-%m-%d %H:%M:%S"),
|
|
352
|
+
"level": log_entry["level"].name,
|
|
353
|
+
"message": log_entry["message"],
|
|
354
|
+
"file": os.path.basename(log_entry["file"].path) if log_entry["file"] else "",
|
|
355
|
+
"line": log_entry["line"],
|
|
356
|
+
"function": log_entry["function"],
|
|
357
|
+
"request_id": log_entry["extra"].get("request_id", "no-request-id")
|
|
358
|
+
}
|
|
359
|
+
headers = {"Content-Type": "application/json"}
|
|
360
|
+
|
|
361
|
+
max_retries = 3
|
|
362
|
+
retry_delay = 1 # seconds
|
|
363
|
+
|
|
364
|
+
for attempt in range(max_retries):
|
|
365
|
+
try:
|
|
366
|
+
response = requests.post(
|
|
367
|
+
self.remote_log_url,
|
|
368
|
+
headers=headers,
|
|
369
|
+
json=payload,
|
|
370
|
+
timeout=5
|
|
371
|
+
)
|
|
372
|
+
response.raise_for_status()
|
|
373
|
+
return
|
|
374
|
+
except requests.RequestException as e:
|
|
375
|
+
if attempt == max_retries - 1: # Last attempt
|
|
376
|
+
self.logger.warning(
|
|
377
|
+
self._msg('FAILED_REMOTE', error=f"Final attempt failed: {e}")
|
|
378
|
+
)
|
|
379
|
+
else:
|
|
380
|
+
time.sleep(retry_delay * (attempt + 1))
|
|
381
|
+
|
|
382
|
+
def add_custom_level(self, level_name, no, color, icon):
|
|
383
|
+
"""
|
|
384
|
+
增加自定义日志级别。
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
level_name (str): 日志级别名称。
|
|
388
|
+
no (int): 日志级别编号。
|
|
389
|
+
color (str): 日志级别颜色。
|
|
390
|
+
icon (str): 日志级别图标。
|
|
391
|
+
"""
|
|
392
|
+
try:
|
|
393
|
+
self.logger.level(level_name, no=no, color=color, icon=icon)
|
|
394
|
+
self.logger.debug(f"Custom log level '{level_name}' added.")
|
|
395
|
+
except TypeError:
|
|
396
|
+
# 如果日志级别已存在,记录调试信息
|
|
397
|
+
self.logger.debug(f"Log level '{level_name}' already exists, skipping.")
|
|
398
|
+
|
|
399
|
+
def __getattr__(self, level: str):
|
|
400
|
+
"""
|
|
401
|
+
使 MyLogger 支持直接调用 Loguru 的日志级别方法。
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
level (str): 日志级别方法名称。
|
|
405
|
+
"""
|
|
406
|
+
return getattr(self.logger, level)
|
|
407
|
+
|
|
408
|
+
def log_decorator(self, msg: Optional[str] = None, level: str = "ERROR", trace: bool = True):
|
|
409
|
+
"""
|
|
410
|
+
增强版日志装饰器,支持自定义日志级别和跟踪配置
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
msg (str): 支持多语言的异常提示信息key(使用_LANG_MAP中的键)
|
|
414
|
+
level (str): 记录异常的日志级别(默认ERROR)
|
|
415
|
+
trace (bool): 是否记录完整堆栈跟踪(默认True)
|
|
416
|
+
"""
|
|
417
|
+
def decorator(func):
|
|
418
|
+
_msg_key = msg or 'UNHANDLED_EXCEPTION'
|
|
419
|
+
log_level = level.upper()
|
|
420
|
+
|
|
421
|
+
if inspect.iscoroutinefunction(func):
|
|
422
|
+
@wraps(func)
|
|
423
|
+
async def async_wrapper(*args, **kwargs):
|
|
424
|
+
self._log_start(func.__name__, args, kwargs, is_async=True)
|
|
425
|
+
start_time = perf_counter()
|
|
426
|
+
try:
|
|
427
|
+
result = await func(*args, **kwargs)
|
|
428
|
+
duration = perf_counter() - start_time
|
|
429
|
+
self._log_end(func.__name__, result, duration, is_async=True)
|
|
430
|
+
return result
|
|
431
|
+
except Exception as e:
|
|
432
|
+
self._log_exception(func.__name__, e, _msg_key, log_level, trace, is_async=True)
|
|
433
|
+
if trace:
|
|
434
|
+
raise
|
|
435
|
+
return None
|
|
436
|
+
return async_wrapper
|
|
437
|
+
else:
|
|
438
|
+
@wraps(func)
|
|
439
|
+
def sync_wrapper(*args, **kwargs):
|
|
440
|
+
self._log_start(func.__name__, args, kwargs, is_async=False)
|
|
441
|
+
start_time = perf_counter()
|
|
442
|
+
try:
|
|
443
|
+
result = func(*args, **kwargs)
|
|
444
|
+
duration = perf_counter() - start_time
|
|
445
|
+
self._log_end(func.__name__, result, duration, is_async=False)
|
|
446
|
+
return result
|
|
447
|
+
except Exception as e:
|
|
448
|
+
self._log_exception(func.__name__, e, _msg_key, log_level, trace, is_async=False)
|
|
449
|
+
if trace:
|
|
450
|
+
raise
|
|
451
|
+
return None
|
|
452
|
+
return sync_wrapper
|
|
453
|
+
return decorator
|
|
454
|
+
|
|
455
|
+
def _log_exception(self, func_name: str, error: Exception, msg_key: str,
|
|
456
|
+
level: str, trace: bool, is_async: bool):
|
|
457
|
+
"""统一的异常记录处理"""
|
|
458
|
+
log_method = getattr(self.logger, level.lower(), self.logger.error)
|
|
459
|
+
|
|
460
|
+
# 获取消息,如果键不存在则使用默认消息
|
|
461
|
+
error_msg = self._msg(msg_key) if msg_key in self._LANG_MAP[self.language] else f"发生异常: {msg_key}"
|
|
462
|
+
error_msg += f" [{type(error).__name__}]"
|
|
463
|
+
|
|
464
|
+
if trace:
|
|
465
|
+
# 修复:避免直接传递 traceback 对象
|
|
466
|
+
log_method(f"{error_msg}: {str(error)}")
|
|
467
|
+
# 单独记录异常堆栈,但不使用 enqueue
|
|
468
|
+
self.logger.opt(exception=True).error("异常堆栈:")
|
|
469
|
+
else:
|
|
470
|
+
log_method(f"{error_msg}: {str(error)}")
|
|
471
|
+
|
|
472
|
+
if is_async:
|
|
473
|
+
self.logger.info(self._msg('END_ASYNC_FUNCTION_CALL') if 'END_ASYNC_FUNCTION_CALL' in self._LANG_MAP[self.language] else "异步函数调用结束")
|
|
474
|
+
else:
|
|
475
|
+
self.logger.info(self._msg('END_FUNCTION_CALL') if 'END_FUNCTION_CALL' in self._LANG_MAP[self.language] else "函数调用结束")
|
|
476
|
+
|
|
477
|
+
def _log_start(self, func_name, args, kwargs, is_async=False):
|
|
478
|
+
"""
|
|
479
|
+
记录函数调用开始的公共逻辑。
|
|
480
|
+
"""
|
|
481
|
+
if is_async:
|
|
482
|
+
self.logger.info(self._msg('START_ASYNC_FUNCTION_CALL'))
|
|
483
|
+
self.logger.info(
|
|
484
|
+
self._msg('CALLING_ASYNC_FUNCTION', func=func_name, args=args, kwargs=kwargs)
|
|
485
|
+
)
|
|
486
|
+
else:
|
|
487
|
+
self.logger.info(self._msg('START_FUNCTION_CALL'))
|
|
488
|
+
self.logger.info(
|
|
489
|
+
self._msg('CALLING_FUNCTION', func=func_name, args=args, kwargs=kwargs)
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def _log_end(self, func_name, result, duration, is_async=False):
|
|
493
|
+
"""
|
|
494
|
+
记录函数调用结束的公共逻辑。
|
|
495
|
+
"""
|
|
496
|
+
if is_async:
|
|
497
|
+
self.logger.info(
|
|
498
|
+
self._msg('ASYNC_FUNCTION_RETURNED', func=func_name, result=result, duration=duration)
|
|
499
|
+
)
|
|
500
|
+
self.logger.info(self._msg('END_ASYNC_FUNCTION_CALL'))
|
|
501
|
+
else:
|
|
502
|
+
self.logger.info(
|
|
503
|
+
self._msg('FUNCTION_RETURNED', func=func_name, result=result, duration=duration)
|
|
504
|
+
)
|
|
505
|
+
self.logger.info(self._msg('END_FUNCTION_CALL'))
|
|
506
|
+
|
|
507
|
+
def get_stats(self):
|
|
508
|
+
"""
|
|
509
|
+
获取日志统计信息。
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
str: 格式化的日志统计信息字符串
|
|
513
|
+
"""
|
|
514
|
+
# 初始化统计数据
|
|
515
|
+
stats = {
|
|
516
|
+
'total': 0,
|
|
517
|
+
'error': 0,
|
|
518
|
+
'warning': 0,
|
|
519
|
+
'info': 0
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
# 如果启用了统计功能,尝试从日志文件中获取统计数据
|
|
523
|
+
if self.enable_stats:
|
|
524
|
+
try:
|
|
525
|
+
# 这里我们可以尝试从日志文件中读取统计信息
|
|
526
|
+
# 简单起见,这里只返回一些示例数据
|
|
527
|
+
stats['total'] = 100
|
|
528
|
+
stats['error'] = 5
|
|
529
|
+
stats['warning'] = 15
|
|
530
|
+
stats['info'] = 80
|
|
531
|
+
except Exception as e:
|
|
532
|
+
self.logger.error(f"获取日志统计信息失败: {e}")
|
|
533
|
+
|
|
534
|
+
# 返回统计信息的格式化字符串
|
|
535
|
+
return self._msg('LOG_STATS', **stats)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
"""
|
|
539
|
+
# ==========================
|
|
540
|
+
# 以下为使用示例
|
|
541
|
+
# ==========================
|
|
542
|
+
if __name__ == '__main__':
|
|
543
|
+
import time
|
|
544
|
+
import json
|
|
545
|
+
import asyncio
|
|
546
|
+
|
|
547
|
+
# 初始化日志记录器
|
|
548
|
+
# - language='zh' 输出中文
|
|
549
|
+
# - language='en' 输出英文
|
|
550
|
+
remote_log_url = None # "https://your-logging-endpoint.com/logs"
|
|
551
|
+
log = MyLogger("test_log", remote_log_url=remote_log_url, language='zh')
|
|
552
|
+
|
|
553
|
+
@log.log_decorator("ZeroDivisionError occurred.")
|
|
554
|
+
def test_zero_division_error(a, b):
|
|
555
|
+
return a / b
|
|
556
|
+
|
|
557
|
+
@log.log_decorator("JSONDecodeError occurred.")
|
|
558
|
+
def test_error():
|
|
559
|
+
json.loads("asdasd")
|
|
560
|
+
|
|
561
|
+
@log.log_decorator("Function execution took too long.")
|
|
562
|
+
def compute_something_sync():
|
|
563
|
+
time.sleep(1)
|
|
564
|
+
return "Sync computation completed"
|
|
565
|
+
|
|
566
|
+
@log.log_decorator("Async function execution took too long.")
|
|
567
|
+
async def compute_something_async():
|
|
568
|
+
await asyncio.sleep(1)
|
|
569
|
+
return "Async computation completed"
|
|
570
|
+
|
|
571
|
+
# 设置 request_id
|
|
572
|
+
token = log.request_id_var.set("12345")
|
|
573
|
+
|
|
574
|
+
try:
|
|
575
|
+
# 常见日志级别示例
|
|
576
|
+
log.info('This is an info log.')
|
|
577
|
+
log.debug('This is a debug log.')
|
|
578
|
+
log.warning('This is a warning log.')
|
|
579
|
+
log.error('This is an error log.')
|
|
580
|
+
log.critical('This is a critical log.')
|
|
581
|
+
log.trace('This is a TRACE level log (Loguru default).')
|
|
582
|
+
|
|
583
|
+
# 测试同步函数
|
|
584
|
+
try:
|
|
585
|
+
result = test_zero_division_error(1, 0)
|
|
586
|
+
log.info(f"test_zero_division_error result: {result}")
|
|
587
|
+
except ZeroDivisionError:
|
|
588
|
+
log.exception("Caught a ZeroDivisionError.")
|
|
589
|
+
result = test_zero_division_error(1, 1)
|
|
590
|
+
|
|
591
|
+
# 测试另一个示例函数
|
|
592
|
+
try:
|
|
593
|
+
result = test_error()
|
|
594
|
+
except json.JSONDecodeError:
|
|
595
|
+
log.exception("Caught a JSONDecodeError.")
|
|
596
|
+
|
|
597
|
+
# 测试同步函数
|
|
598
|
+
result = compute_something_sync()
|
|
599
|
+
log.info(f"compute_something_sync result: {result}")
|
|
600
|
+
|
|
601
|
+
# 测试异步函数
|
|
602
|
+
async def main():
|
|
603
|
+
result = await compute_something_async()
|
|
604
|
+
log.info(f"compute_something_async result: {result}")
|
|
605
|
+
|
|
606
|
+
asyncio.run(main())
|
|
607
|
+
|
|
608
|
+
finally:
|
|
609
|
+
# 重置 request_id
|
|
610
|
+
log.request_id_var.reset(token)
|
|
611
|
+
log.info("All done.")
|
|
612
|
+
"""
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xmi_logger
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: An enhanced logger based on Loguru
|
|
5
|
+
Home-page: https://github.com/wang-zhibo/xmi_logger
|
|
6
|
+
Author: gm.zhibo.wang
|
|
7
|
+
Author-email: gm.zhibo.wang@gmail.com
|
|
8
|
+
Project-URL: Bug Reports, https://github.com/wang-zhibo/xmi_logger/issues
|
|
9
|
+
Project-URL: Source, https://github.com/wang-zhibo/xmi_logger
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.6
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: loguru==0.7.3
|
|
15
|
+
Requires-Dist: requests
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: project-url
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
26
|
+
|
|
27
|
+
## Enhanced Logger
|
|
28
|
+
|
|
29
|
+
这是一个基于 [Loguru](https://github.com/Delgan/loguru) 的扩展日志记录器,提供了一系列增强特性,包括:
|
|
30
|
+
|
|
31
|
+
- 自定义日志格式
|
|
32
|
+
- 日志轮转和保留策略
|
|
33
|
+
- 上下文信息管理(如 `request_id`)
|
|
34
|
+
- 远程日志收集(使用线程池防止阻塞)
|
|
35
|
+
- 装饰器用于记录函数调用和执行时间,支持同步/异步函数
|
|
36
|
+
- 自定义日志级别(避免与 Loguru 预定义的冲突)
|
|
37
|
+
- 统一异常处理
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### 功能概述
|
|
42
|
+
|
|
43
|
+
1. **自定义日志格式**
|
|
44
|
+
可自由配置字段,如时间、进程/线程 ID、日志级别、请求 ID、所在文件、函数、行号等。
|
|
45
|
+
|
|
46
|
+
2. **日志轮转与保留**
|
|
47
|
+
- 支持按照文件大小、时间或文件数量进行滚动,并可自动删除过期日志。
|
|
48
|
+
- 默认使用大小轮转:单个文件超过 `max_size` MB 时自动滚动。
|
|
49
|
+
- 默认保留策略 `retention='9 days'`,可根据需要自定义。
|
|
50
|
+
|
|
51
|
+
3. **上下文管理**
|
|
52
|
+
- 使用 `ContextVar` 储存 `request_id`,可在异步环境中区分不同请求来源的日志。
|
|
53
|
+
|
|
54
|
+
4. **远程日志收集**
|
|
55
|
+
- 通过自定义处理器,使用线程池的方式将日志上报到远程服务,避免主线程阻塞。
|
|
56
|
+
- 默认仅收集 `ERROR` 及以上等级的日志。可在 `_configure_remote_logging()` 方法中自行配置。
|
|
57
|
+
|
|
58
|
+
5. **装饰器**
|
|
59
|
+
- `log_decorator` 可装饰任意同步或异步函数,自动记录:
|
|
60
|
+
- 函数调用开始
|
|
61
|
+
- 参数、返回值
|
|
62
|
+
- 函数执行耗时
|
|
63
|
+
- 异常信息(可选择是否抛出异常)
|
|
64
|
+
|
|
65
|
+
6. **自定义日志级别**
|
|
66
|
+
- 通过 `add_custom_level` 方法添加额外的日志级别(如 `AUDIT`, `SECURITY` 等),避免与已有日志级别冲突。
|
|
67
|
+
|
|
68
|
+
7. **统一异常处理**
|
|
69
|
+
- 注册全局异常处理 (`sys.excepthook`),捕获任何未处理的异常并记录。
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
### 目录结构
|
|
74
|
+
|
|
75
|
+
. ├── logs/ # 日志存放目录(默认) ├── my_logger.py # MyLogger 类源码 ├── README.md # 使用说明 └── requirements.txt # Python依赖(如有)
|
|
76
|
+
|
|
77
|
+
yaml
|
|
78
|
+
复制代码
|
|
79
|
+
|
|
80
|
+
> 其中 `logs/` 是默认日志目录,可以通过初始化时的 `log_dir` 参数修改。
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
### 安装
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install xdeek-logger
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
### 使用示例
|
|
93
|
+
example/main.py
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
### 导入并使用
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
from xdeek_logger import MyLogger
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
初始化日志记录器
|
|
103
|
+
可自定义:
|
|
104
|
+
- 主日志文件名 (e.g., "app_log")
|
|
105
|
+
- 日志目录 log_dir (默认 "logs")
|
|
106
|
+
- 单个日志文件体积最大值 max_size (MB)
|
|
107
|
+
- 日志保留策略 retention (e.g., "7 days")
|
|
108
|
+
- 远程日志收集地址 remote_log_url (默认 None)
|
|
109
|
+
- 线程池最大工作线程数 max_workers (默认 5)
|
|
110
|
+
"""
|
|
111
|
+
logger = MyLogger(
|
|
112
|
+
file_name="app_log",
|
|
113
|
+
log_dir="logs",
|
|
114
|
+
max_size=50,
|
|
115
|
+
retention="7 days",
|
|
116
|
+
remote_log_url=None,
|
|
117
|
+
max_workers=5,
|
|
118
|
+
language='zh' # 新增:语言选项,默认为中文
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 调用日志方法
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
"""直接使用 Loguru 的常见日志方法"""
|
|
127
|
+
logger.info("This is an info message.")
|
|
128
|
+
logger.debug("Debug details here.")
|
|
129
|
+
logger.warning("Be cautious!")
|
|
130
|
+
logger.error("An error occurred.")
|
|
131
|
+
logger.critical("Critical issue!")
|
|
132
|
+
logger.trace("This is a trace message - only if Loguru TRACE level is enabled.")
|
|
133
|
+
|
|
134
|
+
logger.log("CUSTOM_LEVEL", "A special custom message.")
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 使用装饰器记录函数调用
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
@logger.log_decorator("A division error occurred.")
|
|
142
|
+
def divide(a, b):
|
|
143
|
+
return a / b
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
result = divide(10, 0)
|
|
147
|
+
"""# 将触发 ZeroDivisionError"""
|
|
148
|
+
except ZeroDivisionError:
|
|
149
|
+
logger.exception("Handled ZeroDivisionError.")
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
- 此装饰器会自动在函数开始和结束时分别记录函数名、参数、返回值以及耗时。
|
|
153
|
+
- 如果出现异常,则记录 traceback 并打印自定义提示信息。
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
### 记录异步函数调用
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
import asyncio
|
|
160
|
+
|
|
161
|
+
@logger.log_decorator("Async function error.")
|
|
162
|
+
async def async_task():
|
|
163
|
+
await asyncio.sleep(1)
|
|
164
|
+
return "Async result"
|
|
165
|
+
|
|
166
|
+
async def main():
|
|
167
|
+
result = await async_task()
|
|
168
|
+
logger.info(f"Result: {result}")
|
|
169
|
+
|
|
170
|
+
asyncio.run(main())
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### 设置和重置 request_id
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
"""# 设置某个上下文的 request_id"""
|
|
178
|
+
token = logger.request_id_var.set("12345")
|
|
179
|
+
|
|
180
|
+
"""# ...执行与你的请求相关的操作,所有日志都带上 request_id=12345"""
|
|
181
|
+
|
|
182
|
+
"""# 结束后重置"""
|
|
183
|
+
logger.request_id_var.reset(token)
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
### 远程日志收集
|
|
189
|
+
|
|
190
|
+
- 在初始化 MyLogger 时,指定 remote_log_url 即可启用远程日志上报功能:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
logger = MyLogger(
|
|
194
|
+
file_name="app_log",
|
|
195
|
+
remote_log_url="https://your-logging-endpoint.com/logs"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
### 常见问题
|
|
205
|
+
|
|
206
|
+
#### 1. 如何关闭日志多文件策略?
|
|
207
|
+
- 如果仅需要一个主日志文件,可去掉或注释掉 `_get_level_log_path()` 相关的 `logger.add(...)` 调用。
|
|
208
|
+
- 如果希望“只按级别分文件、不需要主日志文件”,可以删除对应的添加主日志文件的 `add` 调用。
|
|
209
|
+
|
|
210
|
+
#### 2. 如何自定义轮转策略(按天、按小时等)?
|
|
211
|
+
- 将 `rotation=f"{self.max_size} MB"` 改为 `rotation="1 day"`、`rotation="00:00"` 等,即可使用 Loguru 的时间轮转功能。
|
|
212
|
+
|
|
213
|
+
#### 3. 如何自定义日志输出格式?
|
|
214
|
+
- 修改 `custom_format` 变量,或在 `logger.add()` 中使用你喜欢的格式,如 **JSON** 格式、单行简洁格式等。
|
|
215
|
+
|
|
216
|
+
#### 4. 如何在函数装饰器中抛出异常?
|
|
217
|
+
- 在装饰器里捕获异常后,如果希望装饰器内不“吞掉”异常,可在 `except` 块里添加 `raise`,这样异常会继续向上传递。
|
|
218
|
+
|
|
219
|
+
#### 5. 如何在远程收集中添加鉴权信息?
|
|
220
|
+
- 在 `_send_to_remote` 方法里,可在 `headers` 中添加 `Authorization` token 或其他自定义请求头。
|
|
221
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xmi_logger
|