-
Notifications
You must be signed in to change notification settings - Fork 817
Add logger exception support for logs API/SDK #4908
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
base: main
Are you sure you want to change the base?
Changes from all commits
9c21864
26ac99b
d226158
6581718
0f2bcf0
a42e569
b02a455
4003ef3
bcafcc1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,6 +76,7 @@ def __init__( | |
| body: AnyValue = None, | ||
| attributes: Optional[_ExtendedAttributes] = None, | ||
| event_name: Optional[str] = None, | ||
| exception: Optional[BaseException] = None, | ||
| ) -> None: ... | ||
|
|
||
| @overload | ||
|
|
@@ -94,6 +95,7 @@ def __init__( | |
| severity_number: Optional[SeverityNumber] = None, | ||
| body: AnyValue = None, | ||
| attributes: Optional[_ExtendedAttributes] = None, | ||
| exception: Optional[BaseException] = None, | ||
| ) -> None: ... | ||
|
|
||
| def __init__( | ||
|
|
@@ -110,6 +112,7 @@ def __init__( | |
| body: AnyValue = None, | ||
| attributes: Optional[_ExtendedAttributes] = None, | ||
| event_name: Optional[str] = None, | ||
| exception: Optional[BaseException] = None, | ||
| ) -> None: | ||
| if not context: | ||
| context = get_current() | ||
|
|
@@ -127,6 +130,7 @@ def __init__( | |
| self.body = body | ||
| self.attributes = attributes | ||
| self.event_name = event_name | ||
| self.exception = exception | ||
|
|
||
|
|
||
| class Logger(ABC): | ||
|
|
@@ -157,6 +161,7 @@ def emit( | |
| body: AnyValue | None = None, | ||
| attributes: _ExtendedAttributes | None = None, | ||
| event_name: str | None = None, | ||
| exception: BaseException | None = None, | ||
| ) -> None: ... | ||
|
|
||
| @overload | ||
|
|
@@ -178,6 +183,7 @@ def emit( | |
| body: AnyValue | None = None, | ||
| attributes: _ExtendedAttributes | None = None, | ||
| event_name: str | None = None, | ||
| exception: BaseException | None = None, | ||
| ) -> None: | ||
| """Emits a :class:`LogRecord` representing a log to the processing pipeline.""" | ||
|
|
||
|
|
@@ -200,6 +206,7 @@ def emit( | |
| body: AnyValue | None = None, | ||
| attributes: _ExtendedAttributes | None = None, | ||
| event_name: str | None = None, | ||
| exception: BaseException | None = None, | ||
| ) -> None: ... | ||
|
|
||
| @overload | ||
|
|
@@ -220,6 +227,7 @@ def emit( | |
| body: AnyValue | None = None, | ||
| attributes: _ExtendedAttributes | None = None, | ||
| event_name: str | None = None, | ||
| exception: BaseException | None = None, | ||
| ) -> None: | ||
| pass | ||
|
|
||
|
|
@@ -266,6 +274,7 @@ def emit( | |
| body: AnyValue | None = None, | ||
| attributes: _ExtendedAttributes | None = None, | ||
| event_name: str | None = None, | ||
| exception: BaseException | None = None, | ||
| ) -> None: ... | ||
|
|
||
| @overload | ||
|
|
@@ -286,6 +295,7 @@ def emit( | |
| body: AnyValue | None = None, | ||
| attributes: _ExtendedAttributes | None = None, | ||
| event_name: str | None = None, | ||
| exception: BaseException | None = None, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: When Not a contract violation (the overloads define them as separate call forms), but it might be worth either forwarding |
||
| ) -> None: | ||
| if record: | ||
| self._logger.emit(record) | ||
|
|
@@ -299,6 +309,7 @@ def emit( | |
| body=body, | ||
| attributes=attributes, | ||
| event_name=event_name, | ||
| exception=exception, | ||
| ) | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -514,6 +514,50 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: | |||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def _get_exception_attributes( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| exception: BaseException, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> dict[str, AnyValue]: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| stacktrace = "".join( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| traceback.format_exception( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| type(exception), value=exception, tb=exception.__traceback__ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| module = type(exception).__module__ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| qualname = type(exception).__qualname__ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| exception_type = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| f"{module}.{qualname}" if module and module != "builtins" else qualname | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| exception_attributes.EXCEPTION_TYPE: exception_type, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| exception_attributes.EXCEPTION_MESSAGE: str(exception), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| exception_attributes.EXCEPTION_STACKTRACE: stacktrace, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| def _apply_exception_attributes( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| log_record: LogRecord, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| exception: BaseException | None, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if exception is None: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| exception_attributes_map = _get_exception_attributes(exception) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| attributes = log_record.attributes | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if attributes: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(attributes, BoundedAttributes): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for key, value in exception_attributes_map.items(): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if key not in attributes: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| attributes[key] = value | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+547
to
+551
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the reason for this branch? |
||||||||||||||||||||||||||||||||||||||||||||||||||
| merged = dict(attributes) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for key, value in exception_attributes_map.items(): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| merged.setdefault(key, value) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| log_record.attributes = merged | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| log_record.attributes = exception_attributes_map | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+545
to
+558
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit:
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we're 3.9 plus, I think |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| class LoggingHandler(logging.Handler): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """A handler class which writes logging records, in OTLP format, to | ||||||||||||||||||||||||||||||||||||||||||||||||||
| a network destination or file. Supports signals from the `logging` module. | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -666,20 +710,32 @@ def emit( | |||||||||||||||||||||||||||||||||||||||||||||||||
| body: AnyValue | None = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| attributes: _ExtendedAttributes | None = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| event_name: str | None = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| exception: BaseException | None = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """Emits the :class:`ReadWriteLogRecord` by setting instrumentation scope | ||||||||||||||||||||||||||||||||||||||||||||||||||
| and forwarding to the processor. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| # If a record is provided, use it directly | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if record is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| record_exception = exception or getattr(record, "exception", None) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Personally, I'm not a huge fan of modifying attributes of an already created log record. For example, if the attributes in the created log record are immutable, the |
||||||||||||||||||||||||||||||||||||||||||||||||||
| if record_exception is None and isinstance( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| record, ReadWriteLogRecord | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| record_exception = getattr( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| record.log_record, "exception", None | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(record, ReadWriteLogRecord): | ||||||||||||||||||||||||||||||||||||||||||||||||||
| _apply_exception_attributes(record, record_exception) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| # pylint:disable=protected-access | ||||||||||||||||||||||||||||||||||||||||||||||||||
| writable_record = ReadWriteLogRecord._from_api_log_record( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| record=record, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| resource=self._resource, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| instrumentation_scope=self._instrumentation_scope, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| _apply_exception_attributes( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| record.log_record, record_exception | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| writable_record = record | ||||||||||||||||||||||||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||||||||||||||||||||||||
| # Create a record from individual parameters | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -692,7 +748,9 @@ def emit( | |||||||||||||||||||||||||||||||||||||||||||||||||
| body=body, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| attributes=attributes, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| event_name=event_name, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| exception=exception, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| _apply_exception_attributes(log_record, exception) | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: If we're creating a log record from scratch (as we are here), we should be able to directly modify the attributes before constructing the record. |
||||||||||||||||||||||||||||||||||||||||||||||||||
| # pylint:disable=protected-access | ||||||||||||||||||||||||||||||||||||||||||||||||||
| writable_record = ReadWriteLogRecord._from_api_log_record( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| record=log_record, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ | |
| Logger, | ||
| LoggerProvider, | ||
| ReadableLogRecord, | ||
| ReadWriteLogRecord, | ||
| ) | ||
| from opentelemetry.sdk._logs._internal import ( | ||
| NoOpLogger, | ||
|
|
@@ -31,6 +32,7 @@ | |
| from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED | ||
| from opentelemetry.sdk.resources import Resource | ||
| from opentelemetry.sdk.util.instrumentation import InstrumentationScope | ||
| from opentelemetry.semconv.attributes import exception_attributes | ||
|
|
||
|
|
||
| class TestLoggerProvider(unittest.TestCase): | ||
|
|
@@ -214,3 +216,65 @@ def test_can_emit_with_keywords_arguments(self): | |
| self.assertEqual(result_log_record.attributes, {"some": "attributes"}) | ||
| self.assertEqual(result_log_record.event_name, "event_name") | ||
| self.assertEqual(log_data.resource, logger.resource) | ||
|
|
||
| def test_emit_with_exception_adds_attributes(self): | ||
| logger, log_record_processor_mock = self._get_logger() | ||
| exc = ValueError("boom") | ||
|
|
||
| logger.emit(body="a log line", exception=exc) | ||
| log_record_processor_mock.on_emit.assert_called_once() | ||
| log_data = log_record_processor_mock.on_emit.call_args.args[0] | ||
| attributes = dict(log_data.log_record.attributes) | ||
| self.assertEqual( | ||
| attributes[exception_attributes.EXCEPTION_TYPE], "ValueError" | ||
| ) | ||
| self.assertEqual( | ||
| attributes[exception_attributes.EXCEPTION_MESSAGE], "boom" | ||
| ) | ||
| self.assertIn( | ||
| "ValueError: boom", | ||
| attributes[exception_attributes.EXCEPTION_STACKTRACE], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: All tests create exceptions without raising them, so def test_emit_with_raised_exception_has_stacktrace(self):
logger, log_record_processor_mock = self._get_logger()
try:
raise ValueError("boom")
except ValueError as exc:
logger.emit(body="error", exception=exc)
log_data = log_record_processor_mock.on_emit.call_args.args[0]
stacktrace = dict(log_data.log_record.attributes)[exception_attributes.EXCEPTION_STACKTRACE]
self.assertIn("Traceback (most recent call last)", stacktrace)
self.assertIn("raise ValueError", stacktrace) |
||
| ) | ||
|
|
||
| def test_emit_logrecord_exception_preserves_user_attributes(self): | ||
| logger, log_record_processor_mock = self._get_logger() | ||
| exc = ValueError("boom") | ||
| log_record = LogRecord( | ||
| observed_timestamp=0, | ||
| body="a log line", | ||
| attributes={exception_attributes.EXCEPTION_TYPE: "custom"}, | ||
| exception=exc, | ||
| ) | ||
|
|
||
| logger.emit(log_record) | ||
| log_record_processor_mock.on_emit.assert_called_once() | ||
| log_data = log_record_processor_mock.on_emit.call_args.args[0] | ||
| attributes = dict(log_data.log_record.attributes) | ||
| self.assertEqual( | ||
| attributes[exception_attributes.EXCEPTION_TYPE], "custom" | ||
| ) | ||
| self.assertEqual( | ||
| attributes[exception_attributes.EXCEPTION_MESSAGE], "boom" | ||
| ) | ||
|
|
||
| def test_emit_readwrite_logrecord_uses_exception(self): | ||
| logger, log_record_processor_mock = self._get_logger() | ||
| exc = RuntimeError("kaput") | ||
| log_record = LogRecord( | ||
| observed_timestamp=0, | ||
| body="a log line", | ||
| exception=exc, | ||
| ) | ||
| readwrite = ReadWriteLogRecord( | ||
| log_record=log_record, | ||
| resource=Resource.create({}), | ||
| instrumentation_scope=logger._instrumentation_scope, | ||
| ) | ||
|
|
||
| logger.emit(readwrite) | ||
| log_record_processor_mock.on_emit.assert_called_once() | ||
| log_data = log_record_processor_mock.on_emit.call_args.args[0] | ||
| attributes = dict(log_data.log_record.attributes) | ||
| self.assertEqual( | ||
| attributes[exception_attributes.EXCEPTION_TYPE], "RuntimeError" | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This overload is deprecated