py.lib
py.lib/log/log_confile.py
. Наведение порядка в коде логирования
1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/log/log_confile.py Sat Jan 28 16:39:53 2023 +0300 1.3 @@ -0,0 +1,246 @@ 1.4 +# coding: utf-8 1.5 +""" Логирование на консоль 1.6 + 1.7 +Метки в журнале о уровне сообщения: 1.8 + "`": Debug 1.9 + ".": Info 1.10 + "*": Warning 1.11 + "!": Error 1.12 + "#": Alert 1.13 + 1.14 +""" 1.15 + 1.16 +from time import monotonic, ctime 1.17 +from datetime import timedelta, date 1.18 +from traceback import extract_tb, extract_stack 1.19 +from sys import exc_info, stderr, stdout 1.20 +from typing import Optional, TextIO, Any 1.21 +from os.path import join as p_join, abspath 1.22 +from threading import RLock 1.23 + 1.24 + 1.25 +TIME_TO_FLUSH: int = 120 # Время по умолчанию с момента прошлого сброса лога, 1.26 + # после которого, выполняется принудительный сброс буферов 1.27 + 1.28 + 1.29 +class Timing(object): 1.30 + def __init__(self, name: Optional[str] = None): 1.31 + if name is None: 1.32 + self.prefix = '' 1.33 + 1.34 + else: 1.35 + self.prefix = f'{name} :: ' 1.36 + 1.37 + self.tsAll = monotonic() 1.38 + self.ts = self.tsAll 1.39 + 1.40 + def get_time(self): 1.41 + return monotonic() - self.ts 1.42 + 1.43 + def reset(self): 1.44 + self.ts = monotonic() 1.45 + self.tsAll = self.ts 1.46 + 1.47 + def __str__(self): 1.48 + ts = monotonic() 1.49 + return self.prefix + '%s(%.4f)' % (timedelta(seconds=(ts - self.tsAll)), ts - self.ts) 1.50 + 1.51 + def __call__(self, msg): 1.52 + _buf = f'{self} | {msg}' 1.53 + self.ts = monotonic() 1.54 + return _buf 1.55 + 1.56 + 1.57 +class NullLog(object): 1.58 + def __init__(self, prefix: str = 'main'): 1.59 + self.prefix = prefix 1.60 + 1.61 + @staticmethod 1.62 + def _write(mark: str, msg: Any): 1.63 + pass # cat > /dev/null 1.64 + 1.65 + def __call__(self, msg): 1.66 + self._write('.', msg) 1.67 + 1.68 + def err(self, msg): 1.69 + self._write('!', msg) 1.70 + 1.71 + def warn(self, msg): 1.72 + self._write('*', msg) 1.73 + 1.74 + def alert(self, msg): 1.75 + self._write('#', msg) 1.76 + 1.77 + def debug(self, msg): 1.78 + self._write('`', msg) 1.79 + 1.80 + @staticmethod 1.81 + def get_timing(name: Optional[str] = None): 1.82 + return Timing(name) 1.83 + 1.84 + def sub_log(self, name: str): 1.85 + return self.__class__(f'{self.prefix}/{name}') 1.86 + 1.87 + def excpt(self, msg, e_class=None, e_obj=None, e_tb=None, stack_skip=0): 1.88 + if e_class is None: 1.89 + e_class, e_obj, e_tb = exc_info() 1.90 + 1.91 + tb_data_tb = list(extract_tb(e_tb))[::-1] 1.92 + tb_data_stack = list(extract_stack())[::-1][(2 + stack_skip):] 1.93 + self.err(msg) 1.94 + self.err('--- EXCEPTION ---') 1.95 + self.err(f' {e_class.__name__} ({e_obj})') 1.96 + self.err('--- TRACEBACK ---') 1.97 + for _tb_file, _tb_line, _tb_func, _tb_text in tb_data_tb: 1.98 + self.err(f'File: {_tb_file}, line {_tb_line} in {_tb_func}') 1.99 + self.err(f' {_tb_text}') 1.100 + 1.101 + self.err('>>> Exception Handler <<<') 1.102 + for _tb_file, _tb_line, _tb_func, _tb_text in tb_data_stack: 1.103 + self.err(f'File: {_tb_file}, line {_tb_line} in {_tb_func}') 1.104 + self.err(f' {_tb_text}') 1.105 + 1.106 + self.err('--- END EXCEPTION ---') 1.107 + 1.108 + 1.109 +class FileLog(NullLog): 1.110 + @staticmethod 1.111 + def _open_file(file_name: str): 1.112 + return open(file_name, 'a', encoding='utf-8') 1.113 + 1.114 + def __init__(self, prefix: str = 'main', 1.115 + file_name: Optional[str] = None, 1.116 + file_obj: Optional[TextIO] = None, 1.117 + time_to_flush: int = TIME_TO_FLUSH 1.118 + ): 1.119 + 1.120 + super().__init__(prefix=prefix) 1.121 + self.fd: Optional[TextIO] = None 1.122 + 1.123 + self.flush_time = monotonic() 1.124 + self.time_to_flush = time_to_flush # Время с момента прошлого сброса лога, 1.125 + # после которого, выполняется принудительный сброс буферов 1.126 + 1.127 + if file_name is not None: 1.128 + self.fd = self._open_file(file_name) 1.129 + 1.130 + else: 1.131 + self.fd = file_obj 1.132 + 1.133 + if self.fd is None: 1.134 + raise ValueError(f'Не задан файл для записи журналов') 1.135 + 1.136 + def flush(self, time_mark: float = None): 1.137 + if time_mark is None: 1.138 + time_mark = monotonic() 1.139 + 1.140 + self.flush_time = time_mark 1.141 + self.fd.flush() 1.142 + 1.143 + def close(self): 1.144 + self.fd.flush() 1.145 + self.fd.close() 1.146 + self.fd = None 1.147 + 1.148 + def __del__(self): 1.149 + if self.fd is not None: 1.150 + self.fd.flush() 1.151 + self.fd.close() 1.152 + self.fd = None 1.153 + 1.154 + def flush_event(self): 1.155 + t = monotonic() 1.156 + if t - self.flush_time >= self.time_to_flush: 1.157 + self.flush(t) 1.158 + 1.159 + def _write(self, mark: str, msg: Any): 1.160 + if self.fd is None: 1.161 + raise ValueError('Попытка использовать закрытый файл журнала') 1.162 + 1.163 + t = ctime() 1.164 + for l in str(msg).splitlines(): 1.165 + self.fd.write(f'{t} | {mark} {self.prefix} | {l}') 1.166 + 1.167 + self.flush_event() 1.168 + 1.169 + def sub_log(self, name: str): 1.170 + if self.fd is None: 1.171 + raise ValueError('Попытка использовать закрытый файл журнала') 1.172 + 1.173 + return self.__class__(f'{self.prefix}/{name}', file_obj=self.fd, time_to_flush=self.time_to_flush) 1.174 + 1.175 + 1.176 +class StderrLog(FileLog): 1.177 + def __init__(self, prefix: str = 'main'): 1.178 + super().__init__(prefix, file_obj=stderr) 1.179 + 1.180 + def flush_event(self): 1.181 + pass # нет необходимости сбрасывать буферы в консоли 1.182 + 1.183 + 1.184 +class StdoutLog(FileLog): 1.185 + def __init__(self, prefix: str = 'main'): 1.186 + super().__init__(prefix, file_obj=stdout) 1.187 + 1.188 + def flush_event(self): 1.189 + pass # нет необходимости сбрасывать буферы в консоли 1.190 + 1.191 + 1.192 +class LogrotateFile(FileLog): 1.193 + def __init__(self, directory: str = '.', prefix: str = 'main', time_to_flush: int = TIME_TO_FLUSH): 1.194 + d = date.today() 1.195 + directory = abspath(directory) 1.196 + file_name = p_join(directory, f'{prefix}-{d}.log') 1.197 + 1.198 + super().__init__(prefix, file_name=file_name, time_to_flush=time_to_flush) 1.199 + self.logrotate_base = p_join(directory, f'{prefix}') 1.200 + self.logrotate_date = d 1.201 + 1.202 + def flush(self, time_mark: float = None, no_rotate: bool = False): 1.203 + if not no_rotate: 1.204 + d = date.today() 1.205 + if self.logrotate_date != d: 1.206 + self.logrotate_rotate() 1.207 + return 1.208 + 1.209 + super().flush(time_mark=time_mark) 1.210 + 1.211 + def logrotate_rotate(self): 1.212 + d = date.today() 1.213 + file_name = f'{self.logrotate_base}-{d}.log' 1.214 + self.fd.flush() 1.215 + self.fd = self._open_file(file_name) 1.216 + self.logrotate_date = d 1.217 + self.flush(no_rotate=True) 1.218 + 1.219 + 1.220 +class ThreadSafeFileLog(FileLog): 1.221 + def __int__(self, *a, **kwa): 1.222 + super().__init__(*a, **kwa) 1.223 + self.thread_safe_lock = RLock() 1.224 + 1.225 + def _write(self, mark: str, msg: Any): 1.226 + with self.thread_safe_lock: 1.227 + super()._write(mark, msg) 1.228 + 1.229 + def flush(self, time_mark: float = None): 1.230 + with self.thread_safe_lock: 1.231 + super().flush(time_mark) 1.232 + 1.233 + 1.234 +class ThreadSafeLogrotateFile(LogrotateFile): 1.235 + def __init__(self, directory: str = '.', prefix: str = 'main', time_to_flush: int = TIME_TO_FLUSH): 1.236 + super().__init__(directory=directory, prefix=prefix, time_to_flush=time_to_flush) 1.237 + self.thread_safe_lock = RLock() 1.238 + 1.239 + def _write(self, mark: str, msg: Any): 1.240 + with self.thread_safe_lock: 1.241 + super()._write(mark, msg) 1.242 + 1.243 + def flush(self, time_mark: float = None, no_rotate: bool = False): 1.244 + with self.thread_safe_lock: 1.245 + super().flush(time_mark=time_mark, no_rotate=no_rotate) 1.246 + 1.247 + def logrotate_rotate(self): 1.248 + with self.thread_safe_lock: 1.249 + super().logrotate_rotate()