1
0

log.py 8.0 KB

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