Skip to content

Commit e64ce23

Browse files
committed
feat: implement on ending in span processor
1 parent be1575c commit e64ce23

File tree

4 files changed

+140
-0
lines changed

4 files changed

+140
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414

1515
- docs: Added sqlcommenter example
1616
([#4734](https://github.com/open-telemetry/opentelemetry-python/pull/4734))
17+
- feat: implement on ending in span processor
18+
([#4775](https://github.com/open-telemetry/opentelemetry-python/pull/4775))
1719

1820
## Version 1.38.0/0.59b0 (2025-10-16)
1921

opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,16 @@ def on_start(
113113
parent_context: The parent context of the span that just started.
114114
"""
115115

116+
def _on_ending(self, span: "Span") -> None:
117+
"""Called when a :class:`opentelemetry.trace.Span` is ending.
118+
119+
This method is called synchronously on the thread that ends the
120+
span, therefore it should not block or throw an exception.
121+
122+
Args:
123+
span: The :class:`opentelemetry.trace.Span` that is ending.
124+
"""
125+
116126
def on_end(self, span: "ReadableSpan") -> None:
117127
"""Called when a :class:`opentelemetry.trace.Span` is ended.
118128
@@ -170,6 +180,10 @@ def on_start(
170180
for sp in self._span_processors:
171181
sp.on_start(span, parent_context=parent_context)
172182

183+
def _on_ending(self, span: "Span") -> None:
184+
for sp in self._span_processors:
185+
sp._on_ending(span)
186+
173187
def on_end(self, span: "ReadableSpan") -> None:
174188
for sp in self._span_processors:
175189
sp.on_end(span)
@@ -254,6 +268,9 @@ def on_start(
254268
lambda sp: sp.on_start, span, parent_context=parent_context
255269
)
256270

271+
def _on_ending(self, span: "Span") -> None:
272+
self._submit_and_await(lambda sp: sp._on_ending, span)
273+
257274
def on_end(self, span: "ReadableSpan") -> None:
258275
self._submit_and_await(lambda sp: sp.on_end, span)
259276

@@ -945,6 +962,7 @@ def end(self, end_time: Optional[int] = None) -> None:
945962

946963
self._end_time = end_time if end_time is not None else time_ns()
947964

965+
self._span_processor._on_ending(self)
948966
self._span_processor.on_end(self._readable_span())
949967

950968
@_check_span_ended

opentelemetry-sdk/tests/trace/test_span_processor.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ def span_event_start_fmt(span_processor_name, span_name):
3232
return span_processor_name + ":" + span_name + ":start"
3333

3434

35+
def span_event_ending_fmt(span_processor_name, span_name):
36+
return span_processor_name + ":" + span_name + ":ending"
37+
38+
3539
def span_event_end_fmt(span_processor_name, span_name):
3640
return span_processor_name + ":" + span_name + ":end"
3741

@@ -50,6 +54,11 @@ def on_end(self, span: "trace.Span") -> None:
5054
self.span_list.append(span_event_end_fmt(self.name, span.name))
5155

5256

57+
class MyExtendedSpanProcessor(MySpanProcessor):
58+
def _on_ending(self, span: "trace.Span") -> None:
59+
self.span_list.append(span_event_ending_fmt(self.name, span.name))
60+
61+
5362
class TestSpanProcessor(unittest.TestCase):
5463
def test_span_processor(self):
5564
tracer_provider = trace.TracerProvider()
@@ -120,6 +129,84 @@ def test_span_processor(self):
120129
# compare if two lists are the same
121130
self.assertListEqual(spans_calls_list, expected_list)
122131

132+
def test_span_processor_with_on_ending(self):
133+
tracer_provider = trace.TracerProvider()
134+
tracer = tracer_provider.get_tracer(__name__)
135+
136+
spans_calls_list = [] # filled by MySpanProcessor
137+
expected_list = [] # filled by hand
138+
139+
# Span processors are created but not added to the tracer yet
140+
sp1 = MyExtendedSpanProcessor("SP1", spans_calls_list)
141+
sp2 = MyExtendedSpanProcessor("SP2", spans_calls_list)
142+
143+
with tracer.start_as_current_span("foo"):
144+
with tracer.start_as_current_span("bar"):
145+
with tracer.start_as_current_span("baz"):
146+
pass
147+
148+
# at this point lists must be empty
149+
self.assertEqual(len(spans_calls_list), 0)
150+
151+
# add single span processor
152+
tracer_provider.add_span_processor(sp1)
153+
154+
with tracer.start_as_current_span("foo"):
155+
expected_list.append(span_event_start_fmt("SP1", "foo"))
156+
157+
with tracer.start_as_current_span("bar"):
158+
expected_list.append(span_event_start_fmt("SP1", "bar"))
159+
160+
with tracer.start_as_current_span("baz"):
161+
expected_list.append(span_event_start_fmt("SP1", "baz"))
162+
163+
expected_list.append(span_event_ending_fmt("SP1", "baz"))
164+
expected_list.append(span_event_end_fmt("SP1", "baz"))
165+
166+
expected_list.append(span_event_ending_fmt("SP1", "bar"))
167+
expected_list.append(span_event_end_fmt("SP1", "bar"))
168+
169+
expected_list.append(span_event_ending_fmt("SP1", "foo"))
170+
expected_list.append(span_event_end_fmt("SP1", "foo"))
171+
172+
self.assertListEqual(spans_calls_list, expected_list)
173+
174+
spans_calls_list.clear()
175+
expected_list.clear()
176+
177+
# go for multiple span processors
178+
tracer_provider.add_span_processor(sp2)
179+
180+
with tracer.start_as_current_span("foo"):
181+
expected_list.append(span_event_start_fmt("SP1", "foo"))
182+
expected_list.append(span_event_start_fmt("SP2", "foo"))
183+
184+
with tracer.start_as_current_span("bar"):
185+
expected_list.append(span_event_start_fmt("SP1", "bar"))
186+
expected_list.append(span_event_start_fmt("SP2", "bar"))
187+
188+
with tracer.start_as_current_span("baz"):
189+
expected_list.append(span_event_start_fmt("SP1", "baz"))
190+
expected_list.append(span_event_start_fmt("SP2", "baz"))
191+
192+
expected_list.append(span_event_ending_fmt("SP1", "baz"))
193+
expected_list.append(span_event_ending_fmt("SP2", "baz"))
194+
expected_list.append(span_event_end_fmt("SP1", "baz"))
195+
expected_list.append(span_event_end_fmt("SP2", "baz"))
196+
197+
expected_list.append(span_event_ending_fmt("SP1", "bar"))
198+
expected_list.append(span_event_ending_fmt("SP2", "bar"))
199+
expected_list.append(span_event_end_fmt("SP1", "bar"))
200+
expected_list.append(span_event_end_fmt("SP2", "bar"))
201+
202+
expected_list.append(span_event_ending_fmt("SP1", "foo"))
203+
expected_list.append(span_event_ending_fmt("SP2", "foo"))
204+
expected_list.append(span_event_end_fmt("SP1", "foo"))
205+
expected_list.append(span_event_end_fmt("SP2", "foo"))
206+
207+
# compare if two lists are the same
208+
self.assertListEqual(spans_calls_list, expected_list)
209+
123210
def test_add_span_processor_after_span_creation(self):
124211
tracer_provider = trace.TracerProvider()
125212
tracer = tracer_provider.get_tracer(__name__)
@@ -176,6 +263,20 @@ def test_on_start(self):
176263
)
177264
multi_processor.shutdown()
178265

266+
def test_on_ending(self):
267+
multi_processor = self.create_multi_span_processor()
268+
269+
mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(0, 5)]
270+
for mock_processor in mocks:
271+
multi_processor.add_span_processor(mock_processor)
272+
273+
span = self.create_default_span()
274+
multi_processor._on_ending(span)
275+
276+
for mock_processor in mocks:
277+
mock_processor._on_ending.assert_called_once_with(span)
278+
multi_processor.shutdown()
279+
179280
def test_on_end(self):
180281
multi_processor = self.create_multi_span_processor()
181282

opentelemetry-sdk/tests/trace/test_trace.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,6 +1428,10 @@ def span_event_start_fmt(span_processor_name, span_name):
14281428
return span_processor_name + ":" + span_name + ":start"
14291429

14301430

1431+
def span_event_ending_fmt(span_processor_name, span_name):
1432+
return span_processor_name + ":" + span_name + ":ending"
1433+
1434+
14311435
def span_event_end_fmt(span_processor_name, span_name):
14321436
return span_processor_name + ":" + span_name + ":end"
14331437

@@ -1442,6 +1446,9 @@ def on_start(
14421446
) -> None:
14431447
self.span_list.append(span_event_start_fmt(self.name, span.name))
14441448

1449+
def _on_ending(self, span: "trace.ReadableSpan") -> None:
1450+
self.span_list.append(span_event_ending_fmt(self.name, span.name))
1451+
14451452
def on_end(self, span: "trace.ReadableSpan") -> None:
14461453
self.span_list.append(span_event_end_fmt(self.name, span.name))
14471454

@@ -1478,10 +1485,13 @@ def test_span_processor(self):
14781485
with tracer.start_as_current_span("baz"):
14791486
expected_list.append(span_event_start_fmt("SP1", "baz"))
14801487

1488+
expected_list.append(span_event_ending_fmt("SP1", "baz"))
14811489
expected_list.append(span_event_end_fmt("SP1", "baz"))
14821490

1491+
expected_list.append(span_event_ending_fmt("SP1", "bar"))
14831492
expected_list.append(span_event_end_fmt("SP1", "bar"))
14841493

1494+
expected_list.append(span_event_ending_fmt("SP1", "foo"))
14851495
expected_list.append(span_event_end_fmt("SP1", "foo"))
14861496

14871497
self.assertListEqual(spans_calls_list, expected_list)
@@ -1504,12 +1514,18 @@ def test_span_processor(self):
15041514
expected_list.append(span_event_start_fmt("SP1", "baz"))
15051515
expected_list.append(span_event_start_fmt("SP2", "baz"))
15061516

1517+
expected_list.append(span_event_ending_fmt("SP1", "baz"))
1518+
expected_list.append(span_event_ending_fmt("SP2", "baz"))
15071519
expected_list.append(span_event_end_fmt("SP1", "baz"))
15081520
expected_list.append(span_event_end_fmt("SP2", "baz"))
15091521

1522+
expected_list.append(span_event_ending_fmt("SP1", "bar"))
1523+
expected_list.append(span_event_ending_fmt("SP2", "bar"))
15101524
expected_list.append(span_event_end_fmt("SP1", "bar"))
15111525
expected_list.append(span_event_end_fmt("SP2", "bar"))
15121526

1527+
expected_list.append(span_event_ending_fmt("SP1", "foo"))
1528+
expected_list.append(span_event_ending_fmt("SP2", "foo"))
15131529
expected_list.append(span_event_end_fmt("SP1", "foo"))
15141530
expected_list.append(span_event_end_fmt("SP2", "foo"))
15151531

@@ -1532,10 +1548,13 @@ def test_add_span_processor_after_span_creation(self):
15321548
# add span processor after spans have been created
15331549
tracer_provider.add_span_processor(sp)
15341550

1551+
expected_list.append(span_event_ending_fmt("SP1", "baz"))
15351552
expected_list.append(span_event_end_fmt("SP1", "baz"))
15361553

1554+
expected_list.append(span_event_ending_fmt("SP1", "bar"))
15371555
expected_list.append(span_event_end_fmt("SP1", "bar"))
15381556

1557+
expected_list.append(span_event_ending_fmt("SP1", "foo"))
15391558
expected_list.append(span_event_end_fmt("SP1", "foo"))
15401559

15411560
self.assertListEqual(spans_calls_list, expected_list)

0 commit comments

Comments
 (0)