log.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. """Main logging part"""
  2. # Friendly Telegram (telegram userbot)
  3. # Copyright (C) 2018-2021 The Authors
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU Affero General Public License for more details.
  12. # You should have received a copy of the GNU Affero General Public License
  13. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. # █ █ ▀ █▄▀ ▄▀█ █▀█ ▀ ▄▀█ ▀█▀ ▄▀█ █▀▄▀█ ▄▀█
  15. # █▀█ █ █ █ █▀█ █▀▄ █ ▄ █▀█ █ █▀█ █ ▀ █ █▀█
  16. #
  17. # © Copyright 2022
  18. #
  19. # https://t.me/hikariatama
  20. #
  21. # 🔒 Licensed under the GNU GPLv3
  22. # 🌐 https://www.gnu.org/licenses/agpl-3.0.html
  23. import asyncio
  24. import logging
  25. import traceback
  26. import io
  27. from . import utils
  28. from ._types import Module
  29. _formatter = logging.Formatter
  30. class TelegramLogsHandler(logging.Handler):
  31. """
  32. Keeps 2 buffers.
  33. One for dispatched messages.
  34. One for unused messages.
  35. When the length of the 2 together is 100
  36. truncate to make them 100 together,
  37. first trimming handled then unused.
  38. """
  39. def __init__(self, target, capacity: int):
  40. super().__init__(0)
  41. self.target = target
  42. self.capacity = capacity
  43. self.buffer = []
  44. self.handledbuffer = []
  45. self.lvl = logging.WARNING # Default loglevel
  46. self._queue = []
  47. self.tg_buff = ""
  48. self._mods = {}
  49. def install_tg_log(self, mod: Module):
  50. if getattr(self, "_task", False):
  51. self._task.cancel()
  52. self._mods[mod._tg_id] = mod
  53. self._task = asyncio.ensure_future(self.queue_poller())
  54. async def queue_poller(self):
  55. while True:
  56. await self.sender()
  57. await asyncio.sleep(3)
  58. def setLevel(self, level: int):
  59. self.lvl = level
  60. def dump(self):
  61. """Return a list of logging entries"""
  62. return self.handledbuffer + self.buffer
  63. def dumps(self, lvl: int = 0) -> list:
  64. """Return all entries of minimum level as list of strings"""
  65. return [
  66. self.target.format(record)
  67. for record in (self.buffer + self.handledbuffer)
  68. if record.levelno >= lvl
  69. ]
  70. async def sender(self):
  71. self._queue = utils.chunks(utils.escape_html(self.tg_buff), 4096)
  72. self.tg_buff = ""
  73. if len(self._queue) > 5:
  74. for mod in self._mods.values():
  75. file = io.BytesIO("".join(self._queue).encode("utf-8"))
  76. file.name = "hikka-logs.txt"
  77. file.seek(0)
  78. await mod.inline.bot.send_document(
  79. mod._logchat,
  80. file,
  81. parse_mode="HTML",
  82. caption="<b>🧳 Journals are too big to send as separate messages</b>",
  83. )
  84. self._queue = []
  85. return
  86. if not self._queue:
  87. return
  88. chunk = self._queue.pop(0)
  89. if not chunk:
  90. return
  91. for mod in self._mods.values():
  92. await mod.inline.bot.send_message(
  93. mod._logchat,
  94. f"<code>{chunk}</code>",
  95. parse_mode="HTML",
  96. disable_notification=True,
  97. )
  98. def emit(self, record: logging.LogRecord):
  99. if record.exc_info is not None:
  100. exc = (
  101. "\n🚫 Traceback:\n"
  102. + "\n".join(
  103. [
  104. line
  105. for line in traceback.format_exception(*record.exc_info)[1:]
  106. if "hikka/dispatcher.py" not in line
  107. and " await func(message)" not in line
  108. ]
  109. ).strip()
  110. )
  111. else:
  112. exc = ""
  113. if record.levelno >= 20:
  114. try:
  115. self.tg_buff += f"[{record.levelname}] {record.name}: {str(record.msg) % record.args}{exc}\n"
  116. except TypeError:
  117. self.tg_buff += f"[{record.levelname}] {record.name}: {record.msg}\n"
  118. if len(self.buffer) + len(self.handledbuffer) >= self.capacity:
  119. if self.handledbuffer:
  120. del self.handledbuffer[0]
  121. else:
  122. del self.buffer[0]
  123. self.buffer.append(record)
  124. if record.levelno >= self.lvl >= 0:
  125. self.acquire()
  126. try:
  127. for precord in self.buffer:
  128. self.target.handle(precord)
  129. self.handledbuffer = (
  130. self.handledbuffer[-(self.capacity - len(self.buffer)) :]
  131. + self.buffer
  132. )
  133. self.buffer = []
  134. finally:
  135. self.release()
  136. def init():
  137. formatter = _formatter(logging.BASIC_FORMAT, "")
  138. handler = logging.StreamHandler()
  139. handler.setFormatter(formatter)
  140. logging.getLogger().handlers = []
  141. logging.getLogger().addHandler(TelegramLogsHandler(handler, 7000))
  142. logging.getLogger().setLevel(logging.NOTSET)
  143. logging.getLogger("telethon").setLevel(logging.WARNING)
  144. logging.getLogger("matplotlib").setLevel(logging.WARNING)
  145. logging.getLogger("aiohttp").setLevel(logging.WARNING)
  146. logging.getLogger("aiogram").setLevel(logging.WARNING)
  147. logging.captureWarnings(True)