| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- import logging
- from logging import Handler, LogRecord
- from logging.handlers import SMTPHandler, BufferingHandler
- from textwrap import dedent
- from typing import List, Optional
- import warnings
- from redmail.email.sender import EmailSender
- class _EmailHandlerMixin:
- def __init__(self, email, kwargs):
- if email is not None:
- # Using copy to prevent modifying the sender
- # if it is used somewhere else
- email = email.copy()
- self.email = email
- self._set_email_kwargs(kwargs)
- else:
- self.set_email(**kwargs)
- self._validate_email()
- def set_email(self,
- host, port,
- username=None, password=None,
- **kwargs):
- "Create a simple default sender"
- if "user_name" in kwargs and username is None:
- warnings.warn("Argument user_name is replaced with username. Please use username instead.", FutureWarning)
- username = kwargs.pop("user_name")
- self.email = EmailSender(
- host=host, port=port,
- username=username, password=password
- )
-
- self._set_email_kwargs(kwargs)
- def get_subject(self, record):
- "Format subject of the email sender"
- return self.email.subject.format(
- record=record, handler=self
- )
- def _set_email_kwargs(self, kwargs:dict):
- for attr, value in kwargs.items():
- if not hasattr(self.email, attr):
- raise AttributeError(f"EmailSender has no attribute {attr}")
- setattr(self.email, attr, value)
- # Set default message body if nothing specified
- has_no_body = (
- self.email.text is None
- and self.email.text_template is None
- and self.email.html is None
- and self.email.html_template is None
- )
- if has_no_body:
- self.email.text = self.default_text
- def _validate_email(self):
- "Validate the email has all required attributes for logging"
- req_attrs = ('host', 'port', 'subject', 'receivers')
- missing = []
- for attr in req_attrs:
- if getattr(self.email, attr) is None:
- missing.append(attr)
- if missing:
- cls_name = type(self).__name__
- raise TypeError(f'{cls_name} email sender missing attributes: {missing}')
- class EmailHandler(_EmailHandlerMixin, Handler):
- """Logging handler for sending a log record as an email
- Parameters
- ----------
- level : int
- Log level of the handler
- email : EmailSender
- Sender instance to be used for sending
- the log records.
- kwargs : dict
- Keyword arguments for creating the
- sender if ``email`` was not passed.
- Examples
- --------
- Minimal example:
- .. code-block:: python
- handler = EmailHandler(
- host="smtp.myhost.com", port=0,
- sender="no-reply@example.com",
- receivers=["me@example.com"],
- )
- Customized example:
- .. code-block:: python
- from redmail import EmailSender
- email = EmailSender(
- host="smtp.myhost.com",
- port=0
- )
- email.email = "no-reply@example.com"
- email.receivers = ["me@example.com"]
- email.html = '''
- <h1>Record: {{ record.levelname }}</h1>
- <pre>{{ record.msg }}</pre>
- <h2>Info</h2>
- <ul>
- <li>Path: {{ record.pathname }}</li>
- <li>Function: {{ record.funcName }}</li>
- <li>Line number: {{ record.lineno }}</li>
- </ul>
- '''
- handler = EmailHandler(email=email)
- import logging
- logger = logging.getLogger()
- logger.addHandler(handler)
- """
- email: EmailSender
- default_text = "{{ msg }}"
- def __init__(self, level:int=logging.NOTSET, email:EmailSender=None, **kwargs):
- _EmailHandlerMixin.__init__(self, email=email, kwargs=kwargs)
- Handler.__init__(self, level)
- def emit(self, record:logging.LogRecord):
- "Emit a record (send email)"
- self.email.send(
- subject=self.get_subject(record),
- body_params={
- "record": record,
- "msg": self.format(record),
- "handler": self,
- }
- )
- class MultiEmailHandler(_EmailHandlerMixin, BufferingHandler):
- """Logging handler for sending multiple log records as an email
- Parameters
- ----------
- capacity : int
- Number of
- email : EmailSender
- Sender instance to be used for sending
- the log records.
- kwargs : dict
- Keyword arguments for creating the
- sender if ``email`` was not passed.
- Examples
- --------
- Minimal example:
- .. code-block:: python
- handler = MultiEmailHandler(
- host="smtp.myhost.com", port=0,
- sender="no-reply@example.com",
- receivers=["me@example.com"],
- )
- Customized example:
- .. code-block:: python
- from redmail import EmailSender
- email = EmailSender(
- host="smtp.myhost.com",
- port=0
- )
- email.sender = "no-reply@example.com"
- email.receivers = ["me@example.com"]
- email.html = '''
- <h1>Record: {{ record.levelname }}</h1>
- <pre>{{ record.msg }}</pre>
- <h2>Info</h2>
- <ul>
- <li>Path: {{ record.pathname }}</li>
- <li>Function: {{ record.funcName }}</li>
- <li>Line number: {{ record.lineno }}</li>
- </ul>
- '''
- handler = EmailHandler(sender=email)
- import logging
- logger = logging.getLogger()
- logger.addHandler(handler)
- """
- default_text = dedent("""
- Log Recods:
- {% for record in records -%}
- {{ handler.format(record) }}
- {% endfor %}""")[1:]
- def __init__(self, capacity:Optional[int]=None, email:EmailSender=None, **kwargs):
- _EmailHandlerMixin.__init__(self, email=email, kwargs=kwargs)
- BufferingHandler.__init__(self, capacity)
- def flush(self):
- "Flush the records (send an email)"
- self.acquire()
- try:
- msgs = []
- for rec in self.buffer:
- # This creates msg, exc_text etc. to the LogRecords
- msgs.append(self.format(rec))
- # For some reason logging does not create this attr unless having asctime in the format string
- if self.formatter is None:
- rec.asctime = logging.Formatter().formatTime(rec)
- self.email.send(
- subject=self.get_subject(self.buffer),
- body_params={
- "records": self.buffer,
- "msgs": msgs,
- "handler": self
- }
- )
- self.buffer = []
- finally:
- self.release()
- def shouldFlush(self, record):
- """Should the handler flush its buffer?
- Returns true if the buffer is up to capacity. This method can be overridden to implement custom flushing strategies.
- """
- if self.capacity is None:
- # Only manual flushing
- return False
- else:
- return super().shouldFlush(record)
- def get_subject(self, records:List[LogRecord]):
- "Get subject of the email"
- if records:
- min_level = min([record.levelno for record in records])
- max_level = max([record.levelno for record in records])
- fmt_kwds = {
- "min_level_name": logging.getLevelName(min_level),
- "max_level_name": logging.getLevelName(max_level),
- }
- else:
- # No log records, getting something
- fmt_kwds = {
- "min_level_name": logging.getLevelName(logging.NOTSET),
- "max_level_name": logging.getLevelName(logging.NOTSET),
- }
- return self.email.subject.format(
- **fmt_kwds,
- handler=self,
- records=records
- )
|