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"
'
\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)