feat: added sending tracebacks with telegram

This commit is contained in:
ITQ
2025-02-21 12:02:04 +03:00
parent 96e56df656
commit 9d92bbdc68
5 changed files with 182 additions and 0 deletions
@@ -18,3 +18,5 @@ MINIO_ENDPOINT=minio:9000
MINIO_CUSTOM_ENDPOINT_URL=http://127.0.0.1:13244 MINIO_CUSTOM_ENDPOINT_URL=http://127.0.0.1:13244
MINIO_ACCESS_KEY=admin MINIO_ACCESS_KEY=admin
MINIO_SECRET_KEY=password MINIO_SECRET_KEY=password
DJANGO_NOTIFIER_TELEGRAM_BOT_TOKEN=6196898691:AAFucgj7ieEuYMvWG_MZAn0Ao3UBuHpvaVY
DJANGO_NOTIFIER_TELEGRAM_CHAT_ID=-1002304409222
+9
View File
@@ -31,3 +31,12 @@ DJANGO_CREATE_SUPERUSER=False
DJANGO_SUPERUSER_USERNAME= DJANGO_SUPERUSER_USERNAME=
DJANGO_SUPERUSER_EMAIL= DJANGO_SUPERUSER_EMAIL=
DJANGO_SUPERUSER_PASSWORD= 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=
@@ -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 = (
"<b>{levelname}</b>\n"
"\t<b>Guid:</b> <code>{correlation_id}</code>\n"
"\t<b>Timestamp:</b> <code>{asctime}</code>\n"
"\t<b>Logger:</b> <code>{name}</code>\n"
"\t<b>File:</b> <code>{pathname}</code> "
"(Line: <code>{lineno}</code>)\n\n"
'<pre><code class="language-message">{message}</code></pre>\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<pre><code class='language-traceback'>{exc_text}</code></pre>"
)
@classmethod
def shutdown_executor(cls) -> None:
cls._executor.shutdown(wait=True)
@@ -300,6 +300,26 @@ USE_X_FORWARDED_PORT = False
WSGI_APPLICATION = "config.wsgi.application" 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 # Logging
LOGGER_NAME = "adnova" LOGGER_NAME = "adnova"
@@ -415,6 +435,24 @@ LOGGING = {
LOGGING_CONFIG = "logging.config.dictConfig" 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 # Models
ABSOLUTE_URL_OVERRIDES: dict[str, Callable] = {} ABSOLUTE_URL_OVERRIDES: dict[str, Callable] = {}