From 1ece898bcd277eb6b284c23ce5dafdc722007597 Mon Sep 17 00:00:00 2001 From: Romuald Brunet Date: Wed, 26 Jul 2023 14:19:33 +0200 Subject: [PATCH 1/6] gh-76913: Add "merge extras" feature to LoggerAdapter By default, LoggerAdapter objects ignores all `extra=` parameter used in the individual log methods, which may be confusing for some users. This commit is aimed at adding an option in the LoggerAdapter class to allow instances / subclasses to merge both the adapter and individual log call extra into a single entry The default behavior is not changed For example: ``` log = LoggerAdapter(..., extra={"component": "XYZ"}) log.info("return %r" % ret, extra={"duration": elapsed}) ``` --- Lib/logging/__init__.py | 21 +++++++++++++++++++-- Lib/test/test_logging.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py index 46e86cb87ecfcb..9aa022373c61bb 100644 --- a/Lib/logging/__init__.py +++ b/Lib/logging/__init__.py @@ -1880,7 +1880,9 @@ class LoggerAdapter(object): information in logging output. """ - def __init__(self, logger, extra=None): + merge_extras = False + + def __init__(self, logger, extra=None, merge_extras=None): """ Initialize the adapter with a logger and a dict-like object which provides contextual information. This constructor signature allows @@ -1890,9 +1892,21 @@ def __init__(self, logger, extra=None): 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_extras=True will instead merge both + maps when logging, the indivicual call extra taking precedence + over the LoggerAdapter instance extra + + .. versionchanged:: 3.13 + Added the ``merge_extras`` parameter. """ self.logger = logger self.extra = extra + if merge_extras is not None: + self.merge_extras = merge_extras def process(self, msg, kwargs): """ @@ -1904,7 +1918,10 @@ def process(self, msg, kwargs): 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_extras 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 def976fbe96ba3..667b4fb47c23a9 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -5433,6 +5433,46 @@ def process(self, msg, kwargs): 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_extras=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_extras=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): From b61f0ef04aed40d2fc6e2feceb8b7ba1d7dff21f Mon Sep 17 00:00:00 2001 From: Romuald Brunet Date: Mon, 31 Jul 2023 16:49:48 +0200 Subject: [PATCH 2/6] gh-76913: Code review fixes --- Lib/logging/__init__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py index 9aa022373c61bb..02eadb89b99312 100644 --- a/Lib/logging/__init__.py +++ b/Lib/logging/__init__.py @@ -1880,9 +1880,7 @@ class LoggerAdapter(object): information in logging output. """ - merge_extras = False - - def __init__(self, logger, extra=None, merge_extras=None): + def __init__(self, logger, extra=None, merge_extras=False): """ Initialize the adapter with a logger and a dict-like object which provides contextual information. This constructor signature allows @@ -1897,7 +1895,7 @@ def __init__(self, logger, extra=None, merge_extras=None): passed on the individual log calls to use its own instead. Initializing it with merge_extras=True will instead merge both - maps when logging, the indivicual call extra taking precedence + maps when logging, the individual call extra taking precedence over the LoggerAdapter instance extra .. versionchanged:: 3.13 @@ -1905,8 +1903,7 @@ def __init__(self, logger, extra=None, merge_extras=None): """ self.logger = logger self.extra = extra - if merge_extras is not None: - self.merge_extras = merge_extras + self.merge_extras = merge_extras def process(self, msg, kwargs): """ From 65dcfcf175ad7fe6b5623358eed77baff3b831e0 Mon Sep 17 00:00:00 2001 From: Romuald Brunet Date: Mon, 31 Jul 2023 16:50:41 +0200 Subject: [PATCH 3/6] gh-76913: Code review fixes (singular form) --- Lib/logging/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py index 02eadb89b99312..e194c4dab27709 100644 --- a/Lib/logging/__init__.py +++ b/Lib/logging/__init__.py @@ -1880,7 +1880,7 @@ class LoggerAdapter(object): information in logging output. """ - def __init__(self, logger, extra=None, merge_extras=False): + 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 @@ -1894,7 +1894,7 @@ def __init__(self, logger, extra=None, merge_extras=False): By default, LoggerAdapter objects will drop the "extra" argument passed on the individual log calls to use its own instead. - Initializing it with merge_extras=True will instead merge both + Initializing it with merge_extra=True will instead merge both maps when logging, the individual call extra taking precedence over the LoggerAdapter instance extra @@ -1903,7 +1903,7 @@ def __init__(self, logger, extra=None, merge_extras=False): """ self.logger = logger self.extra = extra - self.merge_extras = merge_extras + self.merge_extra = merge_extra def process(self, msg, kwargs): """ From c46ea457e168975d87139a112a907f59509bb898 Mon Sep 17 00:00:00 2001 From: Romuald Brunet Date: Mon, 14 Aug 2023 17:07:44 +0200 Subject: [PATCH 4/6] gh-76913: Fix / follow instance attribute name change --- Lib/logging/__init__.py | 2 +- Lib/test/test_logging.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/logging/__init__.py b/Lib/logging/__init__.py index e194c4dab27709..40833c2521f5de 100644 --- a/Lib/logging/__init__.py +++ b/Lib/logging/__init__.py @@ -1915,7 +1915,7 @@ def process(self, msg, kwargs): Normally, you'll only need to override this one method in a LoggerAdapter subclass for your specific needs. """ - if self.merge_extras and "extra" in kwargs: + if self.merge_extra and "extra" in kwargs: kwargs["extra"] = {**self.extra, **kwargs["extra"]} else: kwargs["extra"] = self.extra diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 667b4fb47c23a9..f26846f9663e5c 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -5452,7 +5452,7 @@ def test_extra_not_merged_by_default(self): def test_extra_merged(self): self.adapter = logging.LoggerAdapter(logger=self.logger, extra={'foo': '1'}, - merge_extras=True) + merge_extra=True) self.adapter.critical('foo and bar should be here', extra={'bar': '2'}) self.assertEqual(len(self.recording.records), 1) @@ -5465,7 +5465,7 @@ def test_extra_merged(self): def test_extra_merged_log_call_has_precedence(self): self.adapter = logging.LoggerAdapter(logger=self.logger, extra={'foo': '1'}, - merge_extras=True) + merge_extra=True) self.adapter.critical('foo shall be min', extra={'foo': '2'}) self.assertEqual(len(self.recording.records), 1) From a058d23189011d57794207fdb102e24d25f68a1a Mon Sep 17 00:00:00 2001 From: Romuald Brunet Date: Mon, 14 Aug 2023 17:08:48 +0200 Subject: [PATCH 5/6] gh-76913: Add LoggerAdapter.merge_extra documentation --- Doc/library/logging.rst | 11 +++++++++-- Lib/logging/__init__.py | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Doc/library/logging.rst b/Doc/library/logging.rst index 4c6e74ff66a11a..108c157f5c1a52 100644 --- a/Doc/library/logging.rst +++ b/Doc/library/logging.rst @@ -984,10 +984,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) @@ -1019,6 +1023,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 40833c2521f5de..37e7365f403d9a 100644 --- a/Lib/logging/__init__.py +++ b/Lib/logging/__init__.py @@ -1899,7 +1899,7 @@ def __init__(self, logger, extra=None, merge_extra=False): over the LoggerAdapter instance extra .. versionchanged:: 3.13 - Added the ``merge_extras`` parameter. + The *merge_extra* argument was added. """ self.logger = logger self.extra = extra From 0cf1bebf88a3a550e335a1e2e829f6fe125623a4 Mon Sep 17 00:00:00 2001 From: Romuald Brunet Date: Mon, 14 Aug 2023 17:16:02 +0200 Subject: [PATCH 6/6] gh-76913: Add Misc/NEWS.d entry --- .../next/Library/2023-08-14-17-15-59.gh-issue-76913.LLD0rT.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2023-08-14-17-15-59.gh-issue-76913.LLD0rT.rst 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 00000000000000..5f9a84e714ae20 --- /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`