mdbq 4.0.35__py3-none-any.whl → 4.0.36__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.
- mdbq/__version__.py +1 -1
- mdbq/log/mylogger.py +84 -13
- mdbq/other/error_handler.py +227 -79
- {mdbq-4.0.35.dist-info → mdbq-4.0.36.dist-info}/METADATA +1 -1
- {mdbq-4.0.35.dist-info → mdbq-4.0.36.dist-info}/RECORD +7 -7
- {mdbq-4.0.35.dist-info → mdbq-4.0.36.dist-info}/WHEEL +0 -0
- {mdbq-4.0.35.dist-info → mdbq-4.0.36.dist-info}/top_level.txt +0 -0
mdbq/__version__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
VERSION = '4.0.
|
1
|
+
VERSION = '4.0.36'
|
mdbq/log/mylogger.py
CHANGED
@@ -12,6 +12,7 @@ import atexit
|
|
12
12
|
import traceback
|
13
13
|
import inspect
|
14
14
|
import psutil
|
15
|
+
import multiprocessing
|
15
16
|
|
16
17
|
|
17
18
|
def get_caller_filename(default='mylogger'):
|
@@ -70,7 +71,8 @@ class MyLogger:
|
|
70
71
|
enable_metrics: bool = False,
|
71
72
|
metrics_interval: int = 300,
|
72
73
|
message_limited: int = 1000,
|
73
|
-
flush_interval: int = 5
|
74
|
+
flush_interval: int = 5,
|
75
|
+
enable_multiprocess: bool = False
|
74
76
|
):
|
75
77
|
"""
|
76
78
|
初始化日志器
|
@@ -90,6 +92,7 @@ class MyLogger:
|
|
90
92
|
:param metrics_interval: 指标采集间隔(秒)
|
91
93
|
:param message_limited: 简化日志内容,避免过长
|
92
94
|
:param flush_interval: 定时刷新日志器间隔(秒)
|
95
|
+
:param enable_multiprocess: 是否启用多进程安全日志
|
93
96
|
"""
|
94
97
|
log_path = os.path.join(os.path.expanduser("~"), 'logfile')
|
95
98
|
if name is None:
|
@@ -113,8 +116,14 @@ class MyLogger:
|
|
113
116
|
self.filters = filters or []
|
114
117
|
self.enable_metrics = enable_metrics
|
115
118
|
self.metrics_interval = metrics_interval
|
116
|
-
self.message_limited = message_limited
|
117
|
-
self.flush_interval = flush_interval
|
119
|
+
self.message_limited = max(1, int(message_limited))
|
120
|
+
self.flush_interval = max(1, int(flush_interval))
|
121
|
+
self.enable_multiprocess = enable_multiprocess
|
122
|
+
self._mp_queue = None
|
123
|
+
self._mp_writer_process = None
|
124
|
+
self._is_main_process = multiprocessing.current_process().name == 'MainProcess'
|
125
|
+
self._stop_event = threading.Event()
|
126
|
+
self._flush_thread = None
|
118
127
|
|
119
128
|
# 上下文相关
|
120
129
|
self._context = threading.local()
|
@@ -133,7 +142,9 @@ class MyLogger:
|
|
133
142
|
self.logger = logging.getLogger(name)
|
134
143
|
self._init_logging()
|
135
144
|
|
136
|
-
if self.
|
145
|
+
if self.enable_multiprocess:
|
146
|
+
self._setup_multiprocess_logging()
|
147
|
+
elif self.enable_async:
|
137
148
|
self._setup_async_logging()
|
138
149
|
|
139
150
|
atexit.register(self.shutdown)
|
@@ -184,14 +195,18 @@ class MyLogger:
|
|
184
195
|
class SimpleFormatter(logging.Formatter):
|
185
196
|
def format(self, record):
|
186
197
|
msg = super().format(record)
|
187
|
-
|
188
|
-
|
198
|
+
# 统一处理 extra_data 字段
|
199
|
+
extra_data = getattr(record, 'extra_data', None)
|
200
|
+
if not extra_data and hasattr(record, 'extra'):
|
201
|
+
extra_data = getattr(record, 'extra', None)
|
202
|
+
if extra_data:
|
203
|
+
context_data = extra_data.get('context_data', {})
|
189
204
|
if context_data:
|
190
205
|
msg += f" | Context: {context_data}"
|
191
|
-
metrics =
|
206
|
+
metrics = extra_data.get('性能指标', {})
|
192
207
|
if metrics:
|
193
208
|
msg += f" | Metrics: {metrics}"
|
194
|
-
extra = {k: v for k, v in
|
209
|
+
extra = {k: v for k, v in extra_data.items()
|
195
210
|
if k not in ('context_data', '性能指标')}
|
196
211
|
if extra:
|
197
212
|
msg += f" | Extra: {extra}"
|
@@ -254,6 +269,39 @@ class MyLogger:
|
|
254
269
|
)
|
255
270
|
self._queue_listener.start()
|
256
271
|
|
272
|
+
def _setup_multiprocess_logging(self):
|
273
|
+
"""多进程安全日志:主进程写日志,子进程投递消息"""
|
274
|
+
self._mp_queue = multiprocessing.Queue(self.buffer_size)
|
275
|
+
if self._is_main_process:
|
276
|
+
# 主进程:启动写入进程
|
277
|
+
self._mp_writer_process = multiprocessing.Process(
|
278
|
+
target=self._mp_writer_worker,
|
279
|
+
args=(self._mp_queue,),
|
280
|
+
name=f"{self.name}_mp_writer",
|
281
|
+
daemon=True
|
282
|
+
)
|
283
|
+
self._mp_writer_process.start()
|
284
|
+
else:
|
285
|
+
# 子进程:不需要写入进程
|
286
|
+
pass
|
287
|
+
|
288
|
+
def _mp_writer_worker(self, log_queue):
|
289
|
+
"""日志写入进程,消费队列并写日志"""
|
290
|
+
# 重新初始化logger和handlers(避免多进程fork后锁混乱)
|
291
|
+
self._init_logging()
|
292
|
+
while True:
|
293
|
+
try:
|
294
|
+
record = log_queue.get()
|
295
|
+
if record is None:
|
296
|
+
break
|
297
|
+
level, message, extra = record
|
298
|
+
self._sync_log(level, message, extra)
|
299
|
+
except Exception as e:
|
300
|
+
try:
|
301
|
+
self.logger.error(f"多进程日志写入异常: {e}", extra={'extra_data': {'mp_writer_error': str(e)}})
|
302
|
+
except:
|
303
|
+
pass
|
304
|
+
|
257
305
|
def _get_system_metrics(self) -> Dict[str, Any]:
|
258
306
|
"""获取系统资源使用指标"""
|
259
307
|
if not self.enable_metrics:
|
@@ -337,7 +385,17 @@ class MyLogger:
|
|
337
385
|
|
338
386
|
@log_error_handler(retry_times=1, fallback_level='warning')
|
339
387
|
def _sync_log(self, level: str, message: str, extra: Optional[Dict] = None):
|
340
|
-
|
388
|
+
if self.enable_multiprocess and not self._is_main_process:
|
389
|
+
# 子进程:只投递消息
|
390
|
+
try:
|
391
|
+
self._mp_queue.put((level, message, extra), block=False)
|
392
|
+
except Exception as e:
|
393
|
+
# 投递失败降级本地输出
|
394
|
+
logging.basicConfig()
|
395
|
+
fallback_logger = logging.getLogger(f"{getattr(self, 'name', 'mylogger')}_mp_fallback")
|
396
|
+
fallback_logger.warning(f"[多进程投递失败] {message} {e}")
|
397
|
+
return
|
398
|
+
# 主进程/普通模式:正常写日志
|
341
399
|
if not hasattr(self.logger, level.lower()):
|
342
400
|
return
|
343
401
|
if not isinstance(message, str):
|
@@ -462,7 +520,7 @@ class MyLogger:
|
|
462
520
|
def _format_traceback(self, exc_info):
|
463
521
|
"""格式化异常堆栈"""
|
464
522
|
if exc_info is None:
|
465
|
-
return ""
|
523
|
+
return "No traceback available"
|
466
524
|
return ''.join(traceback.format_exception(type(exc_info), exc_info, exc_info.__traceback__))
|
467
525
|
|
468
526
|
def timeit(self, message: str = "Execution time"):
|
@@ -481,8 +539,7 @@ class MyLogger:
|
|
481
539
|
|
482
540
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
483
541
|
elapsed = time.time() - self.start_time
|
484
|
-
self.logger.info(f"{self.message}: {elapsed:.3f}s",
|
485
|
-
extra={'elapsed_seconds': elapsed})
|
542
|
+
self.logger.info(f"{self.message}: {elapsed:.3f}s", extra={'elapsed_seconds': f"{elapsed:.3f}"})
|
486
543
|
return False
|
487
544
|
|
488
545
|
def _start_flush_thread(self):
|
@@ -522,13 +579,27 @@ class MyLogger:
|
|
522
579
|
|
523
580
|
def shutdown(self):
|
524
581
|
"""关闭日志记录器,确保所有日志被刷新"""
|
582
|
+
if self.enable_multiprocess and self._is_main_process and self._mp_writer_process:
|
583
|
+
try:
|
584
|
+
self._mp_queue.put(None)
|
585
|
+
self._mp_writer_process.join(timeout=5)
|
586
|
+
except:
|
587
|
+
pass
|
588
|
+
if self.enable_multiprocess and self._mp_queue is not None:
|
589
|
+
try:
|
590
|
+
self._mp_queue.close()
|
591
|
+
self._mp_queue.join_thread()
|
592
|
+
except:
|
593
|
+
pass
|
594
|
+
self._mp_queue = None
|
525
595
|
if self.enable_async and self._queue_listener:
|
526
596
|
self._queue_listener.stop()
|
527
|
-
for handler in self.logger.handlers:
|
597
|
+
for handler in self.logger.handlers[:]:
|
528
598
|
try:
|
529
599
|
handler.close()
|
530
600
|
except:
|
531
601
|
pass
|
602
|
+
self.logger.removeHandler(handler)
|
532
603
|
for handler in getattr(self, '_handlers', []):
|
533
604
|
try:
|
534
605
|
handler.close()
|
mdbq/other/error_handler.py
CHANGED
@@ -2,57 +2,214 @@ import traceback
|
|
2
2
|
import sys
|
3
3
|
from functools import wraps
|
4
4
|
import inspect
|
5
|
+
import asyncio
|
6
|
+
from typing import Callable, Optional, Any, List
|
7
|
+
import logging
|
8
|
+
import json
|
5
9
|
|
6
10
|
|
7
|
-
|
11
|
+
class _ErrorHandlerHelper:
|
12
|
+
@staticmethod
|
13
|
+
def get_default_logger():
|
14
|
+
default_logger = logging.getLogger("mdbq.error_handler.default")
|
15
|
+
handler_exists = any(isinstance(h, logging.StreamHandler) for h in default_logger.handlers)
|
16
|
+
if not handler_exists:
|
17
|
+
handler = logging.StreamHandler()
|
18
|
+
handler.setLevel(logging.INFO)
|
19
|
+
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
20
|
+
handler.setFormatter(formatter)
|
21
|
+
default_logger.addHandler(handler)
|
22
|
+
default_logger.setLevel(logging.INFO)
|
23
|
+
default_logger.propagate = False
|
24
|
+
return default_logger
|
25
|
+
|
26
|
+
@staticmethod
|
27
|
+
def filter_fields(info: dict, log_fields):
|
28
|
+
if not log_fields:
|
29
|
+
return info
|
30
|
+
return {k: info[k] for k in log_fields if k in info}
|
31
|
+
|
32
|
+
@staticmethod
|
33
|
+
def build_error_info(func, e, args, kwargs, stack_summary):
|
34
|
+
tb = traceback.extract_tb(sys.exc_info()[2])
|
35
|
+
last_tb = tb[-1] if tb else None
|
36
|
+
return {
|
37
|
+
'函数': func.__name__,
|
38
|
+
'模块': func.__module__,
|
39
|
+
'类型': type(e).__name__,
|
40
|
+
'消息': str(e),
|
41
|
+
'签名': str(inspect.signature(func)),
|
42
|
+
'args': [str(arg) for arg in args] if args else [],
|
43
|
+
'kwargs': {k: str(v) for k, v in kwargs.items()} if kwargs else {},
|
44
|
+
'函数文件': func.__code__.co_filename,
|
45
|
+
'函数行号': func.__code__.co_firstlineno,
|
46
|
+
'异常行号': last_tb.lineno if last_tb else None,
|
47
|
+
'异常文件': last_tb.filename if last_tb else None,
|
48
|
+
'堆栈': stack_summary,
|
49
|
+
}
|
50
|
+
|
51
|
+
@staticmethod
|
52
|
+
def build_final_error_info(func, last_exception, max_retries):
|
53
|
+
tb = traceback.extract_tb(sys.exc_info()[2])
|
54
|
+
last_tb = tb[-1] if tb else None
|
55
|
+
return {
|
56
|
+
'函数': func.__name__,
|
57
|
+
'最终错误类型': type(last_exception).__name__,
|
58
|
+
'最终错误消息': str(last_exception),
|
59
|
+
'总尝试次数': max_retries,
|
60
|
+
'堆栈跟踪': traceback.format_exc(),
|
61
|
+
'异常行号': last_tb.lineno if last_tb else None,
|
62
|
+
'异常文件': last_tb.filename if last_tb else None,
|
63
|
+
}
|
64
|
+
|
65
|
+
@staticmethod
|
66
|
+
def get_stack_summary():
|
67
|
+
stack_lines = traceback.format_exc().splitlines(keepends=True)
|
68
|
+
if len(stack_lines) > 40:
|
69
|
+
return ''.join(stack_lines[:20]) + '\n...\n' + ''.join(stack_lines[-20:])
|
70
|
+
else:
|
71
|
+
return ''.join(stack_lines)
|
72
|
+
|
73
|
+
|
74
|
+
def log_on_exception(
|
75
|
+
logger=None,
|
76
|
+
*,
|
77
|
+
on_exception: Optional[Callable[[dict], None]] = None,
|
78
|
+
default_return: Any = None,
|
79
|
+
log_fields: Optional[List[str]] = None,
|
80
|
+
):
|
81
|
+
"""
|
82
|
+
:param logger: 日志对象
|
83
|
+
:param on_exception: 异常回调,参数为 error_info 字典
|
84
|
+
:param default_return: 异常时返回的默认值
|
85
|
+
:param log_fields: 只记录 error_info 的部分字段
|
86
|
+
"""
|
87
|
+
if logger is not None and not isinstance(logger, logging.Logger):
|
88
|
+
raise TypeError(f"logger 参数必须为 logging.Logger 实例或 None,当前类型为: {type(logger)}")
|
8
89
|
def decorator(func):
|
90
|
+
is_async = asyncio.iscoroutinefunction(func)
|
9
91
|
@wraps(func)
|
10
|
-
def
|
92
|
+
async def async_wrapper(*args, **kwargs):
|
93
|
+
try:
|
94
|
+
return await func(*args, **kwargs)
|
95
|
+
except Exception as e:
|
96
|
+
stack_summary = _ErrorHandlerHelper.get_stack_summary()
|
97
|
+
error_info = _ErrorHandlerHelper.build_error_info(func, e, args, kwargs, stack_summary)
|
98
|
+
error_info = _ErrorHandlerHelper.filter_fields(error_info, log_fields)
|
99
|
+
use_logger = logger if logger is not None else _ErrorHandlerHelper.get_default_logger()
|
100
|
+
if use_logger:
|
101
|
+
if logger is None:
|
102
|
+
use_logger.error(f"执行失败\n详细信息: {json.dumps(error_info, ensure_ascii=False, indent=2)}")
|
103
|
+
else:
|
104
|
+
use_logger.error("执行失败", {'details': error_info})
|
105
|
+
if on_exception:
|
106
|
+
try:
|
107
|
+
on_exception(error_info)
|
108
|
+
except Exception:
|
109
|
+
pass
|
110
|
+
return default_return
|
111
|
+
@wraps(func)
|
112
|
+
def sync_wrapper(*args, **kwargs):
|
11
113
|
try:
|
12
114
|
return func(*args, **kwargs)
|
13
115
|
except Exception as e:
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
else
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
'异常文件': traceback.extract_tb(sys.exc_info()[2])[-1].filename if sys.exc_info()[2] else None,
|
31
|
-
'堆栈': stack_summary,
|
32
|
-
}
|
33
|
-
if logger:
|
34
|
-
logger.error(f"执行失败", {'details': error_info})
|
35
|
-
|
36
|
-
# print(error_info)
|
37
|
-
# raise # 重新抛出异常
|
38
|
-
|
39
|
-
return None # 或者返回其他默认值
|
40
|
-
|
41
|
-
return wrapper
|
116
|
+
stack_summary = _ErrorHandlerHelper.get_stack_summary()
|
117
|
+
error_info = _ErrorHandlerHelper.build_error_info(func, e, args, kwargs, stack_summary)
|
118
|
+
error_info = _ErrorHandlerHelper.filter_fields(error_info, log_fields)
|
119
|
+
use_logger = logger if logger is not None else _ErrorHandlerHelper.get_default_logger()
|
120
|
+
if use_logger:
|
121
|
+
if logger is None:
|
122
|
+
use_logger.error(f"执行失败\n详细信息: {json.dumps(error_info, ensure_ascii=False, indent=2)}")
|
123
|
+
else:
|
124
|
+
use_logger.error("执行失败", {'details': error_info})
|
125
|
+
if on_exception:
|
126
|
+
try:
|
127
|
+
on_exception(error_info)
|
128
|
+
except Exception:
|
129
|
+
pass
|
130
|
+
return default_return
|
131
|
+
return async_wrapper if is_async else sync_wrapper
|
42
132
|
return decorator
|
43
133
|
|
44
134
|
|
45
|
-
def log_on_exception_with_retry(
|
135
|
+
def log_on_exception_with_retry(
|
136
|
+
max_retries=3,
|
137
|
+
delay=1,
|
138
|
+
logger=None,
|
139
|
+
*,
|
140
|
+
on_exception: Optional[Callable[[dict], None]] = None,
|
141
|
+
default_return: Any = None,
|
142
|
+
log_fields: Optional[List[str]] = None,
|
143
|
+
):
|
144
|
+
"""
|
145
|
+
:param logger: 日志对象
|
146
|
+
:param on_exception: 异常回调,参数为 error_info 字典
|
147
|
+
:param default_return: 异常时返回的默认值
|
148
|
+
:param log_fields: 只记录 error_info 的部分字段
|
149
|
+
"""
|
150
|
+
if logger is not None and not isinstance(logger, logging.Logger):
|
151
|
+
raise TypeError(f"logger 参数必须为 logging.Logger 实例或 None,当前类型为: {type(logger)}")
|
46
152
|
def decorator(func):
|
153
|
+
is_async = asyncio.iscoroutinefunction(func)
|
47
154
|
@wraps(func)
|
48
|
-
def
|
155
|
+
async def async_wrapper(*args, **kwargs):
|
49
156
|
last_exception = None
|
50
|
-
|
157
|
+
import time
|
158
|
+
for attempt in range(max_retries):
|
159
|
+
try:
|
160
|
+
return await func(*args, **kwargs)
|
161
|
+
except Exception as e:
|
162
|
+
last_exception = e
|
163
|
+
error_info = {
|
164
|
+
'函数': func.__name__,
|
165
|
+
'重试': attempt + 1,
|
166
|
+
'最大重试': max_retries,
|
167
|
+
'类型': type(e).__name__,
|
168
|
+
'消息': str(e),
|
169
|
+
}
|
170
|
+
error_info = _ErrorHandlerHelper.filter_fields(error_info, log_fields)
|
171
|
+
use_logger = logger if logger is not None else _ErrorHandlerHelper.get_default_logger()
|
172
|
+
if use_logger:
|
173
|
+
if logger is None:
|
174
|
+
use_logger.warning(f"函数 {func.__name__} 第 {attempt + 1} 次尝试失败\n详细信息: {json.dumps(error_info, ensure_ascii=False, indent=2)}")
|
175
|
+
else:
|
176
|
+
use_logger.warning(f"函数 {func.__name__} 第 {attempt + 1} 次尝试失败", {'details': error_info})
|
177
|
+
if on_exception:
|
178
|
+
try:
|
179
|
+
on_exception(error_info)
|
180
|
+
except Exception:
|
181
|
+
pass
|
182
|
+
if attempt < max_retries - 1:
|
183
|
+
await asyncio.sleep(delay)
|
184
|
+
if use_logger:
|
185
|
+
use_logger.info(f"第 {attempt + 1} 次尝试失败,{delay}秒后重试...")
|
186
|
+
else:
|
187
|
+
if use_logger:
|
188
|
+
if logger is None:
|
189
|
+
use_logger.error(f"函数 {func.__name__} 在 {max_retries} 次尝试后仍然失败\n详细信息: {json.dumps(error_info, ensure_ascii=False, indent=2)}")
|
190
|
+
else:
|
191
|
+
use_logger.error(f"函数 {func.__name__} 在 {max_retries} 次尝试后仍然失败", {'details': error_info})
|
192
|
+
final_error_info = _ErrorHandlerHelper.build_final_error_info(func, last_exception, max_retries)
|
193
|
+
final_error_info = _ErrorHandlerHelper.filter_fields(final_error_info, log_fields)
|
194
|
+
if use_logger:
|
195
|
+
if logger is None:
|
196
|
+
use_logger.error(f"最终执行失败\n详细信息: {json.dumps(final_error_info, ensure_ascii=False, indent=2)}")
|
197
|
+
else:
|
198
|
+
use_logger.error("最终执行失败", {'details': final_error_info})
|
199
|
+
if on_exception:
|
200
|
+
try:
|
201
|
+
on_exception(final_error_info)
|
202
|
+
except Exception:
|
203
|
+
pass
|
204
|
+
return default_return
|
205
|
+
@wraps(func)
|
206
|
+
def sync_wrapper(*args, **kwargs):
|
207
|
+
last_exception = None
|
208
|
+
import time
|
51
209
|
for attempt in range(max_retries):
|
52
210
|
try:
|
53
211
|
return func(*args, **kwargs)
|
54
212
|
except Exception as e:
|
55
|
-
import time
|
56
213
|
last_exception = e
|
57
214
|
error_info = {
|
58
215
|
'函数': func.__name__,
|
@@ -61,59 +218,50 @@ def log_on_exception_with_retry(max_retries=3, delay=1, logger=None):
|
|
61
218
|
'类型': type(e).__name__,
|
62
219
|
'消息': str(e),
|
63
220
|
}
|
64
|
-
|
65
|
-
if logger
|
66
|
-
|
67
|
-
|
221
|
+
error_info = _ErrorHandlerHelper.filter_fields(error_info, log_fields)
|
222
|
+
use_logger = logger if logger is not None else _ErrorHandlerHelper.get_default_logger()
|
223
|
+
if use_logger:
|
224
|
+
if logger is None:
|
225
|
+
use_logger.warning(f"函数 {func.__name__} 第 {attempt + 1} 次尝试失败\n详细信息: {json.dumps(error_info, ensure_ascii=False, indent=2)}")
|
226
|
+
else:
|
227
|
+
use_logger.warning(f"函数 {func.__name__} 第 {attempt + 1} 次尝试失败", {'details': error_info})
|
228
|
+
if on_exception:
|
229
|
+
try:
|
230
|
+
on_exception(error_info)
|
231
|
+
except Exception:
|
232
|
+
pass
|
68
233
|
if attempt < max_retries - 1:
|
69
234
|
time.sleep(delay)
|
70
|
-
if
|
71
|
-
|
235
|
+
if use_logger:
|
236
|
+
use_logger.info(f"第 {attempt + 1} 次尝试失败,{delay}秒后重试...")
|
72
237
|
else:
|
73
|
-
if
|
74
|
-
logger
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
238
|
+
if use_logger:
|
239
|
+
if logger is None:
|
240
|
+
use_logger.error(f"函数 {func.__name__} 在 {max_retries} 次尝试后仍然失败\n详细信息: {json.dumps(error_info, ensure_ascii=False, indent=2)}")
|
241
|
+
else:
|
242
|
+
use_logger.error(f"函数 {func.__name__} 在 {max_retries} 次尝试后仍然失败", {'details': error_info})
|
243
|
+
final_error_info = _ErrorHandlerHelper.build_final_error_info(func, last_exception, max_retries)
|
244
|
+
final_error_info = _ErrorHandlerHelper.filter_fields(final_error_info, log_fields)
|
245
|
+
if use_logger:
|
246
|
+
if logger is None:
|
247
|
+
use_logger.error(f"最终执行失败\n详细信息: {json.dumps(final_error_info, ensure_ascii=False, indent=2)}")
|
248
|
+
else:
|
249
|
+
use_logger.error("最终执行失败", {'details': final_error_info})
|
250
|
+
if on_exception:
|
251
|
+
try:
|
252
|
+
on_exception(final_error_info)
|
253
|
+
except Exception:
|
254
|
+
pass
|
255
|
+
return default_return
|
256
|
+
return async_wrapper if is_async else sync_wrapper
|
90
257
|
return decorator
|
91
258
|
|
92
259
|
|
93
260
|
if __name__ == "__main__":
|
94
|
-
|
95
|
-
test_logger = logging.getLogger(__name__)
|
96
|
-
test_logger.setLevel(logging.INFO)
|
97
|
-
|
98
|
-
@log_on_exception(logger=test_logger)
|
261
|
+
@log_on_exception(logger=None)
|
99
262
|
def divide_numbers(a, b):
|
100
263
|
"""测试函数:除法运算"""
|
101
264
|
return a / b
|
102
|
-
|
103
|
-
@log_on_exception_with_retry(max_retries=2, delay=0.5, logger=test_logger)
|
104
|
-
def fetch_data(url):
|
105
|
-
import random
|
106
|
-
"""测试函数:模拟数据获取"""
|
107
|
-
if random.random() < 0.7: # 70% 概率失败
|
108
|
-
raise ConnectionError("网络连接失败")
|
109
|
-
return "数据获取成功"
|
110
|
-
|
111
|
-
# 测试基本错误处理
|
112
|
-
print("=== 测试基本错误处理 ===")
|
265
|
+
|
113
266
|
result1 = divide_numbers(10, 0)
|
114
|
-
|
115
|
-
print(f"结果: {result2}")
|
116
|
-
|
117
|
-
print("\n=== 测试重试机制 ===")
|
118
|
-
result3 = fetch_data("http://example.com")
|
119
|
-
print(f"最终结果: {result3}")
|
267
|
+
|
@@ -1,9 +1,9 @@
|
|
1
1
|
mdbq/__init__.py,sha256=Il5Q9ATdX8yXqVxtP_nYqUhExzxPC_qk_WXQ_4h0exg,16
|
2
|
-
mdbq/__version__.py,sha256=
|
2
|
+
mdbq/__version__.py,sha256=zkcTzC4btf-Gh2O0sS9u1Pl3m_UGMa4rkyfG91EfdD0,18
|
3
3
|
mdbq/aggregation/__init__.py,sha256=EeDqX2Aml6SPx8363J-v1lz0EcZtgwIBYyCJV6CcEDU,40
|
4
4
|
mdbq/aggregation/query_data.py,sha256=WtTFMN78jn43Y-nBTPAXhAK56w3wDuv_cj4YtzzGbZk,169797
|
5
5
|
mdbq/log/__init__.py,sha256=Mpbrav0s0ifLL7lVDAuePEi1hJKiSHhxcv1byBKDl5E,15
|
6
|
-
mdbq/log/mylogger.py,sha256=
|
6
|
+
mdbq/log/mylogger.py,sha256=iDhWkTY6I9T3IJuERWqiXKq1sNf0VuraSEq33ZxLqdw,24930
|
7
7
|
mdbq/myconf/__init__.py,sha256=jso1oHcy6cJEfa7udS_9uO5X6kZLoPBF8l3wCYmr5dM,18
|
8
8
|
mdbq/myconf/myconf.py,sha256=rHvQCnQRKhQ49AZBke-Z4v28hyOLmHt4MylIuB0H6yA,33516
|
9
9
|
mdbq/mysql/__init__.py,sha256=A_DPJyAoEvTSFojiI2e94zP0FKtCkkwKP1kYUCSyQzo,11
|
@@ -14,7 +14,7 @@ mdbq/mysql/unique_.py,sha256=MaztT-WIyEQUs-OOYY4pFulgHVcXR1BfCy3QUz0XM_U,21127
|
|
14
14
|
mdbq/mysql/uploader.py,sha256=SVlrLxoYBEpTu_I771wAehJQVFWOCqXp-lNk2JNYFOE,81881
|
15
15
|
mdbq/other/__init__.py,sha256=jso1oHcy6cJEfa7udS_9uO5X6kZLoPBF8l3wCYmr5dM,18
|
16
16
|
mdbq/other/download_sku_picture.py,sha256=X66sVdvVgzoNzmgVJyPtd7bjEvctEKtLPblEPF65EWc,46940
|
17
|
-
mdbq/other/error_handler.py,sha256=
|
17
|
+
mdbq/other/error_handler.py,sha256=XiygzLiOKy-pYE4xcMbF0cEFxKorHHAhSeVZIDbQvhY,12313
|
18
18
|
mdbq/other/otk.py,sha256=iclBIFbQbhlqzUbcMMoePXBpcP1eZ06ZtjnhcA_EbmE,7241
|
19
19
|
mdbq/other/pov_city.py,sha256=AEOmCOzOwyjHi9LLZWPKi6DUuSC-_M163664I52u9qw,21050
|
20
20
|
mdbq/other/ua_sj.py,sha256=JuVYzc_5QZ9s_oQSrTHVKkQv4S_7-CWx4oIKOARn_9U,22178
|
@@ -25,7 +25,7 @@ mdbq/redis/__init__.py,sha256=YtgBlVSMDphtpwYX248wGge1x-Ex_mMufz4-8W0XRmA,12
|
|
25
25
|
mdbq/redis/getredis.py,sha256=vpBuNc22uj9Vr-_Dh25_wpwWM1e-072EAAIBdB_IpL0,23494
|
26
26
|
mdbq/spider/__init__.py,sha256=RBMFXGy_jd1HXZhngB2T2XTvJqki8P_Fr-pBcwijnew,18
|
27
27
|
mdbq/spider/aikucun.py,sha256=XptHjGzbout9IYzWAOQUpMMV5qEgLTU8pL1ZGt8oNEA,21868
|
28
|
-
mdbq-4.0.
|
29
|
-
mdbq-4.0.
|
30
|
-
mdbq-4.0.
|
31
|
-
mdbq-4.0.
|
28
|
+
mdbq-4.0.36.dist-info/METADATA,sha256=eyN-znN-wNE2_i3A5WsrJI7WnKXN6kUenvnQH2kqBHU,364
|
29
|
+
mdbq-4.0.36.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
30
|
+
mdbq-4.0.36.dist-info/top_level.txt,sha256=2FQ-uLnCSB-OwFiWntzmwosW3X2Xqsg0ewh1axsaylA,5
|
31
|
+
mdbq-4.0.36.dist-info/RECORD,,
|
File without changes
|
File without changes
|