import logging import sys from types import FrameType from typing import TYPE_CHECKING, Any, cast from constants import LogLevelEnum from loguru import logger from sentry_sdk.integrations.logging import EventHandler from settings.config import get_settings if TYPE_CHECKING: from loguru import Record else: Record = dict[str, Any] settings = get_settings() class InterceptHandler(logging.Handler): def emit(self, record: logging.LogRecord) -> None: # Get corresponding Loguru level if it exists try: level = logger.level(record.levelname).name except ValueError: level = str(record.levelno) # Find caller from where originated the logged message frame, depth = logging.currentframe(), 2 while frame.f_code.co_filename == logging.__file__: frame = cast(FrameType, frame.f_back) depth += 1 logger.opt(depth=depth, exception=record.exc_info).log( level, record.getMessage().replace(settings.TELEGRAM_API_TOKEN, "TELEGRAM_API_TOKEN".center(24, '*')), ) def configure_logging(*, level: LogLevelEnum, enable_json_logs: bool, enable_sentry_logs: bool) -> None: logging_level = level.name intercept_handler = InterceptHandler() logging.basicConfig(handlers=[intercept_handler], level=logging_level) formatter = _json_formatter if enable_json_logs else _text_formatter logger.configure( handlers=[ { "sink": sys.stdout, "level": logging_level, "serialize": enable_json_logs, "format": formatter, "colorize": True, } ], ) # sentry sdk не умеет из коробки работать с loguru, нужно добавлять хандлер # https://github.com/getsentry/sentry-python/issues/653#issuecomment-788854865 # https://forum.sentry.io/t/changing-issue-title-when-logging-with-traceback/446 if enable_sentry_logs: handler = EventHandler(level=logging.WARNING) logger.add(handler, diagnose=True, level=logging.WARNING, format=_sentry_formatter) def _json_formatter(record: Record) -> str: # Обрезаем `\n` в конце логов, т.к. в json формате переносы не нужны return record.get("message", "").strip() def _sentry_formatter(record: Record) -> str: return "{name}:{function} {message}" def _text_formatter(record: Record) -> str: # WARNING !!! # Функция должна возвращать строку, которая содержит только шаблоны для форматирования. # Если в строку прокидывать значения из record (или еще откуда-либо), # то loguru может принять их за f-строки и попытается обработать, что приведет к ошибке. # Например, если нужно достать какое-то значение из поля extra, вместо того чтобы прокидывать его в строку формата, # нужно прокидывать подстроку вида {extra[тут_ключ]} # Стандартный формат loguru. Задается через env LOGURU_FORMAT format_ = ( "{time:YYYY-MM-DD HH:mm:ss.SSS} | " "{level: <8} | " "{name}:{function}:{line} - {message}" ) # Добавляем мета параметры по типу user_id, art_id, которые передаются через logger.bind(...) extra = record["extra"] if extra: formatted = ", ".join(f"{key}" + "={extra[" + str(key) + "]}" for key, value in extra.items()) format_ += f" - {formatted}" format_ += "\n" if record["exception"] is not None: format_ += "{exception}\n" return format_