log.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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. # © Copyright 2022
  17. # https://t.me/hikariatama
  18. #
  19. # 🔒 Licensed under the GNU AGPLv3
  20. # 🌐 https://www.gnu.org/licenses/agpl-3.0.html
  21. import asyncio
  22. import inspect
  23. import logging
  24. import io
  25. from typing import Optional
  26. from logging.handlers import RotatingFileHandler
  27. from . import utils
  28. from ._types import Module
  29. _main_formatter = logging.Formatter(
  30. fmt="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
  31. datefmt="%Y-%m-%d %H:%M:%S",
  32. style="%",
  33. )
  34. _tg_formatter = logging.Formatter(
  35. fmt="[%(levelname)s] %(name)s: %(message)s\n",
  36. datefmt=None,
  37. style="%",
  38. )
  39. rotating_handler = RotatingFileHandler(
  40. filename="hikka.log",
  41. mode="a",
  42. maxBytes=10 * 1024 * 1024,
  43. backupCount=1,
  44. encoding="utf-8",
  45. delay=0,
  46. )
  47. rotating_handler.setFormatter(_main_formatter)
  48. class TelegramLogsHandler(logging.Handler):
  49. """
  50. Keeps 2 buffers.
  51. One for dispatched messages.
  52. One for unused messages.
  53. When the length of the 2 together is 100
  54. truncate to make them 100 together,
  55. first trimming handled then unused.
  56. """
  57. def __init__(self, targets: list, capacity: int):
  58. super().__init__(0)
  59. self.targets = targets
  60. self.capacity = capacity
  61. self.buffer = []
  62. self.handledbuffer = []
  63. self.lvl = logging.NOTSET # Default loglevel
  64. self._queue = []
  65. self.tg_buff = []
  66. self._mods = {}
  67. self.force_send_all = False
  68. def install_tg_log(self, mod: Module):
  69. if getattr(self, "_task", False):
  70. self._task.cancel()
  71. self._mods[mod._tg_id] = mod
  72. self._task = asyncio.ensure_future(self.queue_poller())
  73. async def queue_poller(self):
  74. while True:
  75. await self.sender()
  76. await asyncio.sleep(3)
  77. def setLevel(self, level: int):
  78. self.lvl = level
  79. def dump(self):
  80. """Return a list of logging entries"""
  81. return self.handledbuffer + self.buffer
  82. def dumps(self, lvl: Optional[int] = 0, client_id: Optional[int] = None) -> list:
  83. """Return all entries of minimum level as list of strings"""
  84. return [
  85. self.targets[0].format(record)
  86. for record in (self.buffer + self.handledbuffer)
  87. if record.levelno >= lvl
  88. and (not record.hikka_caller or client_id == record.hikka_caller)
  89. ]
  90. async def sender(self):
  91. self._queue = {
  92. client_id: utils.chunks(
  93. utils.escape_html(
  94. "".join(
  95. [
  96. item[0]
  97. for item in self.tg_buff
  98. if not item[1]
  99. or item[1] == client_id
  100. or self.force_send_all
  101. ]
  102. )
  103. ),
  104. 4096,
  105. )
  106. for client_id in self._mods
  107. }
  108. self.tg_buff = []
  109. for client_id in self._mods:
  110. if client_id not in self._queue:
  111. continue
  112. if len(self._queue[client_id]) > 5:
  113. file = io.BytesIO("".join(self._queue[client_id]).encode("utf-8"))
  114. file.name = "hikka-logs.txt"
  115. file.seek(0)
  116. await self._mods[client_id].inline.bot.send_document(
  117. self._mods[client_id]._logchat,
  118. file,
  119. parse_mode="HTML",
  120. caption="<b>🧳 Journals are too big to be sent as separate messages</b>",
  121. )
  122. self._queue[client_id] = []
  123. continue
  124. while self._queue[client_id]:
  125. chunk = self._queue[client_id].pop(0)
  126. if not chunk:
  127. continue
  128. asyncio.ensure_future(
  129. self._mods[client_id].inline.bot.send_message(
  130. self._mods[client_id]._logchat,
  131. f"<code>{chunk}</code>",
  132. parse_mode="HTML",
  133. disable_notification=True,
  134. )
  135. )
  136. def emit(self, record: logging.LogRecord):
  137. try:
  138. caller = next(
  139. (
  140. frame_info.frame.f_locals["_hikka_client_id_logging_tag"]
  141. for frame_info in inspect.stack()
  142. if isinstance(
  143. getattr(getattr(frame_info, "frame", None), "f_locals", {}).get(
  144. "_hikka_client_id_logging_tag"
  145. ),
  146. int,
  147. )
  148. ),
  149. False,
  150. )
  151. if not isinstance(caller, int):
  152. caller = None
  153. except Exception:
  154. caller = None
  155. record.hikka_caller = caller
  156. if record.levelno >= 20:
  157. self.tg_buff += [
  158. (
  159. ("🚫 " if record.exc_info else "") + _tg_formatter.format(record),
  160. caller,
  161. )
  162. ]
  163. if len(self.buffer) + len(self.handledbuffer) >= self.capacity:
  164. if self.handledbuffer:
  165. del self.handledbuffer[0]
  166. else:
  167. del self.buffer[0]
  168. self.buffer.append(record)
  169. if record.levelno >= self.lvl >= 0:
  170. self.acquire()
  171. try:
  172. for precord in self.buffer:
  173. for target in self.targets:
  174. if record.levelno >= target.level:
  175. target.handle(precord)
  176. self.handledbuffer = (
  177. self.handledbuffer[-(self.capacity - len(self.buffer)) :]
  178. + self.buffer
  179. )
  180. self.buffer = []
  181. finally:
  182. self.release()
  183. def init():
  184. handler = logging.StreamHandler()
  185. handler.setLevel(logging.INFO)
  186. handler.setFormatter(_main_formatter)
  187. logging.getLogger().handlers = []
  188. logging.getLogger().addHandler(
  189. TelegramLogsHandler((handler, rotating_handler), 7000)
  190. )
  191. logging.getLogger().setLevel(logging.NOTSET)
  192. logging.getLogger("telethon").setLevel(logging.WARNING)
  193. logging.getLogger("matplotlib").setLevel(logging.WARNING)
  194. logging.getLogger("aiohttp").setLevel(logging.WARNING)
  195. logging.getLogger("aiogram").setLevel(logging.WARNING)
  196. logging.captureWarnings(True)