Skip to content

gh-93162: Add ability to configure QueueHandler/QueueListener together and provide getHandlerByName() and getHandlerNames() APIs. #93269

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 7, 2022
55 changes: 55 additions & 0 deletions Doc/library/logging.config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,61 @@ it with :func:`staticmethod`. For example::
You don't need to wrap with :func:`staticmethod` if you're setting the import
callable on a configurator *instance*.

.. _configure-queue:

Configuring QueueHandler and QueueListener
""""""""""""""""""""""""""""""""""""""""""

If you want to configure a :class:`~logging.handlers.QueueHandler`, noting that this
is normally used in conjunction with a :class:`~logging.handlers.QueueListener`, you
can configure both together. After the configuration, the ``QueueListener`` instance
will be available as the :attr:`listener` attribute of the created handler, and that
in turn will be available to you using :func:`~logging.getHandlerByName` and passing
whatever name you have used for the ``QueueHandler`` in your configuration. The
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
whatever name you have used for the ``QueueHandler`` in your configuration. The
the name you have used for the ``QueueHandler`` in your configuration. The

dictionary schema for configuring the pair is shown in the example YAML snippet below.

.. code-block:: yaml

handlers:
qhand:
class: logging.handlers.QueueHandler
queue: my.module.queuefactory
listener: my.package.CustomListener
handlers:
- hand_name_1
- hand_name_2
...

The ``queue`` and ``listener`` keys are optional.

If the ``queue`` key is present, the corresponding value can be one of the following:

* An actual instance of :class:`queue.Queue` or a subclass thereof. This is of course
only possible if you are constructing or modifying the configuration dictionary in
code.

* A string that resolves to a callable which, when called with no arguments, returns
the :class:`queue.Queue` instance to use. That callable could be a
:class:`queue.Queue` subclass or a function which returns a suitable queue instance.

If the ``queue`` key is absent, a standard unbounded :class:`queue.Queue` instance is
created and used.

If the ``listener`` key is present, the corresponding value can be one of the following:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You say "one of the following" and then there is only one listed?


* A subclass of :class:`logging.handlers.QueueListener`. This is of course only
possible if you are constructing or modifying the configuration dictionary in
code.

If the ``listener`` key is absent, :class:`logging.handlers.QueueListener` is used.

The values under the ``handlers`` key are the names of other handlers in the
configuration (not shown in the above snippet) which will be passed to the queue
listener.

Any custom queue handler and listener classes will need to deal with the same
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Any custom queue handler and listener classes will need to deal with the same
Any custom queue handler and listener classes will need be defined with the same

initialization signatures as :class:`~logging.handlers.QueueHandler` and
:class:`~logging.handlers.QueueListener`.

.. _logging-config-fileformat:

Expand Down
6 changes: 6 additions & 0 deletions Doc/library/logging.handlers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1051,7 +1051,13 @@ possible, while any potentially slow operations (such as sending an email via
want to override this if you want to use blocking behaviour, or a
timeout, or a customized queue implementation.

.. attribute:: listener

When created via configuration using :func:`~logging.config.dictConfig`, this
attribute will contain a :class:`QueueListener` instance for use with this
handler. Otherwise, it will be ``None``.

.. versionadded:: 3.12

.. _queue-listener:

Expand Down
7 changes: 7 additions & 0 deletions Doc/library/logging.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,13 @@ functions.
This undocumented behaviour was considered a mistake, and was removed in
Python 3.4, but reinstated in 3.4.2 due to retain backward compatibility.

.. function:: getHandlerByName(name)

Returns a handler with the specified *name*, or ``None`` if there is no handler
with that name.

.. versionadded:: 3.12

.. function:: makeLogRecord(attrdict)

Creates and returns a new :class:`LogRecord` instance whose attributes are
Expand Down
12 changes: 11 additions & 1 deletion Lib/logging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
'exception', 'fatal', 'getLevelName', 'getLogger', 'getLoggerClass',
'info', 'log', 'makeLogRecord', 'setLoggerClass', 'shutdown',
'warn', 'warning', 'getLogRecordFactory', 'setLogRecordFactory',
'lastResort', 'raiseExceptions', 'getLevelNamesMapping']
'lastResort', 'raiseExceptions', 'getLevelNamesMapping',
'getHandlerByName']

import threading

Expand Down Expand Up @@ -885,6 +886,15 @@ def _addHandlerRef(handler):
finally:
_releaseLock()


def getHandlerByName(name):
"""
Get a handler with the specified *name*, or None if there isn't one with
that name.
"""
return _handlers.get(name)


class Handler(Filterer):
"""
Handler instances dispatch logging events to specific destinations.
Expand Down
66 changes: 59 additions & 7 deletions Lib/logging/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
"""

import errno
import functools
import io
import logging
import logging.handlers
import queue
import re
import struct
import threading
Expand Down Expand Up @@ -563,7 +565,7 @@ def configure(self):
handler.name = name
handlers[name] = handler
except Exception as e:
if 'target not configured yet' in str(e.__cause__):
if ' not configured yet' in str(e.__cause__):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to define an exception type for this rather than use the text of the exception message for control flow (this should be done in a separate PR as it's not a new issue here).

deferred.append(name)
else:
raise ValueError('Unable to configure handler '
Expand Down Expand Up @@ -702,6 +704,21 @@ def add_filters(self, filterer, filters):
except Exception as e:
raise ValueError('Unable to add filter %r' % f) from e

def _configure_queue_handler(self, klass, **kwargs):
if 'queue' in kwargs:
q = kwargs['queue']
else:
q = queue.Queue() # unbounded
rhl = kwargs.get('respect_handler_level', False)
if 'listener' in kwargs:
lklass = kwargs['listener']
else:
lklass = logging.handlers.QueueListener
listener = lklass(q, *kwargs['handlers'], respect_handler_level=rhl)
handler = klass(q)
handler.listener = listener
return handler

def configure_handler(self, config):
"""Configure a handler from a dictionary."""
config_copy = dict(config) # for restoring in case of error
Expand All @@ -721,26 +738,61 @@ def configure_handler(self, config):
factory = c
else:
cname = config.pop('class')
klass = self.resolve(cname)
#Special case for handler which refers to another handler
if callable(cname):
klass = cname
else:
klass = self.resolve(cname)
if issubclass(klass, logging.handlers.MemoryHandler) and\
'target' in config:
# Special case for handler which refers to another handler
try:
th = self.config['handlers'][config['target']]
tn = config['target']
th = self.config['handlers'][tn]
if not isinstance(th, logging.Handler):
config.update(config_copy) # restore for deferred cfg
raise TypeError('target not configured yet')
config['target'] = th
except Exception as e:
raise ValueError('Unable to set target handler '
'%r' % config['target']) from e
raise ValueError('Unable to set target handler %r' % tn) from e
elif issubclass(klass, logging.handlers.QueueHandler):
# Another special case for handler which refers to other handlers
if 'handlers' not in config:
raise ValueError('No handlers specified for a QueueHandler')
if 'queue' in config and\
not isinstance(qspec := config['queue'], queue.Queue):
q = self.resolve(qspec)
if not callable(q):
raise TypeError('Invalid queue specifier %r' % qspec)
config['queue'] = q()
if 'listener' in config:
lspec = config['listener']
if isinstance(lspec, str):
listener = self.resolve(lspec)
config['listener'] = listener
elif not issubclass(lspec, logging.handlers.QueueListener):
raise TypeError('Invalid listener spec %r' % lspec)
hlist = []
try:
for hn in config['handlers']:
h = self.config['handlers'][hn]
if not isinstance(h, logging.Handler):
config.update(config_copy) # restore for deferred cfg
raise TypeError('needed handler %r '
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raise TypeError('needed handler %r '
raise TypeError('Required handler %r '

'is not configured yet' % hn)
hlist.append(h)
except Exception as e:
raise ValueError('Unable to set required handler %r' % hn) from e
config['handlers'] = hlist
elif issubclass(klass, logging.handlers.SMTPHandler) and\
'mailhost' in config:
config['mailhost'] = self.as_tuple(config['mailhost'])
elif issubclass(klass, logging.handlers.SysLogHandler) and\
'address' in config:
config['address'] = self.as_tuple(config['address'])
factory = klass
if issubclass(klass, logging.handlers.QueueHandler):
factory = functools.partial(self._configure_queue_handler, klass)
else:
factory = klass
props = config.pop('.', None)
kwargs = {k: config[k] for k in config if valid_ident(k)}
try:
Expand Down
1 change: 1 addition & 0 deletions Lib/logging/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1424,6 +1424,7 @@ def __init__(self, queue):
"""
logging.Handler.__init__(self)
self.queue = queue
self.listener = None # will be set to listener if configured via dictConfig()

def enqueue(self, record):
"""
Expand Down
Loading