|
| 1 | +""" |
| 2 | +Logger |
| 3 | +""" |
| 4 | + |
| 5 | +import sys |
| 6 | +from pathlib import Path |
| 7 | +from typing import Union, List |
| 8 | +import logging |
| 9 | + |
| 10 | + |
| 11 | +class ColoredFormatter(logging.Formatter): |
| 12 | + """ |
| 13 | + Logging colored formatter |
| 14 | + adapted from https://stackoverflow.com/a/56944256/3638629 |
| 15 | + """ |
| 16 | + |
| 17 | + grey = '\x1b[38;21m' |
| 18 | + blue = '\x1b[38;5;39m' |
| 19 | + yellow = '\x1b[38;5;226m' |
| 20 | + red = '\x1b[38;5;196m' |
| 21 | + bold_red = '\x1b[31;1m' |
| 22 | + reset = '\x1b[0m' |
| 23 | + |
| 24 | + def __init__(self, fmt): |
| 25 | + super().__init__() |
| 26 | + self.fmt = fmt |
| 27 | + self.formats = { |
| 28 | + logging.DEBUG: self.blue + self.fmt + self.reset, |
| 29 | + logging.INFO: self.grey + self.fmt + self.reset, |
| 30 | + logging.WARNING: self.yellow + self.fmt + self.reset, |
| 31 | + logging.ERROR: self.red + self.fmt + self.reset, |
| 32 | + logging.CRITICAL: self.bold_red + self.fmt + self.reset |
| 33 | + } |
| 34 | + |
| 35 | + def format(self, record): |
| 36 | + log_fmt = self.formats.get(record.levelno) |
| 37 | + formatter = logging.Formatter(log_fmt) |
| 38 | + return formatter.format(record) |
| 39 | + |
| 40 | + |
| 41 | +class Logger: |
| 42 | + """ |
| 43 | + Improved logging |
| 44 | + """ |
| 45 | + LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] |
| 46 | + DEFAULT_LEVEL = 'INFO' |
| 47 | + DEFAULT_FORMAT = '%(asctime)s - %(levelname)-8s - %(name)-12s: %(message)s' |
| 48 | + def __init__(self, name: str = None, |
| 49 | + level: str = DEFAULT_LEVEL, |
| 50 | + _format: str = DEFAULT_FORMAT, |
| 51 | + colored_log: bool = False, |
| 52 | + logfile_path: Union[str, Path] = None): |
| 53 | + """ |
| 54 | + Initialize Logger |
| 55 | +
|
| 56 | + Parameters |
| 57 | + ---------- |
| 58 | + name : str |
| 59 | + Name of the Logger object |
| 60 | + default : None |
| 61 | + level : str |
| 62 | + Desired Logging level |
| 63 | + default : 'INFO' |
| 64 | + _format : str |
| 65 | + Desired Logging Format |
| 66 | + default : '%(asctime)s - %(levelname)-8s - %(name)-12s: %(message)s' |
| 67 | + colored_log : bool |
| 68 | + Use colored logging for stdout |
| 69 | + default: False |
| 70 | + logfile_path : str or Path |
| 71 | + Path for logging to a file |
| 72 | + default : None |
| 73 | + """ |
| 74 | + |
| 75 | + self.name = name |
| 76 | + self.level = level.upper() |
| 77 | + self.format = _format |
| 78 | + self.colored_log = colored_log |
| 79 | + |
| 80 | + if self.level not in Logger.LOG_LEVELS: |
| 81 | + raise LookupError('{self.level} is unknown logging level\n' + |
| 82 | + 'Currently supported log levels are:\n' + |
| 83 | + f'{" | ".join(Logger.LOG_LEVELS)}') |
| 84 | + |
| 85 | + # Initialize the root logger if no name is present |
| 86 | + self._logger = logging.getLogger(name) if name else logging.getLogger() |
| 87 | + |
| 88 | + self._logger.setLevel(self.level) |
| 89 | + |
| 90 | + _handlers = [] |
| 91 | + # Add console handler for logger |
| 92 | + _handler = Logger.add_stream_handler( |
| 93 | + level=self.level, |
| 94 | + _format=self.format, |
| 95 | + colored_log=self.colored_log, |
| 96 | + ) |
| 97 | + _handlers.append(_handler) |
| 98 | + self._logger.addHandler(_handler) |
| 99 | + |
| 100 | + # Add file handler for logger |
| 101 | + if logfile_path is not None: |
| 102 | + _handler = Logger.add_file_handler(logfile_path, level=self.level, _format=self.format) |
| 103 | + self._logger.addHandler(_handler) |
| 104 | + _handlers.append(_handler) |
| 105 | + |
| 106 | + def __getattr__(self, attribute): |
| 107 | + """ |
| 108 | + Allows calling logging module methods directly |
| 109 | +
|
| 110 | + Parameters |
| 111 | + ---------- |
| 112 | + attribute : str |
| 113 | + attribute name of a logging object |
| 114 | +
|
| 115 | + Returns |
| 116 | + ------- |
| 117 | + attribute : logging attribute |
| 118 | + """ |
| 119 | + return getattr(self._logger, attribute) |
| 120 | + |
| 121 | + def get_logger(self): |
| 122 | + """ |
| 123 | + Return the logging object |
| 124 | +
|
| 125 | + Returns |
| 126 | + ------- |
| 127 | + logger : Logger object |
| 128 | + """ |
| 129 | + return self._logger |
| 130 | + |
| 131 | + @classmethod |
| 132 | + def add_handlers(cls, logger: logging.Logger, handlers: List[logging.Handler]): |
| 133 | + """ |
| 134 | + Add a list of handlers to a logger |
| 135 | +
|
| 136 | + Parameters |
| 137 | + ---------- |
| 138 | + logger : logging.Logger |
| 139 | + Logger object to add a new handler to |
| 140 | + handlers: list |
| 141 | + A list of handlers to be added to the logger object |
| 142 | +
|
| 143 | + Returns |
| 144 | + ------- |
| 145 | + logger : Logger object |
| 146 | + """ |
| 147 | + for handler in handlers: |
| 148 | + logger.addHandler(handler) |
| 149 | + |
| 150 | + return logger |
| 151 | + |
| 152 | + @classmethod |
| 153 | + def add_stream_handler(cls, level: str = DEFAULT_LEVEL, |
| 154 | + _format: str = DEFAULT_FORMAT, |
| 155 | + colored_log: bool = False): |
| 156 | + """ |
| 157 | + Create stream handler |
| 158 | + This classmethod will allow setting a custom stream handler on children |
| 159 | +
|
| 160 | + Parameters |
| 161 | + ---------- |
| 162 | + level : str |
| 163 | + logging level |
| 164 | + default : 'INFO' |
| 165 | + _format : str |
| 166 | + logging format |
| 167 | + default : '%(asctime)s - %(levelname)-8s - %(name)-12s: %(message)s' |
| 168 | + colored_log : bool |
| 169 | + enable colored output for stdout |
| 170 | + default : False |
| 171 | +
|
| 172 | + Returns |
| 173 | + ------- |
| 174 | + handler : logging.Handler |
| 175 | + stream handler of a logging object |
| 176 | + """ |
| 177 | + |
| 178 | + handler = logging.StreamHandler(sys.stdout) |
| 179 | + handler.setLevel(level) |
| 180 | + _format = ColoredFormatter(_format) if colored_log else logging.Formatter(_format) |
| 181 | + handler.setFormatter(_format) |
| 182 | + |
| 183 | + return handler |
| 184 | + |
| 185 | + @classmethod |
| 186 | + def add_file_handler(cls, logfile_path: Union[str, Path], |
| 187 | + level: str = DEFAULT_LEVEL, |
| 188 | + _format: str = DEFAULT_FORMAT): |
| 189 | + """ |
| 190 | + Create file handler. |
| 191 | + This classmethod will allow setting custom file handler on children |
| 192 | + Create stream handler |
| 193 | + This classmethod will allow setting a custom stream handler on children |
| 194 | +
|
| 195 | + Parameters |
| 196 | + ---------- |
| 197 | + logfile_path: str or Path |
| 198 | + Path for writing out logfiles from logging |
| 199 | + default : False |
| 200 | + level : str |
| 201 | + logging level |
| 202 | + default : 'INFO' |
| 203 | + _format : str |
| 204 | + logging format |
| 205 | + default : '%(asctime)s - %(levelname)-8s - %(name)-12s: %(message)s' |
| 206 | +
|
| 207 | + Returns |
| 208 | + ------- |
| 209 | + handler : logging.Handler |
| 210 | + file handler of a logging object |
| 211 | + """ |
| 212 | + |
| 213 | + logfile_path = Path(logfile_path) |
| 214 | + |
| 215 | + # Create the directory containing the logfile_path |
| 216 | + if not logfile_path.parent.is_dir(): |
| 217 | + logfile_path.mkdir(parents=True, exist_ok=True) |
| 218 | + |
| 219 | + handler = logging.FileHandler(str(logfile_path)) |
| 220 | + handler.setLevel(level) |
| 221 | + handler.setFormatter(logging.Formatter(_format)) |
| 222 | + |
| 223 | + return handler |
0 commit comments