MailingLogger
=============

.. note:: 

  Throughout the examples below, mail messages sent using
  :mod:`smtplib` are printed to the screen so we can see what's going on:

  >>> import smtplib
  >>> server = smtplib.SMTP('localhost')
  >>> server.sendmail('from@example.com', ['to@example.com'], 'The message')
  sending to ['to@example.com'] from 'from@example.com' using ('localhost', 25)
  The message

.. currentmodule:: mailinglogger

:class:`MailingLogger` is a handler for the python logging framework that
sends log entries as email messages using an SMTP server. It is
configured as any other :mod:`logging` handler would be, full details
of which can be found in the `Python core documentation`__. For the
examples below, we'll stick to manually configuring the logging
elements.
 
__ http://docs.python.org/howto/logging.html#configuring-logging

A :class:`MailingLogger` is instantiated as follows:

>>> from mailinglogger import MailingLogger
>>> handler = MailingLogger('from@example.com',('to@example.com',))

It can then be added as a handler for any logger as follows:

>>> import logging
>>> logger = logging.getLogger()
>>> logger.addHandler(handler)

Now, when that logger receives a message, an email containing the
message will be sent:

>>> logging.error('my message')
sending to ('to@example.com',) from 'from@example.com' using ('localhost', 25)
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="us-ascii"
Date: ...
From: from@example.com
MIME-Version: 1.0
Message-ID: <...MailingLogger@...>
Subject: my message
To: to@example.com
X-Log-Level: ERROR
X-Mailer: MailingLogger...
<BLANKLINE>
my message

From the above example, you can see that :class:`MailingLogger` sends
mail messages that are correctly formatted, including ``Date`` and
``Message-ID`` headers.

You will also notice that an ``X-Mailer`` header has been added
specifying that :mod:`mailinglogger` is the sender of the mail.
An ``X-Log-Level`` header has also been added indicating the level of
the message that was logged. These can be useful for filtering mail
sent by :class:`MailingLogger`. If you wish to filter mail by
environment or other configuration data, the support for adding
:ref:`extra headers <mail_extra_headers>` may be useful.

Now, to continue with the examples, just like any other handler, we
can also set the logging level, and messages logged below this level
will not result in emails being sent: 

>>> handler.setLevel(logging.CRITICAL)
>>> logging.error('my message')
>>> handler.setLevel(logging.WARNING)
>>> logging.warning('my message')
sending to ('to@example.com',) from 'from@example.com' using ('localhost', 25)
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="us-ascii"
Date: ...
From: from@example.com
MIME-Version: 1.0
Message-ID: <...MailingLogger@...>
Subject: my message
To: to@example.com
X-Log-Level: WARNING
X-Mailer: MailingLogger...
<BLANKLINE>
my message

Controlling the subject line
----------------------------

As you can see from the above examples, the subject line of the
email sent is, by default, the first line of the message logged.
This can be changed by supplying the `subject` parameter when
instantiating the handler object:

.. invisible-code-block: python

  logging.getLogger('').removeHandler(handler)

>>> handler = MailingLogger('from@example.com',('to@example.com',),
...                         subject='[MyLogger] %(line)s')
>>> logger.addHandler(handler)
>>> logging.error('my %i message',13)
sending to ('to@example.com',) from 'from@example.com' using ('localhost', 25)
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="us-ascii"
Date: ...
From: from@example.com
MIME-Version: 1.0
Message-ID: <...MailingLogger@...>
Subject: [MyLogger] my 13 message
To: to@example.com
X-Log-Level: ERROR
X-Mailer: MailingLogger...
<BLANKLINE>
my 13 message

Full details of how the subject line can be formatted can be found in
the :doc:`subjectformatter` documentation.

Sending empty emails
--------------------

By default, the :class:`MailingLogger` handler will not send emails if
they would have been empty:

>>> logging.error(' ')

However, if you want empty entries to be mailed anyway, all you need
to do is supply the `send_empty_entries` parameter:

.. invisible-code-block: python

  logging.getLogger('').removeHandler(handler)

>>> handler = MailingLogger('from@example.com',('to@example.com',),
...                         send_empty_entries=True)
>>> logger.addHandler(handler)
>>> logging.error(' ')
sending to ('to@example.com',) from 'from@example.com' using ('localhost', 25)
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="us-ascii"
Date: ...
From: from@example.com
MIME-Version: 1.0
Message-ID: <...MailingLogger@...>
Subject: ...
To: to@example.com
X-Log-Level: ERROR
X-Mailer: MailingLogger...
<BLANKLINE>
<BLANKLINE>
  
Limiting the number of emails sent
----------------------------------

Now, one problem that may be encountered with a logger that sends
emails is that if you inadvertantly log a large number of entries
that would result in mail being sent, you may cause problems with
MTAs, mailbox quotas and the like.

To prevent this, :class:`MailingLogger` allows a limit on the number of
entries sent per hour to be specified. By default, this is set to 10
entries per hour. This can be overridden by passing the `flood_level`
option to the :class:`MailingLogger` constructor:

.. invisible-code-block: python

  logging.getLogger('').removeHandler(handler)

>>> handler = MailingLogger('from@example.com',('to@example.com',),
...                         flood_level=1)
>>> logger.addHandler(handler)

With this setup, we can log at most one message:

>>> logging.error('An Error')
sending to ('to@example.com',) from 'from@example.com' using ('localhost', 25)
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="us-ascii"
Date: Mon, 01 Jan 2007 10:00:00 -0000
From: from@example.com
MIME-Version: 1.0
Message-ID: <...MailingLogger@...>
Subject: An Error
To: to@example.com
X-Log-Level: ERROR
X-Mailer: MailingLogger...
<BLANKLINE>
An Error

Now that the flood level has been reached, a final warning message is
sent if any more messages are logged:

>>> logging.error('Another Error')
sending to ('to@example.com',) from 'from@example.com' using ('localhost', 25)
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="us-ascii"
Date: Mon, 01 Jan 2007 10:00:00 -0000
From: from@example.com
MIME-Version: 1.0
Message-ID: <...MailingLogger@...>
Subject: Too Many Log Entries
To: to@example.com
X-Log-Level: CRITICAL
X-Mailer: MailingLogger...
<BLANKLINE>
Too Many Log Entries
<BLANKLINE>
More than 1 entries have been logged that would have resulted in
emails being sent.
<BLANKLINE>
No further emails will be sent for log entries generated between
10:00:00 and 11:00:00
<BLANKLINE>
Please consult any other configured logs, such as a File Logger,
that may contain important entries that have not been emailed.
<BLANKLINE>

Any further messages logged will not result in an email being sent:

>>> logging.error('Yet Another Error')

..  However, once the clock has ticked over to a new hour, messages
    logged will once again be mailed:

    >>> datetime.set(2007, 1, 1, 11)
    >>> time.set(2007, 1, 1, 11)

    >>> logging.error('Yet Another Error')
    sending to ('to@example.com',) from 'from@example.com' using ('localhost', 25)
    Content-Transfer-Encoding: 7bit
    Content-Type: text/plain; charset="us-ascii"
    Date: Mon, 01 Jan 2007 11:00:00 -0000
    From: from@example.com
    MIME-Version: 1.0
    Message-ID: <...MailingLogger@...>
    Subject: Yet Another Error
    To: to@example.com
    X-Log-Level: ERROR
    X-Mailer: MailingLogger...
    <BLANKLINE>
    Yet Another Error

Specifying the host to send email through
-----------------------------------------

By default, as we've seen above, :class:`MailingLogger` uses the local host
to send mails. If you wish to use a specific smtp server to send
mail, this can be done by specifying the `mailhost` parameter to the
:class:`MailingLogger` constructor:

.. invisible-code-block: python

  logging.getLogger('').removeHandler(handler)

>>> handler = MailingLogger('from@example.com',('to@example.com',),
...                         mailhost='smtp.example.com')
>>> logger.addHandler(handler)
>>> logging.error('An Error')
sending to ('to@example.com',) from 'from@example.com' using ('smtp.example.com', 25)
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="us-ascii"
Date: ...
From: from@example.com
MIME-Version: 1.0
Message-ID: <...MailingLogger@...>
Subject: An Error
To: to@example.com
X-Log-Level: ERROR
X-Mailer: MailingLogger...
<BLANKLINE>
An Error

If the smtp server you wish to use is running on non-standard port,
you can configure :class:`MailingLogger` to use this port by specifying
`mailhost` as a tuple containing the smtp server's hostname and the
port on which it is listening:

.. invisible-code-block: python

  logging.getLogger('').removeHandler(handler)

>>> handler = MailingLogger('from@example.com',('to@example.com',),
...                         mailhost=('smtp.example.com',2500))
>>> logger.addHandler(handler)
>>> logging.error('An Error')
sending to ('to@example.com',) from 'from@example.com' using ('smtp.example.com', 2500)
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="us-ascii"
Date: ...
From: from@example.com
MIME-Version: 1.0
Message-ID: <...MailingLogger@...>
Subject: An Error
To: to@example.com
X-Log-Level: ERROR
X-Mailer: MailingLogger...
<BLANKLINE>
An Error

If the smtp server you wish to use requires authentication,
pass the required username and password to the :class:`MailingLogger`
constructor: 

.. invisible-code-block: python

  logging.getLogger('').removeHandler(handler)

>>> handler = MailingLogger('from@example.com',('to@example.com',),
...                         username='auser',password='theirpassword')
>>> logger.addHandler(handler)
>>> logging.error('An Error')
sending to ('to@example.com',) from 'from@example.com' using ('localhost', 25)
(authenticated using username:'auser' and password:'theirpassword')
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="us-ascii"
Date: ...
From: from@example.com
MIME-Version: 1.0
Message-ID: <...MailingLogger@...>
Subject: An Error
To: to@example.com
X-Log-Level: ERROR
X-Mailer: MailingLogger...
<BLANKLINE>
An Error

.. warning::

  For performance reasons, it's recommended that you don't use SMTP
  authentication unless you absolutely need to.

If the smtp server you wish to use requires TLS (Transport Level Security),
pass the required username and password and the secure parameter to the
:class:`MailingLogger` constructor. ``secure`` must be an empty tuple or
contain one or two members. See the `smtplib`__ documentation for details:

__ https://docs.python.org/3/library/smtplib.html

.. invisible-code-block: python

  logging.getLogger('').removeHandler(handler)

>>> handler = MailingLogger('from@example.com',('to@example.com',),
...                         username='auser',password='theirpassword',
...                         secure=())
>>> logger.addHandler(handler)
>>> logging.error('An Error')
sending to ('to@example.com',) from 'from@example.com' using ('localhost', 25)
(authenticated using username:'auser' and password:'theirpassword')
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="us-ascii"
Date: ...
From: from@example.com
MIME-Version: 1.0
Message-ID: <...MailingLogger@...>
Subject: An Error
To: to@example.com
X-Log-Level: ERROR
X-Mailer: MailingLogger...
<BLANKLINE>
An Error

.. _mail_extra_headers:

Adding extra headers
--------------------

If you wish to add headers for filtering purposes, you can use the
headers parameter:

>>> logging.getLogger('').removeHandler(handler)
>>> handler = MailingLogger('from@example.com',('to@example.com',),
...                             headers={'foo':'bar','Baz':'bob'})
>>> logger.addHandler(handler)

Now, when a log message results in an email being send, the email will
be sent with the configured headers:

>>> logging.error('The Error!')
sending to ('to@example.com',) from 'from@example.com' using ('localhost', 25)
Baz: bob
Content-Transfer-Encoding: 7bit
Content-Type: text/plain; charset="us-ascii"
Date: ...
From: from@example.com
MIME-Version: 1.0
Message-ID: <...MailingLogger@...>
Subject: The Error!
To: to@example.com
X-Log-Level: ERROR
X-Mailer: MailingLogger ...
foo: bar
<BLANKLINE>
The Error!
