-
-
Notifications
You must be signed in to change notification settings - Fork 31.7k
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
Changes from all commits
3686d74
2bca114
ef2d96d
debf516
16778fe
42d43b9
9936dff
a29ded8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -661,6 +661,76 @@ 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:`~logging.handlers.QueueHandler.listener` attribute of | ||
the created handler, and that in turn will be available to you using | ||
:func:`~logging.getHandlerByName` and passing 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.queue_factory | ||
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, | ||
such as ``my.module.queue_factory()``. | ||
|
||
* A dict with a ``'()'`` key which is constructed in the usual way as discussed in | ||
:ref:`logging-config-dict-userdef`. The result of this construction should be a | ||
:class:`queue.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: | ||
|
||
* A subclass of :class:`logging.handlers.QueueListener`. This is of course only | ||
possible if you are constructing or modifying the configuration dictionary in | ||
code. | ||
|
||
* A string which resolves to a class which is a subclass of ``QueueListener``, such as | ||
``'my.package.CustomListener'``. | ||
|
||
* A dict with a ``'()'`` key which is constructed in the usual way as discussed in | ||
:ref:`logging-config-dict-userdef`. The result of this construction should be a | ||
callable with the same signature as the ``QueueListener`` initializer. | ||
Comment on lines
+720
to
+721
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Does that mean There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. The callable will be called just as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But that's different than the normal behavior of '()':
from docs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, but in this case the value to be returned is documented explicitly. How can it be returned as an instantiated object in this case, if it has no access to the queue? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also note that "instantiated object" is a generic term. It does not imply any specific type of object, and a callable is also an instantiated object. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think it would be possible to provide an example in the docs? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feel free to submit a documentation PR with what you think should be in there, and I'll take a look. |
||
|
||
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 be defined with the same | ||
initialization signatures as :class:`~logging.handlers.QueueHandler` and | ||
:class:`~logging.handlers.QueueListener`. | ||
|
||
.. versionadded:: 3.12 | ||
|
||
.. _logging-config-fileformat: | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
# Copyright 2001-2019 by Vinay Sajip. All Rights Reserved. | ||
# Copyright 2001-2022 by Vinay Sajip. All Rights Reserved. | ||
# | ||
# Permission to use, copy, modify, and distribute this software and its | ||
# documentation for any purpose and without fee is hereby granted, | ||
|
@@ -19,15 +19,17 @@ | |
is based on PEP 282 and comments thereto in comp.lang.python, and influenced | ||
by Apache's log4j system. | ||
|
||
Copyright (C) 2001-2019 Vinay Sajip. All Rights Reserved. | ||
Copyright (C) 2001-2022 Vinay Sajip. All Rights Reserved. | ||
|
||
To use, simply 'import logging' and log away! | ||
""" | ||
|
||
import errno | ||
import functools | ||
import io | ||
import logging | ||
import logging.handlers | ||
import queue | ||
import re | ||
import struct | ||
import threading | ||
|
@@ -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__): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ' | ||
|
@@ -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 | ||
|
@@ -721,26 +738,83 @@ 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: | ||
qspec = config['queue'] | ||
if not isinstance(qspec, queue.Queue): | ||
if isinstance(qspec, str): | ||
q = self.resolve(qspec) | ||
if not callable(q): | ||
raise TypeError('Invalid queue specifier %r' % qspec) | ||
q = q() | ||
elif isinstance(qspec, dict): | ||
if '()' not in qspec: | ||
raise TypeError('Invalid queue specifier %r' % qspec) | ||
q = self.configure_custom(dict(qspec)) | ||
else: | ||
raise TypeError('Invalid queue specifier %r' % qspec) | ||
config['queue'] = q | ||
if 'listener' in config: | ||
lspec = config['listener'] | ||
if isinstance(lspec, type): | ||
if not issubclass(lspec, logging.handlers.QueueListener): | ||
raise TypeError('Invalid listener specifier %r' % lspec) | ||
else: | ||
if isinstance(lspec, str): | ||
listener = self.resolve(lspec) | ||
if isinstance(listener, type) and\ | ||
not issubclass(listener, logging.handlers.QueueListener): | ||
raise TypeError('Invalid listener specifier %r' % lspec) | ||
elif isinstance(lspec, dict): | ||
if '()' not in lspec: | ||
raise TypeError('Invalid listener specifier %r' % lspec) | ||
listener = self.configure_custom(dict(lspec)) | ||
else: | ||
raise TypeError('Invalid listener specifier %r' % lspec) | ||
if not callable(listener): | ||
raise TypeError('Invalid listener specifier %r' % lspec) | ||
config['listener'] = listener | ||
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('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: | ||
|
There was a problem hiding this comment.
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?