|
| 1 | +# Copyright (C) 2024 Ferenc Nandor Janky |
| 2 | +# Copyright (C) 2024 Attila Gombos |
| 3 | +# Contact: info@effective-range.com |
| 4 | +# |
| 5 | +# This library is free software; you can redistribute it and/or |
| 6 | +# modify it under the terms of the GNU Lesser General Public |
| 7 | +# License as published by the Free Software Foundation; either |
| 8 | +# version 2.1 of the License, or (at your option) any later version. |
| 9 | +# |
| 10 | +# This library is distributed in the hope that it will be useful, |
| 11 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 13 | +# Lesser General Public License for more details. |
| 14 | +# |
| 15 | +# You should have received a copy of the GNU Lesser General Public |
| 16 | +# License along with this library; if not, write to the Free Software |
| 17 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 |
| 18 | +# USA |
| 19 | +import logging |
| 20 | +import logging.handlers |
| 21 | +import os |
| 22 | +import sys |
| 23 | +import warnings |
| 24 | +from logging.handlers import RotatingFileHandler |
| 25 | +from typing import Any, Optional |
| 26 | + |
| 27 | +import structlog |
| 28 | +from structlog._log_levels import add_log_level |
| 29 | +from structlog.dev import ConsoleRenderer |
| 30 | +from structlog.processors import JSONRenderer, StackInfoRenderer, TimeStamper, EventRenamer, format_exc_info, \ |
| 31 | + UnicodeDecoder, CallsiteParameterAdder |
| 32 | +from structlog.stdlib import ProcessorFormatter, LoggerFactory, BoundLogger, PositionalArgumentsFormatter, \ |
| 33 | + add_logger_name |
| 34 | + |
| 35 | +from context_logger import ContextSetupFilter |
| 36 | + |
| 37 | +STRUCTURED_LOGGER = None |
| 38 | + |
| 39 | + |
| 40 | +def get_logger(logger_name: str) -> Any: |
| 41 | + return structlog.get_logger(logger_name) |
| 42 | + |
| 43 | + |
| 44 | +def setup_logging(application_name: str, log_level: str = 'INFO', |
| 45 | + log_file_path: Optional[str] = None, max_bytes: int = 1024 * 1024, backup_count: int = 5, |
| 46 | + add_call_info: bool = False, message_field: str = 'message') -> None: |
| 47 | + global STRUCTURED_LOGGER |
| 48 | + |
| 49 | + if STRUCTURED_LOGGER is not None: |
| 50 | + warnings.warn('Logging has already been set up, overwriting existing configuration.', UserWarning) |
| 51 | + |
| 52 | + STRUCTURED_LOGGER = StructuredLogger(application_name, log_level, log_file_path, max_bytes, backup_count, |
| 53 | + add_call_info, message_field) |
| 54 | + |
| 55 | + |
| 56 | +class StructuredLogger(object): |
| 57 | + |
| 58 | + def __init__(self, application_name: str, log_level: str, log_file_path: Optional[str], max_bytes: int, |
| 59 | + backup_count: int, add_call_info: bool, message_field: str) -> None: |
| 60 | + self._application_name = application_name |
| 61 | + self._log_level = logging.getLevelName(log_level.upper()) |
| 62 | + self._max_bytes = max_bytes |
| 63 | + self._backup_count = backup_count |
| 64 | + self._message_field = message_field |
| 65 | + self._add_call_info = add_call_info |
| 66 | + |
| 67 | + self._shared_processors: Any = [ |
| 68 | + add_log_level, |
| 69 | + add_logger_name, |
| 70 | + PositionalArgumentsFormatter(), |
| 71 | + StackInfoRenderer(), |
| 72 | + TimeStamper(fmt='iso'), |
| 73 | + format_exc_info, |
| 74 | + UnicodeDecoder() |
| 75 | + ] |
| 76 | + |
| 77 | + if self._add_call_info: |
| 78 | + self._add_call_info_processor() |
| 79 | + |
| 80 | + self._shared_processors.append(EventRenamer(self._message_field)) |
| 81 | + |
| 82 | + root = logging.getLogger() |
| 83 | + root.setLevel(self._log_level) |
| 84 | + |
| 85 | + self._clear_handlers(root) |
| 86 | + |
| 87 | + console_handler = self._create_console_handler() |
| 88 | + root.addHandler(console_handler) |
| 89 | + |
| 90 | + if log_file_path is not None: |
| 91 | + file_handler = self._create_file_handler(log_file_path) |
| 92 | + root.addHandler(file_handler) |
| 93 | + |
| 94 | + structlog.configure( |
| 95 | + processors=self._shared_processors + [ProcessorFormatter.wrap_for_formatter], |
| 96 | + logger_factory=LoggerFactory(), |
| 97 | + wrapper_class=BoundLogger, |
| 98 | + cache_logger_on_first_use=True |
| 99 | + ) |
| 100 | + |
| 101 | + def _add_call_info_processor(self) -> None: |
| 102 | + self._shared_processors.append(CallsiteParameterAdder( |
| 103 | + { |
| 104 | + structlog.processors.CallsiteParameter.MODULE, |
| 105 | + structlog.processors.CallsiteParameter.PATHNAME, |
| 106 | + structlog.processors.CallsiteParameter.FUNC_NAME, |
| 107 | + structlog.processors.CallsiteParameter.LINENO, |
| 108 | + structlog.processors.CallsiteParameter.PROCESS_NAME, |
| 109 | + structlog.processors.CallsiteParameter.THREAD_NAME |
| 110 | + } |
| 111 | + )) |
| 112 | + |
| 113 | + def _clear_handlers(self, root: logging.Logger) -> None: |
| 114 | + for handler in root.handlers: |
| 115 | + if isinstance(handler, logging.StreamHandler) or isinstance(handler, RotatingFileHandler): |
| 116 | + root.removeHandler(handler) |
| 117 | + |
| 118 | + def _create_console_handler(self) -> logging.Handler: |
| 119 | + formatter = ProcessorFormatter( |
| 120 | + foreign_pre_chain=self._shared_processors + [self._enrich_stdlib_log], |
| 121 | + processors=[ProcessorFormatter.remove_processors_meta, ConsoleRenderer(event_key=self._message_field)] |
| 122 | + ) |
| 123 | + |
| 124 | + handler = logging.StreamHandler(sys.stdout) |
| 125 | + handler.setFormatter(formatter) |
| 126 | + handler.addFilter(ContextSetupFilter(self._application_name)) |
| 127 | + |
| 128 | + return handler |
| 129 | + |
| 130 | + def _create_file_handler(self, log_file_path: str) -> RotatingFileHandler: |
| 131 | + formatter = ProcessorFormatter( |
| 132 | + foreign_pre_chain=self._shared_processors + [self._enrich_stdlib_log], |
| 133 | + processors=[ProcessorFormatter.remove_processors_meta, JSONRenderer()] |
| 134 | + ) |
| 135 | + |
| 136 | + self._ensure_directory_exists(log_file_path) |
| 137 | + |
| 138 | + handler = RotatingFileHandler(log_file_path, maxBytes=self._max_bytes, backupCount=self._backup_count) |
| 139 | + handler.setFormatter(formatter) |
| 140 | + handler.addFilter(ContextSetupFilter(self._application_name)) |
| 141 | + |
| 142 | + return handler |
| 143 | + |
| 144 | + def _ensure_directory_exists(self, log_file_path: str) -> None: |
| 145 | + directory = os.path.dirname(log_file_path) |
| 146 | + |
| 147 | + if not os.path.exists(directory): |
| 148 | + os.makedirs(directory) |
| 149 | + |
| 150 | + def _enrich_stdlib_log(self, _: logging.Logger, __: str, event_dict: dict[Any, Any]) -> dict[Any, Any]: |
| 151 | + event_dict.update(event_dict['_record'].msg) |
| 152 | + |
| 153 | + return event_dict |
0 commit comments