log.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import logging
  2. from logging import Handler, LogRecord
  3. from logging.handlers import SMTPHandler, BufferingHandler
  4. from textwrap import dedent
  5. from typing import List, Optional
  6. import warnings
  7. from redmail.email.sender import EmailSender
  8. class _EmailHandlerMixin:
  9. def __init__(self, email, kwargs):
  10. if email is not None:
  11. # Using copy to prevent modifying the sender
  12. # if it is used somewhere else
  13. email = email.copy()
  14. self.email = email
  15. self._set_email_kwargs(kwargs)
  16. else:
  17. self.set_email(**kwargs)
  18. self._validate_email()
  19. def set_email(self,
  20. host, port,
  21. username=None, password=None,
  22. **kwargs):
  23. "Create a simple default sender"
  24. if "user_name" in kwargs and username is None:
  25. warnings.warn("Argument user_name is replaced with username. Please use username instead.", FutureWarning)
  26. username = kwargs.pop("user_name")
  27. self.email = EmailSender(
  28. host=host, port=port,
  29. username=username, password=password
  30. )
  31. self._set_email_kwargs(kwargs)
  32. def get_subject(self, record):
  33. "Format subject of the email sender"
  34. return self.email.subject.format(
  35. record=record, handler=self
  36. )
  37. def _set_email_kwargs(self, kwargs:dict):
  38. for attr, value in kwargs.items():
  39. if not hasattr(self.email, attr):
  40. raise AttributeError(f"EmailSender has no attribute {attr}")
  41. setattr(self.email, attr, value)
  42. # Set default message body if nothing specified
  43. has_no_body = (
  44. self.email.text is None
  45. and self.email.text_template is None
  46. and self.email.html is None
  47. and self.email.html_template is None
  48. )
  49. if has_no_body:
  50. self.email.text = self.default_text
  51. def _validate_email(self):
  52. "Validate the email has all required attributes for logging"
  53. req_attrs = ('host', 'port', 'subject', 'receivers')
  54. missing = []
  55. for attr in req_attrs:
  56. if getattr(self.email, attr) is None:
  57. missing.append(attr)
  58. if missing:
  59. cls_name = type(self).__name__
  60. raise TypeError(f'{cls_name} email sender missing attributes: {missing}')
  61. class EmailHandler(_EmailHandlerMixin, Handler):
  62. """Logging handler for sending a log record as an email
  63. Parameters
  64. ----------
  65. level : int
  66. Log level of the handler
  67. email : EmailSender
  68. Sender instance to be used for sending
  69. the log records.
  70. kwargs : dict
  71. Keyword arguments for creating the
  72. sender if ``email`` was not passed.
  73. Examples
  74. --------
  75. Minimal example:
  76. .. code-block:: python
  77. handler = EmailHandler(
  78. host="smtp.myhost.com", port=0,
  79. sender="no-reply@example.com",
  80. receivers=["me@example.com"],
  81. )
  82. Customized example:
  83. .. code-block:: python
  84. from redmail import EmailSender
  85. email = EmailSender(
  86. host="smtp.myhost.com",
  87. port=0
  88. )
  89. email.email = "no-reply@example.com"
  90. email.receivers = ["me@example.com"]
  91. email.html = '''
  92. <h1>Record: {{ record.levelname }}</h1>
  93. <pre>{{ record.msg }}</pre>
  94. <h2>Info</h2>
  95. <ul>
  96. <li>Path: {{ record.pathname }}</li>
  97. <li>Function: {{ record.funcName }}</li>
  98. <li>Line number: {{ record.lineno }}</li>
  99. </ul>
  100. '''
  101. handler = EmailHandler(email=email)
  102. import logging
  103. logger = logging.getLogger()
  104. logger.addHandler(handler)
  105. """
  106. email: EmailSender
  107. default_text = "{{ msg }}"
  108. def __init__(self, level:int=logging.NOTSET, email:EmailSender=None, **kwargs):
  109. _EmailHandlerMixin.__init__(self, email=email, kwargs=kwargs)
  110. Handler.__init__(self, level)
  111. def emit(self, record:logging.LogRecord):
  112. "Emit a record (send email)"
  113. self.email.send(
  114. subject=self.get_subject(record),
  115. body_params={
  116. "record": record,
  117. "msg": self.format(record),
  118. "handler": self,
  119. }
  120. )
  121. class MultiEmailHandler(_EmailHandlerMixin, BufferingHandler):
  122. """Logging handler for sending multiple log records as an email
  123. Parameters
  124. ----------
  125. capacity : int
  126. Number of
  127. email : EmailSender
  128. Sender instance to be used for sending
  129. the log records.
  130. kwargs : dict
  131. Keyword arguments for creating the
  132. sender if ``email`` was not passed.
  133. Examples
  134. --------
  135. Minimal example:
  136. .. code-block:: python
  137. handler = MultiEmailHandler(
  138. host="smtp.myhost.com", port=0,
  139. sender="no-reply@example.com",
  140. receivers=["me@example.com"],
  141. )
  142. Customized example:
  143. .. code-block:: python
  144. from redmail import EmailSender
  145. email = EmailSender(
  146. host="smtp.myhost.com",
  147. port=0
  148. )
  149. email.sender = "no-reply@example.com"
  150. email.receivers = ["me@example.com"]
  151. email.html = '''
  152. <h1>Record: {{ record.levelname }}</h1>
  153. <pre>{{ record.msg }}</pre>
  154. <h2>Info</h2>
  155. <ul>
  156. <li>Path: {{ record.pathname }}</li>
  157. <li>Function: {{ record.funcName }}</li>
  158. <li>Line number: {{ record.lineno }}</li>
  159. </ul>
  160. '''
  161. handler = EmailHandler(sender=email)
  162. import logging
  163. logger = logging.getLogger()
  164. logger.addHandler(handler)
  165. """
  166. default_text = dedent("""
  167. Log Recods:
  168. {% for record in records -%}
  169. {{ handler.format(record) }}
  170. {% endfor %}""")[1:]
  171. def __init__(self, capacity:Optional[int]=None, email:EmailSender=None, **kwargs):
  172. _EmailHandlerMixin.__init__(self, email=email, kwargs=kwargs)
  173. BufferingHandler.__init__(self, capacity)
  174. def flush(self):
  175. "Flush the records (send an email)"
  176. self.acquire()
  177. try:
  178. msgs = []
  179. for rec in self.buffer:
  180. # This creates msg, exc_text etc. to the LogRecords
  181. msgs.append(self.format(rec))
  182. # For some reason logging does not create this attr unless having asctime in the format string
  183. if self.formatter is None:
  184. rec.asctime = logging.Formatter().formatTime(rec)
  185. self.email.send(
  186. subject=self.get_subject(self.buffer),
  187. body_params={
  188. "records": self.buffer,
  189. "msgs": msgs,
  190. "handler": self
  191. }
  192. )
  193. self.buffer = []
  194. finally:
  195. self.release()
  196. def shouldFlush(self, record):
  197. """Should the handler flush its buffer?
  198. Returns true if the buffer is up to capacity. This method can be overridden to implement custom flushing strategies.
  199. """
  200. if self.capacity is None:
  201. # Only manual flushing
  202. return False
  203. else:
  204. return super().shouldFlush(record)
  205. def get_subject(self, records:List[LogRecord]):
  206. "Get subject of the email"
  207. if records:
  208. min_level = min([record.levelno for record in records])
  209. max_level = max([record.levelno for record in records])
  210. fmt_kwds = {
  211. "min_level_name": logging.getLevelName(min_level),
  212. "max_level_name": logging.getLevelName(max_level),
  213. }
  214. else:
  215. # No log records, getting something
  216. fmt_kwds = {
  217. "min_level_name": logging.getLevelName(logging.NOTSET),
  218. "max_level_name": logging.getLevelName(logging.NOTSET),
  219. }
  220. return self.email.subject.format(
  221. **fmt_kwds,
  222. handler=self,
  223. records=records
  224. )