Add logger exception support for logs API/SDK#4908
Add logger exception support for logs API/SDK#4908iblancasa wants to merge 15 commits intoopen-telemetry:mainfrom
Conversation
Signed-off-by: Israel Blancas <iblancasa@gmail.com>
|
Haven't looked in detail at the PR but we are usually cautious on adding in development stuff to the api / sdk. |
|
hey @xrmx! check out open-telemetry/opentelemetry-specification#4886 if you can give this PR a basic review and "lgtm" without approving/merging it, then we can treat it as a "qualifying prototype" for marking it stable in the spec. we already have 3 qualifying prototypes which is the min bar for stabilizing the spec, but the more the better! |
Fixes #4858 Implementations / Prototypes: - Java: [`ExtendedLogRecordBuilder.setException(Throwable)`](https://github.com/open-telemetry/opentelemetry-java/blob/main/api/incubator/src/main/java/io/opentelemetry/api/incubator/logs/ExtendedLogRecordBuilder.java#L137) - Builder method called before `emit()` - Merged and available to users - .NET: [`LogRecordAttributeList.RecordException(Exception)`](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Api/Logs/LogRecordAttributeList.cs#L191-L198) - Attribute list method called before `EmitLog()` - JavaScript: [opentelemetry-js#6385](open-telemetry/opentelemetry-js#6385) - Field on LogRecord passed to `emit()` - Python: [opentelemetry-python#4908](open-telemetry/opentelemetry-python#4908) - Parameter on LogRecord passed to `emit()` (only the Python prototype doesn't yet meet the definition of [qualifying prototype](https://github.com/open-telemetry/opentelemetry-specification/blob/main/CONTRIBUTING.md#proposing-a-change) yet) Co-authored-by: Robert Pająk <pellared@hotmail.com>
Missed this comment but already approved the other PR 😅 |
nagkumar91
left a comment
There was a problem hiding this comment.
Nice implementation — spec compliance looks solid. A couple of nits below.
| body: AnyValue | None = None, | ||
| attributes: _ExtendedAttributes | None = None, | ||
| event_name: str | None = None, | ||
| exception: BaseException | None = None, |
There was a problem hiding this comment.
nit: When record is provided alongside an exception kwarg, the exception is silently dropped here (only forwarded in the else branch). The SDK's Logger.emit() explicitly handles both (exception or getattr(record, "exception", None)), so there's a subtle inconsistency between the proxy and SDK paths.
Not a contract violation (the overloads define them as separate call forms), but it might be worth either forwarding exception here too (self._logger.emit(record, exception=exception)) or adding a comment explaining why it's intentionally omitted.
| ) | ||
| self.assertIn( | ||
| "ValueError: boom", | ||
| attributes[exception_attributes.EXCEPTION_STACKTRACE], |
There was a problem hiding this comment.
nit: All tests create exceptions without raising them, so __traceback__ is None and traceback.format_exception() produces minimal output with no actual stack frames. Consider adding at least one test that raises and catches an exception to verify real stacktrace formatting:
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)| """ | ||
| # If a record is provided, use it directly | ||
| if record is not None: | ||
| record_exception = exception or getattr(record, "exception", None) |
There was a problem hiding this comment.
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 _apply_exception_attributes function will raise an exception at runtime.
| 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 | ||
| 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 |
There was a problem hiding this comment.
nit:
| 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 | |
| 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 | |
| attributes = log_record.attributes or {} | |
| if isinstance(attributes, BoundedAttributes): | |
| for key, value in exception_attributes_map.items(): | |
| if key not in attributes: | |
| attributes[key] = value | |
| return | |
| log_record.attributes = { | |
| **exception_attributes_map, | |
| **attributes, | |
| } |
There was a problem hiding this comment.
Since we're 3.9 plus, I think a | b is recommended. at least we've been doing that in GenAI. I believe it calls into native code so shoudl be much quicker
There was a problem hiding this comment.
I tried this shape, but it ended up surfacing type-check issues on this branch.
| event_name=event_name, | ||
| exception=exception, | ||
| ) | ||
| _apply_exception_attributes(log_record, exception) |
There was a problem hiding this comment.
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.
| severity_number: Optional[SeverityNumber] = None, | ||
| body: AnyValue = None, | ||
| attributes: Optional[_ExtendedAttributes] = None, | ||
| exception: Optional[BaseException] = None, |
There was a problem hiding this comment.
This overload is deprecated
| exception: Optional[BaseException] = None, |
| if isinstance(attributes, BoundedAttributes): | ||
| for key, value in exception_attributes_map.items(): | ||
| if key not in attributes: | ||
| attributes[key] = value | ||
| return |
There was a problem hiding this comment.
What's the reason for this branch?
There was a problem hiding this comment.
The branch is there to preserve BoundedAttributes behavior.
Signed-off-by: Israel Blancas <iblancasa@gmail.com>
| record_exception = getattr( | ||
| record.log_record, "exception", None | ||
| ) | ||
| if not isinstance(record, ReadWriteLogRecord): |
There was a problem hiding this comment.
Might not be relevant to this PR but do we know what this instance check is here? Doesn't emit expect a LogRecord and not a ReadWriteLogRecord? When would the latter be ever passed in?
There was a problem hiding this comment.
ReadWriteLogRecord is an SDK-internal type, so (I guess) this branch is just a defensive fast path for already-materialized SDK records, while the public Logger.emit() API still expects callers to pass a LogRecord.
Description
Fixes #4907
Type of change
Please delete options that are not relevant.
How Has This Been Tested?
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
Does This PR Require a Contrib Repo Change?
Checklist: