feat: added sending tracebacks with telegram
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
# 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] = {}
|
||||
|
||||
Reference in New Issue
Block a user