From 9d92bbdc68bd2b8862821457775bb4d318d0f2d2 Mon Sep 17 00:00:00 2001 From: ITQ Date: Fri, 21 Feb 2025 12:02:04 +0300 Subject: [PATCH] feat: added sending tracebacks with telegram --- solution/infrastructure/backend/.env.template | 2 + solution/services/backend/.env.template | 9 ++ .../backend/config/notifiers/__init__.py | 0 .../backend/config/notifiers/telegram.py | 133 ++++++++++++++++++ solution/services/backend/config/settings.py | 38 +++++ 5 files changed, 182 insertions(+) create mode 100644 solution/services/backend/config/notifiers/__init__.py create mode 100644 solution/services/backend/config/notifiers/telegram.py diff --git a/solution/infrastructure/backend/.env.template b/solution/infrastructure/backend/.env.template index 4338d62..e219fb4 100644 --- a/solution/infrastructure/backend/.env.template +++ b/solution/infrastructure/backend/.env.template @@ -18,3 +18,5 @@ MINIO_ENDPOINT=minio:9000 MINIO_CUSTOM_ENDPOINT_URL=http://127.0.0.1:13244 MINIO_ACCESS_KEY=admin MINIO_SECRET_KEY=password +DJANGO_NOTIFIER_TELEGRAM_BOT_TOKEN=6196898691:AAFucgj7ieEuYMvWG_MZAn0Ao3UBuHpvaVY +DJANGO_NOTIFIER_TELEGRAM_CHAT_ID=-1002304409222 diff --git a/solution/services/backend/.env.template b/solution/services/backend/.env.template index 5988a69..378245d 100644 --- a/solution/services/backend/.env.template +++ b/solution/services/backend/.env.template @@ -31,3 +31,12 @@ DJANGO_CREATE_SUPERUSER=False DJANGO_SUPERUSER_USERNAME= DJANGO_SUPERUSER_EMAIL= DJANGO_SUPERUSER_PASSWORD= + + +# Notifiers settings (only with DJANGO_DEBUG=False) + +# Telegram + +DJANGO_NOTIFIER_TELEGRAM_BOT_TOKEN= +DJANGO_NOTIFIER_TELEGRAM_CHAT_ID= +DJANGO_NOTIFIER_TELEGRAM_THREAD_ID= diff --git a/solution/services/backend/config/notifiers/__init__.py b/solution/services/backend/config/notifiers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solution/services/backend/config/notifiers/telegram.py b/solution/services/backend/config/notifiers/telegram.py new file mode 100644 index 0000000..6ca3a65 --- /dev/null +++ b/solution/services/backend/config/notifiers/telegram.py @@ -0,0 +1,133 @@ +import datetime +import logging +import time +import traceback +from concurrent.futures import ThreadPoolExecutor + +import httpx +from django.utils.timezone import get_current_timezone + +TELEGRAM_LOG_HANDLER = logging.getLogger("telegram_log_handler") + +LEVEL_EMOJIS = { + "DEBUG": "🐞", + "INFO": "â„šī¸", + "WARNING": "âš ī¸", + "ERROR": "❌", + "CRITICAL": "🚨", +} + + +class LoggingHandler(logging.Handler): + _executor = ThreadPoolExecutor(max_workers=5) + + def __init__( + self, + token: str, + chat_id: int, + thread_id: int | None = None, + retries: int | None = 3, + delay: int | None = 2, + timeout: int | None = 5, + ) -> None: + super().__init__() + + self.token = token + self.chat_id = chat_id + self.thread_id = thread_id + self.retries = retries + self.delay = delay + self.timeout = timeout + self.api_url = f"https://api.telegram.org/bot{self.token}/sendMessage" + + self.template = ( + "{levelname}\n" + "\tGuid: {correlation_id}\n" + "\tTimestamp: {asctime}\n" + "\tLogger: {name}\n" + "\tFile: {pathname} " + "(Line: {lineno})\n\n" + '
{message}
\n' + ) + + def emit(self, record: logging.LogRecord) -> None: + try: + formatted_record = self.format(record) + self._executor.submit(self._send_message, formatted_record) + except Exception as e: # noqa: BLE001 + self.handleError(record) + TELEGRAM_LOG_HANDLER.exception(e) + + def _send_message(self, formatted_record: str) -> None: + payload = { + "chat_id": self.chat_id, + "text": formatted_record, + "parse_mode": "HTML", + } + if self.thread_id: + payload["reply_to_message_id"] = self.thread_id + + for attempt in range(1, self.retries + 1): + response = httpx.post( + self.api_url, + data=payload, + timeout=self.timeout, + ) + if response.status_code != httpx.codes.OK: + if attempt == self.retries: + TELEGRAM_LOG_HANDLER.exception( + "Failed to send to Telegram after %d attempts: %s", + self.retries, + response.text, + ) + else: + time.sleep(self.delay) + else: + return + + def format(self, record: logging.LogRecord) -> str: + try: + asctime = datetime.datetime.fromtimestamp( + record.created, + tz=get_current_timezone(), + ).strftime("%Y-%m-%d %H:%M:%S %Z") + level_emoji = LEVEL_EMOJIS.get(record.levelname, "") + + formatted_message = self.template.format( + levelname=f"{level_emoji} {record.levelname}", + correlation_id=getattr(record, "correlation_id", "N/A"), + asctime=asctime, + name=record.name, + pathname=record.pathname, + lineno=record.lineno, + message=record.getMessage(), + ) + + if record.exc_info: + formatted_message += self._format_exception(record.exc_info) + + formatted_message += ( + f"\n#{record.levelname.lower()} " + f"#{record.name.replace('.', '_')}" + ) + if hasattr(record, "correlation_id"): + formatted_message += f" #{record.correlation_id}" + except Exception as format_error: # noqa: BLE001 + TELEGRAM_LOG_HANDLER.exception( + "Error formatting log record: %s", + format_error, + ) + return f"Error formatting log record: {format_error}" + else: + return formatted_message + + @staticmethod + def _format_exception(exc_info: Exception) -> str: + exc_text = "".join(traceback.format_exception(*exc_info)) + return ( + f"\n
{exc_text}
" + ) + + @classmethod + def shutdown_executor(cls) -> None: + cls._executor.shutdown(wait=True) diff --git a/solution/services/backend/config/settings.py b/solution/services/backend/config/settings.py index 140f1f1..122b846 100644 --- a/solution/services/backend/config/settings.py +++ b/solution/services/backend/config/settings.py @@ -300,6 +300,26 @@ USE_X_FORWARDED_PORT = False WSGI_APPLICATION = "config.wsgi.application" +# Notifiers + +# Telegram + +NOTIFIER_TELEGRAM_BOT_TOKEN = env( + "DJANGO_NOTIFIER_TELEGRAM_BOT_TOKEN", + default=None, +) + +NOTIFIER_TELEGRAM_CHAT_ID = env( + "DJANGO_NOTIFIER_TELEGRAM_CHAT_ID", + default=None, +) + +NOTIFIER_TELEGRAM_THREAD_ID = env( + "DJANGO_NOTIFIER_TELEGRAM_THREAD_ID", + default=None, +) + + # Logging LOGGER_NAME = "adnova" @@ -415,6 +435,24 @@ LOGGING = { LOGGING_CONFIG = "logging.config.dictConfig" +if NOTIFIER_TELEGRAM_BOT_TOKEN and NOTIFIER_TELEGRAM_CHAT_ID: + LOGGING_HANDLERS["telegram"] = { + "class": "config.notifiers.telegram.LoggingHandler", + "level": "INFO", + "filters": ["require_debug_false"], + "token": NOTIFIER_TELEGRAM_BOT_TOKEN, + "chat_id": NOTIFIER_TELEGRAM_CHAT_ID, + "thread_id": NOTIFIER_TELEGRAM_THREAD_ID, + "retries": 5, + "delay": 2, + "timeout": 5, + } + LOGGING_LOGGERS["django"]["handlers"].append("telegram") + LOGGING_LOGGERS["django.request"]["handlers"].append("telegram") + LOGGING_LOGGERS["health-check"]["handlers"].append("telegram") + LOGGING_LOGGERS[LOGGER_NAME]["handlers"].append("telegram") + + # Models ABSOLUTE_URL_OVERRIDES: dict[str, Callable] = {}