Skip to content

ref: Allow to start and finish StreamedSpans (9)#5598

Draft
sentrivana wants to merge 12 commits intoivana/span-first-8-bucket-based-limits-in-batcherfrom
ivana/span-first-9-start-end
Draft

ref: Allow to start and finish StreamedSpans (9)#5598
sentrivana wants to merge 12 commits intoivana/span-first-8-bucket-based-limits-in-batcherfrom
ivana/span-first-9-start-end

Conversation

@sentrivana
Copy link
Contributor

@sentrivana sentrivana commented Mar 6, 2026

Description

This PR adds logic for starting and finishing spans, both via a context manager with start_span(...), as well as directly with span = start_span(...) and span.end().

Summary

  • Added internal _start() and _end() functions that handle the starting and ending logic.
    • _start() handles keeping track of what was called context_manager_state on old Spans, i.e., remembering which span was set on the scope before this span so that it can be restored once this span is over.
    • _end() handles restoring the old span, setting the end timestamp, adding final attributes on it, and queueing it in the span batcher.
  • Added __enter__ and __exit__ methods on both StreamedSpan and NoOpStreamedSpan. These call the internal methods above, with __exit__ additionally setting the span status to error if it encountered one.
  • Added a new span.end() public function that allows to set an optional end_timestamp. If provided, it sets it and calls _end() internally.
  • There is a span.finish() too, which is deprecated from the get go, just to make the migration to span first a bit easier (since the method is called finish() on legacy spans).

Issues

Reminders

@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


This PR will not appear in the changelog.


🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

Codecov Results 📊

14 passed | Total: 14 | Pass Rate: 100% | Execution Time: 3.70s

📊 Comparison with Base Branch

Metric Change
Total Tests
Passed Tests
Failed Tests
Skipped Tests

✨ No test changes detected

All tests are passing successfully.

❌ Patch coverage is 23.60%. Project has 15139 uncovered lines.
❌ Project coverage is 24.68%. Comparing base (base) to head (head).

Files with missing lines (2)
File Patch % Lines
traces.py 41.48% ⚠️ 134 Missing
_span_batcher.py 31.94% ⚠️ 49 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
- Coverage    25.17%    24.68%    -0.49%
==========================================
  Files          189       189         —
  Lines        19924     20100      +176
  Branches      6458      6500       +42
==========================================
+ Hits          5015      4961       -54
- Misses       14909     15139      +230
- Partials       387       381        -6

Generated by Codecov Action

Comment on lines +97 to +98
if item._timestamp:
res["end_timestamp"] = item._timestamp.timestamp()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no scenario where a span that's already in the span batcher wouldn't have an end _timestamp, but mypy doesn't know that. 🤡

try:
# profiling depends on this value and requires that
# it is measured in nanoseconds
self._start_timestamp_monotonic_ns = nanosecond_time()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Anything related to _start_timestamp_monotonic_ns is just copy-pasted from the Span implementation

"""
self._end(end_timestamp)

def finish(self, end_timestamp: "Optional[Union[float, datetime]]" = None) -> None:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just here for compat reasons, to make the transition from the old API easier (very slightly).

self.end(end_timestamp)

def _start(self) -> None:
if self._active:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

_active basically controls whether a span will be set on the scope, so if it's False, we don't need to keep track of the old parent span to restore it later, because we're never replacing it.

Comment on lines +447 to +452
if self._scope is None:
return

old_span = self._scope.span
self._scope.span = self
self._previous_span_on_scope = old_span
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unlike the main span class, no-op spans don't have an active flag. The user cannot control whether a no-op span will be set on the scope; the SDK does this internally, and it uses the _scope attribute to remember the decision. In the case of normal spans, the _scope attribute plays a different role: they all need a scope, regardless of whether they'll actually be set on it or not, because they will be captured using that scope in either case.

Coming back to no-op spans, the decision whether to set them on the scope or not is tightly coupled to how we want ignore_spans to work (coming in a future PR). Basically, if a segment/root span would be ignored, all of its children should be as well. That's easiest to accomplish if we actually set the root no-op span on the scope. On the other hand, if a non-root span is ignored, it's not set on scope, so that any children spans effectively use the last not ignored span that's set on the scope as parent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant