From 4c6d5e998aa862a3551d24e5bec03cbd7f6d39a4 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Tue, 13 Jul 2021 23:07:56 -0500 Subject: [PATCH 01/12] Add support for custom attributes in OTLPHandler --- .../src/opentelemetry/sdk/logs/__init__.py | 21 ++++++++++++--- opentelemetry-sdk/tests/logs/test_handler.py | 26 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py index 8b0da9e22a..b32637b38a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -244,20 +244,35 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: return True +# skip natural LogRecord attributes +# http://docs.python.org/library/logging.html#logrecord-attributes +_RESERVED_ATTRS = frozenset(( + 'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename', + 'funcName', 'levelname', 'levelno', 'lineno', 'module', + 'msecs', 'message', 'msg', 'name', 'pathname', 'process', + 'processName', 'relativeCreated', 'stack_info', 'thread', 'threadName') +) + + class OTLPHandler(logging.Handler): """A handler class which writes logging records, in OTLP format, to a network destination or file. """ - def __init__(self, level=logging.NOTSET, log_emitter=None) -> None: + def __init__(self, level=logging.NOTSET, log_emitter=None, *, attributes_key: Optional[str] = None) -> None: super().__init__(level=level) self._log_emitter = log_emitter or get_log_emitter(__name__) + self.attributes_key = attributes_key + + def _get_attributes(self, record: logging.LogRecord) -> Attributes: + if self.attributes_key is not None: + return vars(record)[self.attributes_key] + return {k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS} def _translate(self, record: logging.LogRecord) -> LogRecord: timestamp = int(record.created * 1e9) span_context = get_current_span().get_span_context() - # TODO: attributes (or resource attributes?) from record metadata - attributes: Attributes = {} + attributes = self._get_attributes(record) severity_number = std_to_otlp(record.levelno) return LogRecord( timestamp=timestamp, diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 1d1b84f0fd..3b3daf48f8 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -65,6 +65,32 @@ def test_log_record_no_span_context(self): log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags ) + def test_log_record_user_attributes(self): + """Attributes can be injected into logs by adding them as attributes to the LogRecord""" + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(log_emitter=emitter_mock) + # Assert emit gets called for warning message + logger.warning("Wanrning message", extra={"http.status_code": 200}) + args, _ = emitter_mock.emit.call_args_list[0] + log_record = args[0] + + self.assertIsNotNone(log_record) + self.assertEqual(log_record.attributes, {"http.status_code": 200}) + + def test_log_record_user_attributes_key(self): + """Users can specify a key to extract attributes from""" + emitter_mock = Mock(spec=LogEmitter) + logger = logging.getLogger(__name__) + handler = OTLPHandler(level=logging.NOTSET, log_emitter=emitter_mock, attributes_key="opentelemetry") + logger.addHandler(handler) + # Assert emit gets called for warning message + logger.warning("Wanrning message", extra={"opentelemetry": {"http.status_code": 200}, "ignored": True}) + args, _ = emitter_mock.emit.call_args_list[0] + log_record = args[0] + + self.assertIsNotNone(log_record) + self.assertEqual(log_record.attributes, {"http.status_code": 200}) + def test_log_record_trace_correlation(self): emitter_mock = Mock(spec=LogEmitter) logger = get_logger(log_emitter=emitter_mock) From 28632ca49920b3b3e65ee4892abc7b766400328d Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Tue, 13 Jul 2021 23:10:55 -0500 Subject: [PATCH 02/12] blacken --- .../src/opentelemetry/sdk/logs/__init__.py | 42 +++++++++++++++---- opentelemetry-sdk/tests/logs/test_handler.py | 14 ++++++- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py index b32637b38a..732b6740c6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -246,11 +246,31 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: # skip natural LogRecord attributes # http://docs.python.org/library/logging.html#logrecord-attributes -_RESERVED_ATTRS = frozenset(( - 'args', 'asctime', 'created', 'exc_info', 'exc_text', 'filename', - 'funcName', 'levelname', 'levelno', 'lineno', 'module', - 'msecs', 'message', 'msg', 'name', 'pathname', 'process', - 'processName', 'relativeCreated', 'stack_info', 'thread', 'threadName') +_RESERVED_ATTRS = frozenset( + ( + "args", + "asctime", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "levelname", + "levelno", + "lineno", + "module", + "msecs", + "message", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack_info", + "thread", + "threadName", + ) ) @@ -259,7 +279,13 @@ class OTLPHandler(logging.Handler): a network destination or file. """ - def __init__(self, level=logging.NOTSET, log_emitter=None, *, attributes_key: Optional[str] = None) -> None: + def __init__( + self, + level=logging.NOTSET, + log_emitter=None, + *, + attributes_key: Optional[str] = None, + ) -> None: super().__init__(level=level) self._log_emitter = log_emitter or get_log_emitter(__name__) self.attributes_key = attributes_key @@ -267,7 +293,9 @@ def __init__(self, level=logging.NOTSET, log_emitter=None, *, attributes_key: Op def _get_attributes(self, record: logging.LogRecord) -> Attributes: if self.attributes_key is not None: return vars(record)[self.attributes_key] - return {k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS} + return { + k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS + } def _translate(self, record: logging.LogRecord) -> LogRecord: timestamp = int(record.created * 1e9) diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 3b3daf48f8..e5eed1934e 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -81,10 +81,20 @@ def test_log_record_user_attributes_key(self): """Users can specify a key to extract attributes from""" emitter_mock = Mock(spec=LogEmitter) logger = logging.getLogger(__name__) - handler = OTLPHandler(level=logging.NOTSET, log_emitter=emitter_mock, attributes_key="opentelemetry") + handler = OTLPHandler( + level=logging.NOTSET, + log_emitter=emitter_mock, + attributes_key="opentelemetry", + ) logger.addHandler(handler) # Assert emit gets called for warning message - logger.warning("Wanrning message", extra={"opentelemetry": {"http.status_code": 200}, "ignored": True}) + logger.warning( + "Wanrning message", + extra={ + "opentelemetry": {"http.status_code": 200}, + "ignored": True, + }, + ) args, _ = emitter_mock.emit.call_args_list[0] log_record = args[0] From 93e00373470e1bde8da25e6add5e903a4c1a6166 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Tue, 13 Jul 2021 23:38:15 -0500 Subject: [PATCH 03/12] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db6af9bb64..40bda1f0b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/open-telemetry/opentelemetry-python/compare/v1.3.0-0.22b0...HEAD) ### Added +- Give OTLPHandler the ability to process attributes + ([#1952](https://github.com/open-telemetry/opentelemetry-python/pull/1952)) - Add global LogEmitterProvider and convenience function get_log_emitter ([#1901](https://github.com/open-telemetry/opentelemetry-python/pull/1901)) - Add OTLPHandler for standard library logging module From db57083d4fbe0ad70b493ccce2fc1d9987ed9f11 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Tue, 13 Jul 2021 23:43:51 -0500 Subject: [PATCH 04/12] Dummy change for CLA --- opentelemetry-sdk/tests/logs/test_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index e5eed1934e..9eda34b21a 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -66,7 +66,7 @@ def test_log_record_no_span_context(self): ) def test_log_record_user_attributes(self): - """Attributes can be injected into logs by adding them as attributes to the LogRecord""" + """Attributes can be injected into logs by adding them to the LogRecord""" emitter_mock = Mock(spec=LogEmitter) logger = get_logger(log_emitter=emitter_mock) # Assert emit gets called for warning message From 826b02ac929e55a2d2c5e07a14840f709d1beab9 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Tue, 13 Jul 2021 23:45:52 -0500 Subject: [PATCH 05/12] fix typo --- opentelemetry-sdk/tests/logs/test_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 9eda34b21a..5194f9ab3d 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -70,7 +70,7 @@ def test_log_record_user_attributes(self): emitter_mock = Mock(spec=LogEmitter) logger = get_logger(log_emitter=emitter_mock) # Assert emit gets called for warning message - logger.warning("Wanrning message", extra={"http.status_code": 200}) + logger.warning("Warning message", extra={"http.status_code": 200}) args, _ = emitter_mock.emit.call_args_list[0] log_record = args[0] @@ -89,7 +89,7 @@ def test_log_record_user_attributes_key(self): logger.addHandler(handler) # Assert emit gets called for warning message logger.warning( - "Wanrning message", + "Warning message", extra={ "opentelemetry": {"http.status_code": 200}, "ignored": True, From 231fb16315987c35c6575d9042144626f8ccd475 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Wed, 14 Jul 2021 10:41:22 -0500 Subject: [PATCH 06/12] Remove atribute_keyu param --- .../src/opentelemetry/sdk/logs/__init__.py | 5 ---- opentelemetry-sdk/tests/logs/test_handler.py | 24 ------------------- 2 files changed, 29 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py index 732b6740c6..fc777135b6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -283,16 +283,11 @@ def __init__( self, level=logging.NOTSET, log_emitter=None, - *, - attributes_key: Optional[str] = None, ) -> None: super().__init__(level=level) self._log_emitter = log_emitter or get_log_emitter(__name__) - self.attributes_key = attributes_key def _get_attributes(self, record: logging.LogRecord) -> Attributes: - if self.attributes_key is not None: - return vars(record)[self.attributes_key] return { k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS } diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 5194f9ab3d..5113d7a5e4 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -77,30 +77,6 @@ def test_log_record_user_attributes(self): self.assertIsNotNone(log_record) self.assertEqual(log_record.attributes, {"http.status_code": 200}) - def test_log_record_user_attributes_key(self): - """Users can specify a key to extract attributes from""" - emitter_mock = Mock(spec=LogEmitter) - logger = logging.getLogger(__name__) - handler = OTLPHandler( - level=logging.NOTSET, - log_emitter=emitter_mock, - attributes_key="opentelemetry", - ) - logger.addHandler(handler) - # Assert emit gets called for warning message - logger.warning( - "Warning message", - extra={ - "opentelemetry": {"http.status_code": 200}, - "ignored": True, - }, - ) - args, _ = emitter_mock.emit.call_args_list[0] - log_record = args[0] - - self.assertIsNotNone(log_record) - self.assertEqual(log_record.attributes, {"http.status_code": 200}) - def test_log_record_trace_correlation(self): emitter_mock = Mock(spec=LogEmitter) logger = get_logger(log_emitter=emitter_mock) From ea248c2c9f85b4939a6b77bdb17fe67bef6cd08c Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Wed, 14 Jul 2021 20:36:27 -0500 Subject: [PATCH 07/12] Use suggested attributes --- .../src/opentelemetry/sdk/logs/__init__.py | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py index fc777135b6..111db2c80f 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -247,30 +247,28 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: # skip natural LogRecord attributes # http://docs.python.org/library/logging.html#logrecord-attributes _RESERVED_ATTRS = frozenset( - ( - "args", - "asctime", - "created", - "exc_info", - "exc_text", - "filename", - "funcName", - "levelname", - "levelno", - "lineno", - "module", - "msecs", - "message", - "msg", - "name", - "pathname", - "process", - "processName", - "relativeCreated", - "stack_info", - "thread", - "threadName", - ) + "asctime", + "args", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "getMessage", + "levelname", + "levelno", + "lineno", + "module", + "msecs", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack_info", + "thread", + "threadName", ) From dd2b3dc8610d639f01cb1d6c35ec7528222d3e04 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Wed, 14 Jul 2021 20:37:09 -0500 Subject: [PATCH 08/12] Fix parens --- .../src/opentelemetry/sdk/logs/__init__.py | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py index 111db2c80f..f4f2ff86cb 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -247,28 +247,30 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: # skip natural LogRecord attributes # http://docs.python.org/library/logging.html#logrecord-attributes _RESERVED_ATTRS = frozenset( - "asctime", - "args", - "created", - "exc_info", - "exc_text", - "filename", - "funcName", - "getMessage", - "levelname", - "levelno", - "lineno", - "module", - "msecs", - "msg", - "name", - "pathname", - "process", - "processName", - "relativeCreated", - "stack_info", - "thread", - "threadName", + ( + "asctime", + "args", + "created", + "exc_info", + "exc_text", + "filename", + "funcName", + "getMessage", + "levelname", + "levelno", + "lineno", + "module", + "msecs", + "msg", + "name", + "pathname", + "process", + "processName", + "relativeCreated", + "stack_info", + "thread", + "threadName", + ) ) From 57653744a958edb524bf75e53c0140f0e83a5346 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Wed, 14 Jul 2021 23:06:30 -0500 Subject: [PATCH 09/12] Add example test --- opentelemetry-sdk/tests/logs/test_handler.py | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 5113d7a5e4..c78bf0db22 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -15,6 +15,7 @@ import logging import unittest from unittest.mock import Mock +from uuid import uuid4 from opentelemetry.sdk import trace from opentelemetry.sdk.logs import LogEmitter, OTLPHandler @@ -77,6 +78,30 @@ def test_log_record_user_attributes(self): self.assertIsNotNone(log_record) self.assertEqual(log_record.attributes, {"http.status_code": 200}) + def test_log_record_include_LogRecord_attribute(self): + """Users can include LogRecord attributes by giving them a non-colliding name""" + emitter_mock = Mock(spec=LogEmitter) + # a unique logger to avoid messing with other tests + logger = logging.getLogger(str(uuid4()).replace("-", ".")) + handler = OTLPHandler(level=logging.NOTSET, log_emitter=emitter_mock) + logger.addHandler(handler) + + def filter(record: logging.LogRecord) -> bool: + record.foobar = record.levelname + return True + + handler.addFilter(filter) + + # Assert emit gets called for warning message + logger.warning("Warning message", extra={"http.status_code": 200}) + + args, _ = emitter_mock.emit.call_args_list[0] + log_record = args[0] + + self.assertIsNotNone(log_record) + self.assertEqual(log_record.attributes, {"http.status_code": 200, "foobar": "WARNING"}) + + def test_log_record_trace_correlation(self): emitter_mock = Mock(spec=LogEmitter) logger = get_logger(log_emitter=emitter_mock) From 248ac56cc8d1e666ee930a30eca77b94509090ff Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Wed, 14 Jul 2021 23:08:42 -0500 Subject: [PATCH 10/12] fmt --- opentelemetry-sdk/tests/logs/test_handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index c78bf0db22..6899f7ca3a 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -99,8 +99,10 @@ def filter(record: logging.LogRecord) -> bool: log_record = args[0] self.assertIsNotNone(log_record) - self.assertEqual(log_record.attributes, {"http.status_code": 200, "foobar": "WARNING"}) - + self.assertEqual( + log_record.attributes, + {"http.status_code": 200, "foobar": "WARNING"}, + ) def test_log_record_trace_correlation(self): emitter_mock = Mock(spec=LogEmitter) From f8f4876967080f7427aab5f7da1f80ad061d2952 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Thu, 15 Jul 2021 00:04:31 -0500 Subject: [PATCH 11/12] remove example test --- opentelemetry-sdk/tests/logs/test_handler.py | 27 -------------------- 1 file changed, 27 deletions(-) diff --git a/opentelemetry-sdk/tests/logs/test_handler.py b/opentelemetry-sdk/tests/logs/test_handler.py index 6899f7ca3a..5113d7a5e4 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -15,7 +15,6 @@ import logging import unittest from unittest.mock import Mock -from uuid import uuid4 from opentelemetry.sdk import trace from opentelemetry.sdk.logs import LogEmitter, OTLPHandler @@ -78,32 +77,6 @@ def test_log_record_user_attributes(self): self.assertIsNotNone(log_record) self.assertEqual(log_record.attributes, {"http.status_code": 200}) - def test_log_record_include_LogRecord_attribute(self): - """Users can include LogRecord attributes by giving them a non-colliding name""" - emitter_mock = Mock(spec=LogEmitter) - # a unique logger to avoid messing with other tests - logger = logging.getLogger(str(uuid4()).replace("-", ".")) - handler = OTLPHandler(level=logging.NOTSET, log_emitter=emitter_mock) - logger.addHandler(handler) - - def filter(record: logging.LogRecord) -> bool: - record.foobar = record.levelname - return True - - handler.addFilter(filter) - - # Assert emit gets called for warning message - logger.warning("Warning message", extra={"http.status_code": 200}) - - args, _ = emitter_mock.emit.call_args_list[0] - log_record = args[0] - - self.assertIsNotNone(log_record) - self.assertEqual( - log_record.attributes, - {"http.status_code": 200, "foobar": "WARNING"}, - ) - def test_log_record_trace_correlation(self): emitter_mock = Mock(spec=LogEmitter) logger = get_logger(log_emitter=emitter_mock) From 539df2976460b71300667ac06c7e4c12600da34b Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Wed, 4 Aug 2021 11:43:49 -0500 Subject: [PATCH 12/12] make _get_attributes a static method --- opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py index f4f2ff86cb..02c22578f5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -287,7 +287,8 @@ def __init__( super().__init__(level=level) self._log_emitter = log_emitter or get_log_emitter(__name__) - def _get_attributes(self, record: logging.LogRecord) -> Attributes: + @staticmethod + def _get_attributes(record: logging.LogRecord) -> Attributes: return { k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS }