diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f1c2529fc..5f4c305ae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893)) ### 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 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py index 8b0da9e22a..02c22578f5 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py @@ -244,20 +244,59 @@ 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( + ( + "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", + ) +) + + 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, + ) -> None: super().__init__(level=level) self._log_emitter = log_emitter or get_log_emitter(__name__) + @staticmethod + def _get_attributes(record: logging.LogRecord) -> Attributes: + 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 d9d9566d2a..474a87fe8d 100644 --- a/opentelemetry-sdk/tests/logs/test_handler.py +++ b/opentelemetry-sdk/tests/logs/test_handler.py @@ -65,6 +65,18 @@ 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 to the LogRecord""" + emitter_mock = Mock(spec=LogEmitter) + logger = get_logger(log_emitter=emitter_mock) + # 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}) + def test_log_record_trace_correlation(self): emitter_mock = Mock(spec=LogEmitter) logger = get_logger(log_emitter=emitter_mock)