Skip to content

Commit d0dbc0b

Browse files
authored
Add support for user defined attributes in OTLPHandler (#1952)
1 parent b31790a commit d0dbc0b

File tree

3 files changed

+56
-3
lines changed

3 files changed

+56
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3434
([#1893](https://github.com/open-telemetry/opentelemetry-python/pull/1893))
3535

3636
### Added
37+
- Give OTLPHandler the ability to process attributes
38+
([#1952](https://github.com/open-telemetry/opentelemetry-python/pull/1952))
3739
- Add global LogEmitterProvider and convenience function get_log_emitter
3840
([#1901](https://github.com/open-telemetry/opentelemetry-python/pull/1901))
3941
- Add OTLPHandler for standard library logging module

opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,20 +244,59 @@ def force_flush(self, timeout_millis: int = 30000) -> bool:
244244
return True
245245

246246

247+
# skip natural LogRecord attributes
248+
# http://docs.python.org/library/logging.html#logrecord-attributes
249+
_RESERVED_ATTRS = frozenset(
250+
(
251+
"asctime",
252+
"args",
253+
"created",
254+
"exc_info",
255+
"exc_text",
256+
"filename",
257+
"funcName",
258+
"getMessage",
259+
"levelname",
260+
"levelno",
261+
"lineno",
262+
"module",
263+
"msecs",
264+
"msg",
265+
"name",
266+
"pathname",
267+
"process",
268+
"processName",
269+
"relativeCreated",
270+
"stack_info",
271+
"thread",
272+
"threadName",
273+
)
274+
)
275+
276+
247277
class OTLPHandler(logging.Handler):
248278
"""A handler class which writes logging records, in OTLP format, to
249279
a network destination or file.
250280
"""
251281

252-
def __init__(self, level=logging.NOTSET, log_emitter=None) -> None:
282+
def __init__(
283+
self,
284+
level=logging.NOTSET,
285+
log_emitter=None,
286+
) -> None:
253287
super().__init__(level=level)
254288
self._log_emitter = log_emitter or get_log_emitter(__name__)
255289

290+
@staticmethod
291+
def _get_attributes(record: logging.LogRecord) -> Attributes:
292+
return {
293+
k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS
294+
}
295+
256296
def _translate(self, record: logging.LogRecord) -> LogRecord:
257297
timestamp = int(record.created * 1e9)
258298
span_context = get_current_span().get_span_context()
259-
# TODO: attributes (or resource attributes?) from record metadata
260-
attributes: Attributes = {}
299+
attributes = self._get_attributes(record)
261300
severity_number = std_to_otlp(record.levelno)
262301
return LogRecord(
263302
timestamp=timestamp,

opentelemetry-sdk/tests/logs/test_handler.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ def test_log_record_no_span_context(self):
6565
log_record.trace_flags, INVALID_SPAN_CONTEXT.trace_flags
6666
)
6767

68+
def test_log_record_user_attributes(self):
69+
"""Attributes can be injected into logs by adding them to the LogRecord"""
70+
emitter_mock = Mock(spec=LogEmitter)
71+
logger = get_logger(log_emitter=emitter_mock)
72+
# Assert emit gets called for warning message
73+
logger.warning("Warning message", extra={"http.status_code": 200})
74+
args, _ = emitter_mock.emit.call_args_list[0]
75+
log_record = args[0]
76+
77+
self.assertIsNotNone(log_record)
78+
self.assertEqual(log_record.attributes, {"http.status_code": 200})
79+
6880
def test_log_record_trace_correlation(self):
6981
emitter_mock = Mock(spec=LogEmitter)
7082
logger = get_logger(log_emitter=emitter_mock)

0 commit comments

Comments
 (0)