py.lib
2023-01-30
Parent:6f8bea109183
py.lib/log/log_confile.py
. Убираем лишние форматированные строки * Проблема с созданием вложенных журналов в сложных ситуациях с локами и ротируемым журналом.
| awgur@11 | 1 # coding: utf-8 |
| awgur@11 | 2 """ Логирование на консоль |
| awgur@11 | 3 |
| awgur@11 | 4 Метки в журнале о уровне сообщения: |
| awgur@11 | 5 "`": Debug |
| awgur@11 | 6 ".": Info |
| awgur@11 | 7 "*": Warning |
| awgur@11 | 8 "!": Error |
| awgur@11 | 9 "#": Alert |
| awgur@43 | 10 |
| awgur@11 | 11 """ |
| awgur@11 | 12 |
| awgur@43 | 13 from time import monotonic, ctime |
| awgur@43 | 14 from datetime import timedelta, date |
| awgur@43 | 15 from traceback import extract_tb, extract_stack |
| awgur@11 | 16 from sys import exc_info, stderr, stdout |
| awgur@43 | 17 from typing import Optional, TextIO, Any |
| awgur@44 | 18 from os.path import join as p_join, abspath, split as p_split |
| awgur@43 | 19 from threading import RLock |
| awgur@43 | 20 |
| awgur@43 | 21 |
| awgur@43 | 22 TIME_TO_FLUSH: int = 120 # Время по умолчанию с момента прошлого сброса лога, |
| awgur@43 | 23 # после которого, выполняется принудительный сброс буферов |
| awgur@11 | 24 |
| awgur@11 | 25 |
| awgur@11 | 26 class Timing(object): |
| awgur@43 | 27 def __init__(self, name: Optional[str] = None): |
| awgur@11 | 28 if name is None: |
| awgur@11 | 29 self.prefix = '' |
| awgur@43 | 30 |
| awgur@11 | 31 else: |
| awgur@43 | 32 self.prefix = f'{name} :: ' |
| awgur@43 | 33 |
| awgur@30 | 34 self.tsAll = monotonic() |
| awgur@11 | 35 self.ts = self.tsAll |
| awgur@43 | 36 |
| awgur@43 | 37 def get_time(self): |
| awgur@30 | 38 return monotonic() - self.ts |
| awgur@43 | 39 |
| awgur@11 | 40 def reset(self): |
| awgur@30 | 41 self.ts = monotonic() |
| awgur@11 | 42 self.tsAll = self.ts |
| awgur@43 | 43 |
| awgur@11 | 44 def __str__(self): |
| awgur@30 | 45 ts = monotonic() |
| awgur@11 | 46 return self.prefix + '%s(%.4f)' % (timedelta(seconds=(ts - self.tsAll)), ts - self.ts) |
| awgur@43 | 47 |
| awgur@11 | 48 def __call__(self, msg): |
| awgur@43 | 49 _buf = f'{self} | {msg}' |
| awgur@30 | 50 self.ts = monotonic() |
| awgur@11 | 51 return _buf |
| awgur@11 | 52 |
| awgur@11 | 53 |
| awgur@43 | 54 class NullLog(object): |
| awgur@43 | 55 def __init__(self, prefix: str = 'main'): |
| awgur@43 | 56 self.prefix = prefix |
| awgur@11 | 57 |
| awgur@11 | 58 @staticmethod |
| awgur@43 | 59 def _write(mark: str, msg: Any): |
| awgur@43 | 60 pass # cat > /dev/null |
| awgur@11 | 61 |
| awgur@11 | 62 def __call__(self, msg): |
| awgur@43 | 63 self._write('.', msg) |
| awgur@11 | 64 |
| awgur@11 | 65 def err(self, msg): |
| awgur@43 | 66 self._write('!', msg) |
| awgur@11 | 67 |
| awgur@11 | 68 def warn(self, msg): |
| awgur@43 | 69 self._write('*', msg) |
| awgur@11 | 70 |
| awgur@11 | 71 def alert(self, msg): |
| awgur@43 | 72 self._write('#', msg) |
| awgur@11 | 73 |
| awgur@11 | 74 def debug(self, msg): |
| awgur@43 | 75 self._write('`', msg) |
| awgur@11 | 76 |
| awgur@11 | 77 @staticmethod |
| awgur@43 | 78 def get_timing(name: Optional[str] = None): |
| awgur@11 | 79 return Timing(name) |
| awgur@43 | 80 |
| awgur@43 | 81 def sub_log(self, name: str): |
| awgur@43 | 82 return self.__class__(f'{self.prefix}/{name}') |
| awgur@43 | 83 |
| awgur@43 | 84 def excpt(self, msg, e_class=None, e_obj=None, e_tb=None, stack_skip=0): |
| awgur@43 | 85 if e_class is None: |
| awgur@43 | 86 e_class, e_obj, e_tb = exc_info() |
| awgur@43 | 87 |
| awgur@43 | 88 tb_data_tb = list(extract_tb(e_tb))[::-1] |
| awgur@43 | 89 tb_data_stack = list(extract_stack())[::-1][(2 + stack_skip):] |
| awgur@11 | 90 self.err(msg) |
| awgur@11 | 91 self.err('--- EXCEPTION ---') |
| awgur@43 | 92 self.err(f' {e_class.__name__} ({e_obj})') |
| awgur@11 | 93 self.err('--- TRACEBACK ---') |
| awgur@43 | 94 for _tb_file, _tb_line, _tb_func, _tb_text in tb_data_tb: |
| awgur@43 | 95 self.err(f'File: {_tb_file}, line {_tb_line} in {_tb_func}') |
| awgur@43 | 96 self.err(f' {_tb_text}') |
| awgur@43 | 97 |
| awgur@11 | 98 self.err('>>> Exception Handler <<<') |
| awgur@43 | 99 for _tb_file, _tb_line, _tb_func, _tb_text in tb_data_stack: |
| awgur@43 | 100 self.err(f'File: {_tb_file}, line {_tb_line} in {_tb_func}') |
| awgur@43 | 101 self.err(f' {_tb_text}') |
| awgur@43 | 102 |
| awgur@11 | 103 self.err('--- END EXCEPTION ---') |
| awgur@11 | 104 |
| awgur@11 | 105 |
| awgur@43 | 106 class FileLog(NullLog): |
| awgur@11 | 107 @staticmethod |
| awgur@43 | 108 def _open_file(file_name: str): |
| awgur@43 | 109 return open(file_name, 'a', encoding='utf-8') |
| awgur@43 | 110 |
| awgur@43 | 111 def __init__(self, prefix: str = 'main', |
| awgur@43 | 112 file_name: Optional[str] = None, |
| awgur@43 | 113 file_obj: Optional[TextIO] = None, |
| awgur@43 | 114 time_to_flush: int = TIME_TO_FLUSH |
| awgur@43 | 115 ): |
| awgur@43 | 116 |
| awgur@43 | 117 super().__init__(prefix=prefix) |
| awgur@43 | 118 self.fd: Optional[TextIO] = None |
| awgur@43 | 119 |
| awgur@43 | 120 self.flush_time = monotonic() |
| awgur@43 | 121 self.time_to_flush = time_to_flush # Время с момента прошлого сброса лога, |
| awgur@43 | 122 # после которого, выполняется принудительный сброс буферов |
| awgur@43 | 123 |
| awgur@43 | 124 if file_name is not None: |
| awgur@43 | 125 self.fd = self._open_file(file_name) |
| awgur@43 | 126 |
| awgur@43 | 127 else: |
| awgur@43 | 128 self.fd = file_obj |
| awgur@43 | 129 |
| awgur@43 | 130 if self.fd is None: |
| awgur@44 | 131 raise ValueError('Не задан файл для записи журналов') |
| awgur@43 | 132 |
| awgur@43 | 133 def flush(self, time_mark: float = None): |
| awgur@43 | 134 if time_mark is None: |
| awgur@43 | 135 time_mark = monotonic() |
| awgur@43 | 136 |
| awgur@43 | 137 self.flush_time = time_mark |
| awgur@43 | 138 self.fd.flush() |
| awgur@43 | 139 |
| awgur@43 | 140 def close(self): |
| awgur@43 | 141 self.fd.flush() |
| awgur@43 | 142 self.fd.close() |
| awgur@43 | 143 self.fd = None |
| awgur@43 | 144 |
| awgur@43 | 145 def __del__(self): |
| awgur@43 | 146 if self.fd is not None: |
| awgur@43 | 147 self.fd.flush() |
| awgur@43 | 148 self.fd.close() |
| awgur@43 | 149 self.fd = None |
| awgur@43 | 150 |
| awgur@43 | 151 def flush_event(self): |
| awgur@43 | 152 t = monotonic() |
| awgur@43 | 153 if t - self.flush_time >= self.time_to_flush: |
| awgur@43 | 154 self.flush(t) |
| awgur@43 | 155 |
| awgur@43 | 156 def _write(self, mark: str, msg: Any): |
| awgur@43 | 157 if self.fd is None: |
| awgur@43 | 158 raise ValueError('Попытка использовать закрытый файл журнала') |
| awgur@43 | 159 |
| awgur@43 | 160 t = ctime() |
| awgur@43 | 161 for l in str(msg).splitlines(): |
| awgur@43 | 162 self.fd.write(f'{t} | {mark} {self.prefix} | {l}') |
| awgur@43 | 163 |
| awgur@43 | 164 self.flush_event() |
| awgur@43 | 165 |
| awgur@43 | 166 def sub_log(self, name: str): |
| awgur@43 | 167 if self.fd is None: |
| awgur@43 | 168 raise ValueError('Попытка использовать закрытый файл журнала') |
| awgur@43 | 169 |
| awgur@43 | 170 return self.__class__(f'{self.prefix}/{name}', file_obj=self.fd, time_to_flush=self.time_to_flush) |
| awgur@11 | 171 |
| awgur@11 | 172 |
| awgur@43 | 173 class StderrLog(FileLog): |
| awgur@43 | 174 def __init__(self, prefix: str = 'main'): |
| awgur@43 | 175 super().__init__(prefix, file_obj=stderr) |
| awgur@43 | 176 |
| awgur@43 | 177 def flush_event(self): |
| awgur@43 | 178 pass # нет необходимости сбрасывать буферы в консоли |
| awgur@43 | 179 |
| awgur@43 | 180 |
| awgur@43 | 181 class StdoutLog(FileLog): |
| awgur@43 | 182 def __init__(self, prefix: str = 'main'): |
| awgur@43 | 183 super().__init__(prefix, file_obj=stdout) |
| awgur@43 | 184 |
| awgur@43 | 185 def flush_event(self): |
| awgur@43 | 186 pass # нет необходимости сбрасывать буферы в консоли |
| awgur@43 | 187 |
| awgur@43 | 188 |
| awgur@43 | 189 class LogrotateFile(FileLog): |
| awgur@44 | 190 def __init__(self, |
| awgur@44 | 191 file_base: str, |
| awgur@44 | 192 file_obj: Optional[TextIO] = None, |
| awgur@44 | 193 prefix: str = 'main', |
| awgur@44 | 194 time_to_flush: int = TIME_TO_FLUSH): |
| awgur@44 | 195 |
| awgur@43 | 196 d = date.today() |
| awgur@44 | 197 if file_obj is None: |
| awgur@44 | 198 super().__init__(prefix=prefix, file_name=f'{file_base}-{d}.log', time_to_flush=time_to_flush) |
| awgur@44 | 199 else: |
| awgur@44 | 200 super().__init__(prefix=prefix, file_obj=file_obj, time_to_flush=time_to_flush) |
| awgur@44 | 201 |
| awgur@44 | 202 self.logrotate_base = file_base |
| awgur@44 | 203 self.logrotate_date = d |
| awgur@44 | 204 |
| awgur@44 | 205 @classmethod |
| awgur@44 | 206 def make(cls, directory: str = '.', prefix: str = 'main', time_to_flush: int = TIME_TO_FLUSH): |
| awgur@44 | 207 if len(p_split(prefix)) > 1: |
| awgur@44 | 208 raise ValueError(f'Префикс журналирования не должен быть похож ' |
| awgur@44 | 209 f'на пусть файловой системы, убери знак разделения директорий: {prefix}') |
| awgur@44 | 210 |
| awgur@43 | 211 directory = abspath(directory) |
| awgur@44 | 212 file_base = p_join(directory, prefix) |
| awgur@44 | 213 return cls(file_base=file_base, prefix=prefix, time_to_flush=time_to_flush) |
| awgur@43 | 214 |
| awgur@43 | 215 def flush(self, time_mark: float = None, no_rotate: bool = False): |
| awgur@43 | 216 if not no_rotate: |
| awgur@43 | 217 d = date.today() |
| awgur@43 | 218 if self.logrotate_date != d: |
| awgur@43 | 219 self.logrotate_rotate() |
| awgur@43 | 220 return |
| awgur@43 | 221 |
| awgur@43 | 222 super().flush(time_mark=time_mark) |
| awgur@43 | 223 |
| awgur@43 | 224 def logrotate_rotate(self): |
| awgur@43 | 225 d = date.today() |
| awgur@43 | 226 file_name = f'{self.logrotate_base}-{d}.log' |
| awgur@43 | 227 self.fd.flush() |
| awgur@43 | 228 self.fd = self._open_file(file_name) |
| awgur@43 | 229 self.logrotate_date = d |
| awgur@43 | 230 self.flush(no_rotate=True) |
| awgur@43 | 231 |
| awgur@44 | 232 def sub_log(self, name: str): |
| awgur@44 | 233 return self.__class__( |
| awgur@44 | 234 file_base=self.logrotate_base, |
| awgur@44 | 235 file_obj=self.fd, prefix=f'{self.prefix}/{name}', |
| awgur@44 | 236 time_to_flush=self.time_to_flush |
| awgur@44 | 237 ) |
| awgur@44 | 238 |
| awgur@43 | 239 |
| awgur@43 | 240 class ThreadSafeFileLog(FileLog): |
| awgur@44 | 241 def __int__(self, prefix: str = 'main', |
| awgur@44 | 242 file_name: Optional[str] = None, |
| awgur@44 | 243 file_obj: Optional[TextIO] = None, |
| awgur@44 | 244 time_to_flush: int = TIME_TO_FLUSH |
| awgur@44 | 245 ): |
| awgur@44 | 246 super().__init__(prefix=prefix, file_name=file_name, file_obj=file_obj, time_to_flush=time_to_flush) |
| awgur@43 | 247 self.thread_safe_lock = RLock() |
| awgur@43 | 248 |
| awgur@43 | 249 def _write(self, mark: str, msg: Any): |
| awgur@43 | 250 with self.thread_safe_lock: |
| awgur@43 | 251 super()._write(mark, msg) |
| awgur@43 | 252 |
| awgur@43 | 253 def flush(self, time_mark: float = None): |
| awgur@43 | 254 with self.thread_safe_lock: |
| awgur@43 | 255 super().flush(time_mark) |
| awgur@43 | 256 |
| awgur@44 | 257 def sub_log(self, name: str): |
| awgur@44 | 258 if self.fd is None: |
| awgur@44 | 259 raise ValueError('Попытка использовать закрытый файл журнала') |
| awgur@44 | 260 |
| awgur@44 | 261 obj = self.__class__(prefix=f'{self.prefix}/{name}', file_obj=self.fd, time_to_flush=self.time_to_flush) |
| awgur@44 | 262 obj.thread_safe_lock = self.thread_safe_lock |
| awgur@44 | 263 return obj |
| awgur@44 | 264 |
| awgur@43 | 265 |
| awgur@43 | 266 class ThreadSafeLogrotateFile(LogrotateFile): |
| awgur@44 | 267 def __init__(self, file_base: str, file_obj: Optional[TextIO] = None, prefix: str = 'main', |
| awgur@44 | 268 time_to_flush: int = TIME_TO_FLUSH): |
| awgur@44 | 269 |
| awgur@44 | 270 super().__init__(file_base=file_base, file_obj=file_obj, prefix=prefix, time_to_flush=time_to_flush) |
| awgur@43 | 271 self.thread_safe_lock = RLock() |
| awgur@43 | 272 |
| awgur@43 | 273 def _write(self, mark: str, msg: Any): |
| awgur@43 | 274 with self.thread_safe_lock: |
| awgur@43 | 275 super()._write(mark, msg) |
| awgur@43 | 276 |
| awgur@43 | 277 def flush(self, time_mark: float = None, no_rotate: bool = False): |
| awgur@43 | 278 with self.thread_safe_lock: |
| awgur@43 | 279 super().flush(time_mark=time_mark, no_rotate=no_rotate) |
| awgur@43 | 280 |
| awgur@43 | 281 def logrotate_rotate(self): |
| awgur@43 | 282 with self.thread_safe_lock: |
| awgur@43 | 283 super().logrotate_rotate() |
| awgur@44 | 284 |
| awgur@44 | 285 def sub_log(self, name: str): |
| awgur@44 | 286 if self.fd is None: |
| awgur@44 | 287 raise ValueError('Попытка использовать закрытый файл журнала') |
| awgur@44 | 288 |
| awgur@44 | 289 obj = self.__class__(file_base=self.logrotate_base, |
| awgur@44 | 290 file_obj=self.fd, |
| awgur@44 | 291 prefix=f'{self.prefix}/{name}', |
| awgur@44 | 292 time_to_flush=self.time_to_flush |
| awgur@44 | 293 ) |
| awgur@44 | 294 |
| awgur@44 | 295 obj.thread_safe_lock = self.thread_safe_lock |
| awgur@44 | 296 return obj |