From 678dd18e12ebc70f82eddb34995301387b3ca6e8 Mon Sep 17 00:00:00 2001 From: aserbin Date: Tue, 25 Nov 2025 11:01:36 +0400 Subject: [PATCH] feat: added logger --- logger/__init__.py | 1 + logger/builder.py | 60 ++++++++++++++++++++++++++ logger/constants.py | 25 +++++++++++ logger/filters.py | 17 ++++++++ logger/formatter.py | 20 +++++++++ logger/gunzip_rotating_file_handler.py | 35 +++++++++++++++ utils/singleton.py | 7 +++ 7 files changed, 165 insertions(+) create mode 100644 logger/__init__.py create mode 100644 logger/builder.py create mode 100644 logger/constants.py create mode 100644 logger/filters.py create mode 100644 logger/formatter.py create mode 100644 logger/gunzip_rotating_file_handler.py create mode 100644 utils/singleton.py diff --git a/logger/__init__.py b/logger/__init__.py new file mode 100644 index 0000000..f30fff0 --- /dev/null +++ b/logger/__init__.py @@ -0,0 +1 @@ +from logger.builder import logger_builder as logger_builder diff --git a/logger/builder.py b/logger/builder.py new file mode 100644 index 0000000..9c6f8c4 --- /dev/null +++ b/logger/builder.py @@ -0,0 +1,60 @@ +import logging +import logging.config + +from logger.constants import ( + LEVEL_NAME, + BACKUP_COUNT, + LOGS_FOLDER, + MAX_LOG_FILE_SIZE_BYTES, +) +from logger.formatter import JsonFormatter +from logger.gunzip_rotating_file_handler import GunZipRotatingFileHandler +from logger.filters import LevelFilter, RequestIdFilter +from utils.singleton import Singleton + + +class LoggerBuilder(metaclass=Singleton): + def get_logger(self) -> logging.Logger: + logger = logging.getLogger("crm") + logger.setLevel(logging.DEBUG) + logger.handlers.clear() + self.set_handlers(logger) + return logger + + def set_handlers(self, logger: logging.Logger): + LOGGER_LEVEL_STEP = 10 + + for level in range(logging.DEBUG, logging.CRITICAL + 1, LOGGER_LEVEL_STEP): + logger.addHandler(self.create_rotating_file_handler(level)) + + logger.addHandler(self.create_console_handler()) + + @classmethod + def create_rotating_file_handler(cls, level: int) -> GunZipRotatingFileHandler: + folder = LOGS_FOLDER / LEVEL_NAME[level] + folder.mkdir(parents=True, exist_ok=True) + filename = LEVEL_NAME[level] + ".log" + + file_handler = GunZipRotatingFileHandler( + folder / filename, + maxBytes=MAX_LOG_FILE_SIZE_BYTES, + encoding="UTF-8", + backupCount=BACKUP_COUNT[level], + ) + + file_handler.addFilter(LevelFilter(level)) + file_handler.addFilter(RequestIdFilter()) + file_handler.setFormatter(JsonFormatter()) + + return file_handler + + @classmethod + def create_console_handler(cls) -> logging.StreamHandler: + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + + console_handler.setFormatter(JsonFormatter()) + return console_handler + + +logger_builder = LoggerBuilder() diff --git a/logger/constants.py b/logger/constants.py new file mode 100644 index 0000000..b2fca21 --- /dev/null +++ b/logger/constants.py @@ -0,0 +1,25 @@ +import logging +from pathlib import Path + +from constants import APP_PATH + +LEVEL_NAME = { + logging.FATAL: "fatal", + logging.CRITICAL: "critical", + logging.ERROR: "error", + logging.WARNING: "warning", + logging.INFO: "info", + logging.DEBUG: "debug", +} + +BACKUP_COUNT = { + logging.FATAL: 5, + logging.CRITICAL: 5, + logging.ERROR: 4, + logging.WARNING: 3, + logging.INFO: 2, + logging.DEBUG: 1, +} + +MAX_LOG_FILE_SIZE_BYTES = 30 * 1024 * 1024 # 30 Mb +LOGS_FOLDER = Path(APP_PATH) / Path("logs") diff --git a/logger/filters.py b/logger/filters.py new file mode 100644 index 0000000..d548173 --- /dev/null +++ b/logger/filters.py @@ -0,0 +1,17 @@ +import logging +import uuid + + +class LevelFilter(logging.Filter): + def __init__(self, level): + super().__init__() + self.level = level + + def filter(self, record): + return record.levelno == self.level + + +class RequestIdFilter(logging.Filter): + def filter(self, record): + record.request_id = str(uuid.uuid4()) + return True diff --git a/logger/formatter.py b/logger/formatter.py new file mode 100644 index 0000000..e08339f --- /dev/null +++ b/logger/formatter.py @@ -0,0 +1,20 @@ +import json +import logging +from datetime import datetime, UTC + + +class JsonFormatter(logging.Formatter): + def format(self, record: any): + log_record = { + "timestamp": datetime.now(UTC).isoformat(), + "level": record.levelname, + "module": record.module, + "line": record.lineno, + "message": record.getMessage(), + "request_id": record.request_id, + } + + if record.exc_info: + log_record["exception"] = self.formatException(record.exc_info) + + return json.dumps(log_record) diff --git a/logger/gunzip_rotating_file_handler.py b/logger/gunzip_rotating_file_handler.py new file mode 100644 index 0000000..35bdf98 --- /dev/null +++ b/logger/gunzip_rotating_file_handler.py @@ -0,0 +1,35 @@ +import gzip +from logging.handlers import RotatingFileHandler +import shutil +import os + + +class GunZipRotatingFileHandler(RotatingFileHandler): + def doRollover(self): + if self.stream: + self.stream.close() + self.stream = None + + if self.backupCount > 0: + # Rotate existing backup files + for i in range(self.backupCount - 1, 0, -1): + sfn = self.rotation_filename("%s.%d.gz" % (self.baseFilename, i)) + dfn = self.rotation_filename("%s.%d.gz" % (self.baseFilename, i + 1)) + if os.path.exists(sfn): + if os.path.exists(dfn): + os.remove(dfn) + os.rename(sfn, dfn) + + # Compress current log file to .1.gz + dfn = self.rotation_filename(self.baseFilename + ".1.gz") + if os.path.exists(dfn): + os.remove(dfn) + + if os.path.exists(self.baseFilename): + with open(self.baseFilename, "rb") as f_in: + with gzip.open(dfn, "wb") as f_out: + shutil.copyfileobj(f_in, f_out) + os.remove(self.baseFilename) + + if not self.delay: + self.stream = self._open() diff --git a/utils/singleton.py b/utils/singleton.py new file mode 100644 index 0000000..3776cb9 --- /dev/null +++ b/utils/singleton.py @@ -0,0 +1,7 @@ +class Singleton(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) + return cls._instances[cls]