feat: logging
This commit is contained in:
1
logger/__init__.py
Normal file
1
logger/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from logger.builder import logger_builder as logger_builder
|
||||
60
logger/builder.py
Normal file
60
logger/builder.py
Normal file
@ -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 core.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()
|
||||
25
logger/constants.py
Normal file
25
logger/constants.py
Normal file
@ -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")
|
||||
17
logger/filters.py
Normal file
17
logger/filters.py
Normal file
@ -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
|
||||
20
logger/formatter.py
Normal file
20
logger/formatter.py
Normal file
@ -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)
|
||||
35
logger/gunzip_rotating_file_handler.py
Normal file
35
logger/gunzip_rotating_file_handler.py
Normal file
@ -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()
|
||||
Reference in New Issue
Block a user