unitlog 0.0.3__tar.gz → 0.0.4__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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: unitlog
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Home-page: https://github.com/yujun2647/unitlog
5
5
  Download-URL:
6
6
  Author: walkerjun
@@ -9,6 +9,14 @@ License: Apache-2.0
9
9
  Requires-Python: >=3.6
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: home-page
17
+ Dynamic: license
18
+ Dynamic: license-file
19
+ Dynamic: requires-python
12
20
 
13
21
  # unitlog
14
22
 
@@ -0,0 +1 @@
1
+ __version__ = "0.0.4"
@@ -0,0 +1,235 @@
1
+ import os
2
+ import sys
3
+ import atexit
4
+ import logging
5
+ import traceback
6
+ from queue import Empty
7
+ import multiprocessing as mp
8
+ from multiprocessing.synchronize import Event
9
+
10
+ from unitlog.handlers import (LogBox, UnitHandler, UnitFileHandler,
11
+ UnitConsoleHandler)
12
+
13
+
14
+ class PoxyConsoleLogWriter(object):
15
+
16
+ def __init__(self, stream=sys.stdout):
17
+ self.stream = stream
18
+
19
+ def emit(self, log_msg):
20
+ self.stream.write(log_msg)
21
+ self.stream.flush()
22
+
23
+ def close(self):
24
+ self.stream.close()
25
+
26
+
27
+ class PoxyFileLogWriter(PoxyConsoleLogWriter):
28
+ def __init__(self, log_filepath, file_mode="a"):
29
+ super().__init__(stream=open(log_filepath, file_mode))
30
+
31
+
32
+ class UnitLog(object):
33
+
34
+ def __init__(self):
35
+ self.started: Event = mp.Event()
36
+ self.stopped: Event = mp.Event()
37
+ self.log_num = mp.Value('i', 0)
38
+ self.worker = mp.Process(target=self.listening_log_msg, daemon=True)
39
+ self._proxy_handler_map = {}
40
+
41
+ def _init_proxy_handler(self, log_box: LogBox) -> PoxyConsoleLogWriter:
42
+ hkey = f"{log_box.log_type}-{log_box.log_filepath}"
43
+ if hkey not in self._proxy_handler_map:
44
+ if log_box.log_type == "console":
45
+ self._proxy_handler_map[hkey] = PoxyConsoleLogWriter()
46
+ elif log_box.log_type == "file":
47
+ abs_log_filepath = os.path.abspath(log_box.log_filepath)
48
+ dir_path = os.path.dirname(abs_log_filepath)
49
+ if not os.path.exists(dir_path):
50
+ os.makedirs(dir_path, exist_ok=True)
51
+
52
+ self._proxy_handler_map[hkey] = PoxyFileLogWriter(
53
+ log_filepath=abs_log_filepath,
54
+ file_mode=log_box.file_mode
55
+ )
56
+ else:
57
+ raise TypeError(f"Unsupported log type: {log_box.log_type}")
58
+ return self._proxy_handler_map[hkey]
59
+
60
+ def listening_log_msg(self):
61
+ while True:
62
+ self.started.set()
63
+ try:
64
+ log_box: LogBox = UnitHandler.DEFAULT_LOG_QUEUE.get(timeout=0.1)
65
+ except Empty:
66
+ if self.stopped.is_set():
67
+ break
68
+ continue
69
+ except KeyboardInterrupt:
70
+ continue
71
+ try:
72
+ handler = self._init_proxy_handler(log_box)
73
+
74
+ handler.emit(log_box.log_msg)
75
+ if os.environ.get("ENV-TEST", "prod") == "test":
76
+ self.log_num.value += 1
77
+ except Exception as e:
78
+ print(f"unexpect exception: {e}\n "
79
+ f"{traceback.format_exc()}")
80
+ if os.environ.get("ENV-TEST", "prod") == "test":
81
+ print(f"all log num: {self.log_num.value}")
82
+
83
+ def register_logger(self, name, level=logging.INFO,
84
+ console_log=True, file_log=False, file_log_mode="a",
85
+ log_filepath=None,
86
+ parent_logger_name=None,
87
+ force_all_console_log_to_file=False) -> logging.Logger:
88
+ if not self.started.is_set():
89
+ self.worker.start()
90
+ if not self.started.wait(timeout=3):
91
+ raise ValueError("unit log process is not started")
92
+
93
+ logger = logging.getLogger(name)
94
+ logger.setLevel(level)
95
+ if parent_logger_name is not None:
96
+ parent_logger = logging.getLogger(parent_logger_name)
97
+ logger.parent = parent_logger
98
+ logger.propagate = True
99
+ else:
100
+ logger.propagate = False
101
+
102
+ simple_formatter = logging.Formatter(
103
+ fmt="%(asctime)s [line:%(lineno)d] %(levelname)s %(message)s",
104
+ datefmt="%a, %d %b %Y %H:%M:%S"
105
+ )
106
+ full_formatter = logging.Formatter(
107
+ fmt="%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s "
108
+ "%(message)s",
109
+ datefmt="%a, %d %b %Y %H:%M:%S"
110
+ )
111
+ if console_log:
112
+ console_handler = UnitConsoleHandler()
113
+ console_handler.setFormatter(simple_formatter)
114
+ logger.handlers.append(console_handler)
115
+ if file_log:
116
+ assert log_filepath, "log_filepath must be set"
117
+ file_handler = UnitFileHandler(log_filepath, mode=file_log_mode)
118
+ file_handler.setFormatter(full_formatter)
119
+ logger.handlers.append(file_handler)
120
+ logger.info("\nLog_filename: {}".format(log_filepath))
121
+
122
+ if force_all_console_log_to_file: # 强制控制所有标准输出到 文件
123
+ self.force_all_console_log_to_file(log_filepath)
124
+
125
+ return logger
126
+
127
+
128
+ @classmethod
129
+ def force_all_console_log_to_file(cls, log_filepath):
130
+ # ==========================================
131
+ # 新增函数:重定向底层 C/C++ 输出
132
+ # ==========================================
133
+ def _redirect_c_libraries_output(log_path):
134
+ """
135
+ 使用 os.dup2 强制将底层 C/C++ 的 stdout/stderr 重定向到日志文件。
136
+ 解决 sherpa-onnx, PyQt, OpenCV 等 C 库打印无法被 Python 捕获的问题。
137
+ """
138
+ # 1. 打开日志文件 (使用 append 模式)
139
+ # 这里的 buffer 设置为 0 (unbuffered) 或者 line buffered,确保 C 代码崩溃前能写入
140
+ try:
141
+ # 打开文件获取文件描述符
142
+ # distinct file object specifically for low-level redirection
143
+ log_file = open(log_path, 'a+')
144
+ log_fd = log_file.fileno()
145
+
146
+ # 2. 刷新 Python 的缓冲区,防止重定向导致之前的日志丢失
147
+ sys.stdout.flush()
148
+ sys.stderr.flush()
149
+
150
+ # 3. 核心:重定向 FD 1 (stdout) 和 FD 2 (stderr)
151
+ # 这一步之后,所有的 C printf/std::cout 都会直接写进文件
152
+ os.dup2(log_fd, 1)
153
+ os.dup2(log_fd, 2)
154
+
155
+ # 保持 log_file 对象引用,防止被垃圾回收导致 FD 关闭
156
+ return log_file
157
+ except Exception as e:
158
+ print(f"Failed to redirect C logs: {e}")
159
+ return None
160
+
161
+ class Logger(object):
162
+ def __init__(self, filename):
163
+ self.terminal = sys.stdout # 记录原来的控制台,防止 IDE 里看不到了
164
+ self.log = open(filename, "a", encoding="utf-8") # 'a' 追加模式
165
+
166
+ def write(self, message):
167
+ # 1. 尝试写回控制台(方便开发调试)
168
+ try:
169
+ if self.terminal:
170
+ self.terminal.write(message)
171
+ except:
172
+ pass # 打包成 no console 后这里可能会报错,直接忽略
173
+
174
+ # 2. 写入文件
175
+ try:
176
+ self.log.write(message)
177
+ # 【关键】立即刷新缓冲区,否则崩溃瞬间可能来不及写入文件
178
+ self.log.flush()
179
+ except:
180
+ pass
181
+
182
+ def flush(self):
183
+ # 兼容性函数,必须保留
184
+ try:
185
+ if self.terminal:
186
+ self.terminal.flush()
187
+ self.log.flush()
188
+ except:
189
+ pass
190
+
191
+ # 3. 【关键改动】区分环境进行重定向
192
+ # 判断是否是打包后的环境
193
+ # 逻辑:如果是 PyInstaller 打包 (frozen) 或者 环境变量 MIT_LOG=1,都视为需要重定向
194
+ FORCE_ALL_CONSOLE_LOG_TO_FILE = os.environ.get("FORCE_ALL_CONSOLE_LOG_TO_FILE") == "1"
195
+ IS_FROZEN = getattr(sys, 'frozen', False) or FORCE_ALL_CONSOLE_LOG_TO_FILE
196
+ print(f"FORCE_ALL_CONSOLE_LOG_TO_FILE: {FORCE_ALL_CONSOLE_LOG_TO_FILE} \t IS_FROZEN: {IS_FROZEN}")
197
+
198
+ if IS_FROZEN:
199
+ # --- 打包环境 (Exe) ---
200
+ # 1. 接管 Python 层面 (你原来的做法)
201
+ sys.stdout = Logger(log_filepath)
202
+ sys.stderr = sys.stdout
203
+
204
+ # 2. 接管 C/C++ 层面 (新增的做法)
205
+ # 这会让 sherpa-onnx 的报错也进文件
206
+ # 注意:在 Linux/Mac 上非常有效,Windows 上通常也有效
207
+ _c_log_ref = _redirect_c_libraries_output(log_filepath)
208
+
209
+ print(f"Native C++ stdout/stderr redirected to {log_filepath}")
210
+
211
+ else:
212
+ # --- 开发环境 (IDE) ---
213
+ # 在开发时,我们通常不希望 C++ 输出消失在控制台
214
+ # 所以这里我们 *只* 使用你原来的 Logger 记录 Python print
215
+ # 这样 IDE 控制台里既能看到 Python print,也能看到 C++ print (IDE 会自己捕获 FD)
216
+ # 同时 Python print 也会被写入文件
217
+
218
+ # 如果你确实希望开发时文件里也有 sherpa-onnx 的日志,
219
+ # 你可以把上面的 redirect_c_libraries_output 打开,
220
+ # 但代价是你的 PyCharm 控制台里那行红色的警告会消失。
221
+ sys.stdout = Logger(log_filepath)
222
+ sys.stderr = sys.stdout
223
+
224
+ DEFAULT_LOG = UnitLog()
225
+
226
+ register_logger: UnitLog.register_logger = DEFAULT_LOG.register_logger
227
+ atexit.register(lambda: DEFAULT_LOG.stopped.set())
228
+
229
+ if __name__ == "__main__":
230
+ import time
231
+
232
+ _logger = logging.getLogger("test")
233
+ register_logger(name="test", level=logging.DEBUG,
234
+ log_filepath="./temp/test.log")
235
+ _logger.info("lllll")
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: unitlog
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Home-page: https://github.com/yujun2647/unitlog
5
5
  Download-URL:
6
6
  Author: walkerjun
@@ -9,6 +9,14 @@ License: Apache-2.0
9
9
  Requires-Python: >=3.6
10
10
  Description-Content-Type: text/markdown
11
11
  License-File: LICENSE
12
+ Dynamic: author
13
+ Dynamic: author-email
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: home-page
17
+ Dynamic: license
18
+ Dynamic: license-file
19
+ Dynamic: requires-python
12
20
 
13
21
  # unitlog
14
22
 
@@ -1 +0,0 @@
1
- __version__ = "0.0.3"
@@ -1,135 +0,0 @@
1
- import os
2
- import sys
3
- import atexit
4
- import logging
5
- import traceback
6
- from queue import Empty
7
- import multiprocessing as mp
8
- from multiprocessing.synchronize import Event
9
-
10
- from unitlog.handlers import (LogBox, UnitHandler, UnitFileHandler,
11
- UnitConsoleHandler)
12
-
13
-
14
- class PoxyConsoleLogWriter(object):
15
-
16
- def __init__(self, stream=sys.stdout):
17
- self.stream = stream
18
-
19
- def emit(self, log_msg):
20
- self.stream.write(log_msg)
21
- self.stream.flush()
22
-
23
- def close(self):
24
- self.stream.close()
25
-
26
-
27
- class PoxyFileLogWriter(PoxyConsoleLogWriter):
28
- def __init__(self, log_filepath, file_mode="a"):
29
- super().__init__(stream=open(log_filepath, file_mode))
30
-
31
-
32
- class UnitLog(object):
33
-
34
- def __init__(self):
35
- self.started: Event = mp.Event()
36
- self.stopped: Event = mp.Event()
37
- self.log_num = mp.Value('i', 0)
38
- self.worker = mp.Process(target=self.listening_log_msg, daemon=True)
39
- self._proxy_handler_map = {}
40
-
41
- def _init_proxy_handler(self, log_box: LogBox) -> PoxyConsoleLogWriter:
42
- hkey = f"{log_box.log_type}-{log_box.log_filepath}"
43
- if hkey not in self._proxy_handler_map:
44
- if log_box.log_type == "console":
45
- self._proxy_handler_map[hkey] = PoxyConsoleLogWriter()
46
- elif log_box.log_type == "file":
47
- abs_log_filepath = os.path.abspath(log_box.log_filepath)
48
- dir_path = os.path.dirname(abs_log_filepath)
49
- if not os.path.exists(dir_path):
50
- os.makedirs(dir_path, exist_ok=True)
51
-
52
- self._proxy_handler_map[hkey] = PoxyFileLogWriter(
53
- log_filepath=abs_log_filepath,
54
- file_mode=log_box.file_mode
55
- )
56
- else:
57
- raise TypeError(f"Unsupported log type: {log_box.log_type}")
58
- return self._proxy_handler_map[hkey]
59
-
60
- def listening_log_msg(self):
61
- while True:
62
- self.started.set()
63
- try:
64
- log_box: LogBox = UnitHandler.DEFAULT_LOG_QUEUE.get(timeout=0.1)
65
- except Empty:
66
- if self.stopped.is_set():
67
- break
68
- continue
69
- except KeyboardInterrupt:
70
- continue
71
- try:
72
- handler = self._init_proxy_handler(log_box)
73
-
74
- handler.emit(log_box.log_msg)
75
- if os.environ.get("ENV-TEST", "prod") == "test":
76
- self.log_num.value += 1
77
- except Exception as e:
78
- print(f"unexpect exception: {e}\n "
79
- f"{traceback.format_exc()}")
80
- if os.environ.get("ENV-TEST", "prod") == "test":
81
- print(f"all log num: {self.log_num.value}")
82
-
83
- def register_logger(self, name, level=logging.INFO,
84
- console_log=True, file_log=False, file_log_mode="a",
85
- log_filepath=None,
86
- parent_logger_name=None) -> logging.Logger:
87
- if not self.started.is_set():
88
- self.worker.start()
89
- if not self.started.wait(timeout=3):
90
- raise ValueError("unit log process is not started")
91
-
92
- logger = logging.getLogger(name)
93
- logger.setLevel(level)
94
- if parent_logger_name is not None:
95
- parent_logger = logging.getLogger(parent_logger_name)
96
- logger.parent = parent_logger
97
- logger.propagate = True
98
- else:
99
- logger.propagate = False
100
-
101
- simple_formatter = logging.Formatter(
102
- fmt="%(asctime)s [line:%(lineno)d] %(levelname)s %(message)s",
103
- datefmt="%a, %d %b %Y %H:%M:%S"
104
- )
105
- full_formatter = logging.Formatter(
106
- fmt="%(asctime)s %(filename)s [line:%(lineno)d] %(levelname)s "
107
- "%(message)s",
108
- datefmt="%a, %d %b %Y %H:%M:%S"
109
- )
110
- if console_log:
111
- console_handler = UnitConsoleHandler()
112
- console_handler.setFormatter(simple_formatter)
113
- logger.handlers.append(console_handler)
114
- if file_log:
115
- assert log_filepath, "log_filepath must be set"
116
- file_handler = UnitFileHandler(log_filepath, mode=file_log_mode)
117
- file_handler.setFormatter(full_formatter)
118
- logger.handlers.append(file_handler)
119
- logger.info("\nLog_filename: {}".format(log_filepath))
120
-
121
- return logger
122
-
123
-
124
- DEFAULT_LOG = UnitLog()
125
-
126
- register_logger = DEFAULT_LOG.register_logger
127
- atexit.register(lambda: DEFAULT_LOG.stopped.set())
128
-
129
- if __name__ == "__main__":
130
- import time
131
-
132
- _logger = logging.getLogger("test")
133
- register_logger(name="test", level=logging.DEBUG,
134
- log_filepath="./temp/test.log")
135
- _logger.info("lllll")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes