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_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
|
||||||
|
|||||||
@@ -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] = {}
|
||||||
|
|||||||
Reference in New Issue
Block a user