Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for user defined attributes in OTLPHandler #1952

Merged
merged 14 commits into from
Aug 16, 2021
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 41 additions & 3 deletions opentelemetry-sdk/src/opentelemetry/sdk/logs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,20 +244,58 @@ 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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are skipping now to separate the user-defined extra attributes but we will revisit this later if needed.

(
"asctime",
"args",
"created",
"exc_info",
"exc_text",
"filename",
"funcName",
"getMessage",
"levelname",
"levelno",
"lineno",
"module",
"msecs",
"msg",
"name",
"pathname",
"process",
"processName",
srikanthccv marked this conversation as resolved.
Show resolved Hide resolved
"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__)

def _get_attributes(self, record: logging.LogRecord) -> Attributes:
return {
k: v for k, v in vars(record).items() if k not in _RESERVED_ATTRS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is extra the only keyword that will be filtered in?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

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,
Expand Down
12 changes: 12 additions & 0 deletions opentelemetry-sdk/tests/logs/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down