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

Remove double record exception #712

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

dmontagu
Copy link
Contributor

@dmontagu dmontagu commented Dec 20, 2024

We are currently double-recording exceptions in LogfireSpan's due to some convoluted logic that this PR simplifies

Copy link

cloudflare-workers-and-pages bot commented Dec 20, 2024

Deploying logfire-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: fe7d66b
Status: ✅  Deploy successful!
Preview URL: https://e6902e5a.logfire-docs.pages.dev
Branch Preview URL: https://remove-double-record-excepti.logfire-docs.pages.dev

View logs

@@ -1948,13 +1948,13 @@ def __init__(self, span: trace_api.Span) -> None:
self._token = context_api.attach(trace_api.set_span_in_context(self._span))

def __enter__(self) -> FastLogfireSpan:
self._span.__enter__()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is maybe unnecessary but seems reasonable to add, and I hope this class will be refactored/simplified soon anyway.

Copy link
Contributor

Choose a reason for hiding this comment

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

Given how optimised this class is, I'd rather not add it here, but maybe it can stay in LogfireSpan.

@@ -1990,6 +1990,7 @@ def __enter__(self) -> LogfireSpan:
attributes=self._otlp_attributes,
links=self._links,
)
self._span.__enter__()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same note as above, this might not be necessary but seems fine to add and arguably more correct(?)

Comment on lines +2007 to +2012
if self._span.is_recording():
with handle_internal_errors():
if self._added_attributes:
self._span.set_attribute(
ATTRIBUTES_JSON_SCHEMA_KEY, attributes_json_schema(self._json_schema_properties)
)
Copy link
Contributor Author

@dmontagu dmontagu Dec 20, 2024

Choose a reason for hiding this comment

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

I just moved the logic from end() here, noting we can drop a check since we already know self._span is not None, and we don't currently rely on the LogfireSpan methods being called at the same time that the otel methods would be called.

Comment on lines 175 to 176
if self.is_recording() and isinstance(exc_value, BaseException):
self.record_exception(exc_value, escaped=True)
Copy link
Contributor Author

@dmontagu dmontagu Dec 20, 2024

Choose a reason for hiding this comment

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

This is basically just the logic that used to be in main._exit_span. The parent class's version of this just calls self.end(), and I kept that below.

Comment on lines 3178 to 3195
def test_logfire_span_records_exceptions_once():
# logfire.configure(send_to_logfire=False, console=False, additional_span_processors=[BatchSpanProcessor(exporter)])
n_calls_to_record_exception = 0

def patched_record_exception(*args: Any, **kwargs: Any) -> Any:
nonlocal n_calls_to_record_exception
n_calls_to_record_exception += 1

return record_exception(*args, **kwargs)

with patch('logfire._internal.tracer.record_exception', patched_record_exception), patch(
'logfire._internal.main.record_exception', patched_record_exception
):
with pytest.raises(RuntimeError):
with logfire.span('foo'):
raise RuntimeError('error')

assert n_calls_to_record_exception == 1
Copy link
Contributor Author

@dmontagu dmontagu Dec 20, 2024

Choose a reason for hiding this comment

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

I confirmed this test fails on main and makes two calls instead of one.

Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like the doubling was introduced by #696

Copy link

codecov bot commented Dec 20, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 100.00%. Comparing base (54ff986) to head (fe7d66b).

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #712   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          137       137           
  Lines        11031     11041   +10     
  Branches      1539      1539           
=========================================
+ Hits         11031     11041   +10     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

ATTRIBUTES_JSON_SCHEMA_KEY, attributes_json_schema(self._json_schema_properties)
)

self._span.end(end_time)
Copy link
Contributor Author

@dmontagu dmontagu Dec 20, 2024

Choose a reason for hiding this comment

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

We used to only call self._span.end here if self._span was recording. Is that actually the right behavior though? The changes I made ensure that end() always gets called, even on a non-recording span, and that's why the timestamps changed below (because it generates another timestamp in the unconditional call to end()).

I can go back to only calling span.end() if the span is recording but I'm not sure if that's correct..

Copy link
Contributor

Choose a reason for hiding this comment

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

import opentelemetry.trace

import logfire

logfire.configure()

tracer = opentelemetry.trace.get_tracer(__name__)
span = tracer.start_span('foo')
assert span.is_recording()
span.end()
assert not span.is_recording()
span.end()  # Logs a warning: Calling end() on an ended span.

So keeping the current behaviour seems best.

Comment on lines 175 to 178
if self.is_recording() and isinstance(exc_value, BaseException):
self.record_exception(exc_value, escaped=True)

self.end()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Suggested change
if self.is_recording() and isinstance(exc_value, BaseException):
self.record_exception(exc_value, escaped=True)
self.end()
if self.is_recording():
if isinstance(exc_value, BaseException):
self.record_exception(exc_value, escaped=True)
self.end()

I confirmed that this would get us back to the old timestamp behavior, again, not sure if this is better or worse than the version currently in this PR. But I think one of these two options should be okay

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I decided to make this change because, 1, it preserves the old behavior, and 2, I think it marginally reduces overhead. We can always just dedent the call to self.end() if we think the method should be called even if the inner span is not a recording span.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants