From a482e5bf0022f85424a6308529a9ad51f1bfbb71 Mon Sep 17 00:00:00 2001 From: Romuald Brunet Date: Tue, 15 Aug 2023 09:23:54 +0200 Subject: [PATCH] gh-76913: Add "merge extras" feature to LoggerAdapter (GH-107292) --- Doc/library/logging.rst | 11 ++++- Lib/logging/__init__.py | 18 ++++++++- Lib/test/test_logging.py | 40 +++++++++++++++++++ ...3-08-14-17-15-59.gh-issue-76913.LLD0rT.rst | 1 + 4 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-08-14-17-15-59.gh-issue-76913.LLD0rT.rst diff --git a/Doc/library/logging.rst b/Doc/library/logging.rst index b582c918df0..49e870e9e24 100644 --- a/Doc/library/logging.rst +++ b/Doc/library/logging.rst @@ -1002,10 +1002,14 @@ LoggerAdapter Objects information into logging calls. For a usage example, see the section on :ref:`adding contextual information to your logging output `. -.. class:: LoggerAdapter(logger, extra) +.. class:: LoggerAdapter(logger, extra, merge_extra=False) Returns an instance of :class:`LoggerAdapter` initialized with an - underlying :class:`Logger` instance and a dict-like object. + underlying :class:`Logger` instance, a dict-like object (*extra*), and a + boolean (*merge_extra*) indicating whether or not the *extra* argument of + individual log calls should be merged with the :class:`LoggerAdapter` extra. + The default behavior is to ignore the *extra* argument of individual log + calls and only use the one of the :class:`LoggerAdapter` instance .. method:: process(msg, kwargs) @@ -1037,6 +1041,9 @@ interchangeably. Remove the undocumented ``warn()`` method which was an alias to the ``warning()`` method. +.. versionchanged:: 3.13 + The *merge_extra* argument was added. + Thread Safety ------------- diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py index 527fc5c6317..2d228e56309 100644 --- a/Lib/logging/__init__.py +++ b/Lib/logging/__init__.py @@ -1879,7 +1879,7 @@ class LoggerAdapter(object): information in logging output. """ - def __init__(self, logger, extra=None): + def __init__(self, logger, extra=None, merge_extra=False): """ Initialize the adapter with a logger and a dict-like object which provides contextual information. This constructor signature allows @@ -1889,9 +1889,20 @@ class LoggerAdapter(object): following example: adapter = LoggerAdapter(someLogger, dict(p1=v1, p2="v2")) + + By default, LoggerAdapter objects will drop the "extra" argument + passed on the individual log calls to use its own instead. + + Initializing it with merge_extra=True will instead merge both + maps when logging, the individual call extra taking precedence + over the LoggerAdapter instance extra + + .. versionchanged:: 3.13 + The *merge_extra* argument was added. """ self.logger = logger self.extra = extra + self.merge_extra = merge_extra def process(self, msg, kwargs): """ @@ -1903,7 +1914,10 @@ class LoggerAdapter(object): Normally, you'll only need to override this one method in a LoggerAdapter subclass for your specific needs. """ - kwargs["extra"] = self.extra + if self.merge_extra and "extra" in kwargs: + kwargs["extra"] = {**self.extra, **kwargs["extra"]} + else: + kwargs["extra"] = self.extra return msg, kwargs # diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index def976fbe96..f26846f9663 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -5433,6 +5433,46 @@ class LoggerAdapterTest(unittest.TestCase): self.assertIs(adapter.manager, orig_manager) self.assertIs(self.logger.manager, orig_manager) + def test_extra_in_records(self): + self.adapter = logging.LoggerAdapter(logger=self.logger, + extra={'foo': '1'}) + + self.adapter.critical('foo should be here') + self.assertEqual(len(self.recording.records), 1) + record = self.recording.records[0] + self.assertTrue(hasattr(record, 'foo')) + self.assertEqual(record.foo, '1') + + def test_extra_not_merged_by_default(self): + self.adapter.critical('foo should NOT be here', extra={'foo': 'nope'}) + self.assertEqual(len(self.recording.records), 1) + record = self.recording.records[0] + self.assertFalse(hasattr(record, 'foo')) + + def test_extra_merged(self): + self.adapter = logging.LoggerAdapter(logger=self.logger, + extra={'foo': '1'}, + merge_extra=True) + + self.adapter.critical('foo and bar should be here', extra={'bar': '2'}) + self.assertEqual(len(self.recording.records), 1) + record = self.recording.records[0] + self.assertTrue(hasattr(record, 'foo')) + self.assertTrue(hasattr(record, 'bar')) + self.assertEqual(record.foo, '1') + self.assertEqual(record.bar, '2') + + def test_extra_merged_log_call_has_precedence(self): + self.adapter = logging.LoggerAdapter(logger=self.logger, + extra={'foo': '1'}, + merge_extra=True) + + self.adapter.critical('foo shall be min', extra={'foo': '2'}) + self.assertEqual(len(self.recording.records), 1) + record = self.recording.records[0] + self.assertTrue(hasattr(record, 'foo')) + self.assertEqual(record.foo, '2') + class LoggerTest(BaseTest, AssertErrorMessage): diff --git a/Misc/NEWS.d/next/Library/2023-08-14-17-15-59.gh-issue-76913.LLD0rT.rst b/Misc/NEWS.d/next/Library/2023-08-14-17-15-59.gh-issue-76913.LLD0rT.rst new file mode 100644 index 00000000000..5f9a84e714a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-08-14-17-15-59.gh-issue-76913.LLD0rT.rst @@ -0,0 +1 @@ +Add *merge_extra* parameter/feature to :class:`logging.LoggerAdapter`