Skip to content

Commit 56f6d8e

Browse files
committed
feat: implement on ending in span processor
1 parent 5307dd0 commit 56f6d8e

File tree

5 files changed

+221
-0
lines changed

5 files changed

+221
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
- Add `opentelemetry-exporter-credential-provider-gcp` as an optional dependency to `opentelemetry-exporter-otlp-proto-grpc`
2222
and `opentelemetry-exporter-otlp-proto-http`
2323
([#4760](https://github.com/open-telemetry/opentelemetry-python/pull/4760))
24+
- feat: implement on ending in span processor
25+
([#4775](https://github.com/open-telemetry/opentelemetry-python/pull/4775))
2426
- semantic-conventions: Bump to 1.38.0
2527
([#4791](https://github.com/open-telemetry/opentelemetry-python/pull/4791))
2628
- [BREAKING] Remove LogData and extend SDK LogRecord to have instrumentation scope

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

Lines changed: 21 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,11 @@ 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+
# pylint: disable=protected-access
186+
sp._on_ending(span)
187+
173188
def on_end(self, span: "ReadableSpan") -> None:
174189
for sp in self._span_processors:
175190
sp.on_end(span)
@@ -254,6 +269,10 @@ def on_start(
254269
lambda sp: sp.on_start, span, parent_context=parent_context
255270
)
256271

272+
def _on_ending(self, span: "Span") -> None:
273+
# pylint: disable=protected-access
274+
self._submit_and_await(lambda sp: sp._on_ending, span)
275+
257276
def on_end(self, span: "ReadableSpan") -> None:
258277
self._submit_and_await(lambda sp: sp.on_end, span)
259278

@@ -945,6 +964,8 @@ def end(self, end_time: Optional[int] = None) -> None:
945964

946965
self._end_time = end_time if end_time is not None else time_ns()
947966

967+
# pylint: disable=protected-access
968+
self._span_processor._on_ending(self)
948969
self._span_processor.on_end(self._readable_span())
949970

950971
@_check_span_ended

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ def on_start(
101101
) -> None:
102102
pass
103103

104+
def _on_ending(self, span: Span) -> None:
105+
pass
106+
104107
def on_end(self, span: ReadableSpan) -> None:
105108
if not (span.context and span.context.trace_flags.sampled):
106109
return
@@ -187,6 +190,9 @@ def on_start(
187190
) -> None:
188191
pass
189192

193+
def _on_ending(self, span: Span) -> None:
194+
pass
195+
190196
def on_end(self, span: ReadableSpan) -> None:
191197
if not (span.context and span.context.trace_flags.sampled):
192198
return

opentelemetry-sdk/tests/trace/test_span_processor.py

Lines changed: 172 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,85 @@ def test_span_processor(self):
120129
# compare if two lists are the same
121130
self.assertListEqual(spans_calls_list, expected_list)
122131

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

145233
self.assertListEqual(spans_calls_list, expected_list)
146234

235+
def test_on_ending_not_implemented_does_not_raise(self):
236+
tracer_provider = trace.TracerProvider()
237+
tracer = tracer_provider.get_tracer(__name__)
238+
239+
spans_calls_list = [] # filled by MySpanProcessor
240+
expected_list = [] # filled by hand
241+
242+
# Does not implement _on_ending
243+
sp = MySpanProcessor("SP1", spans_calls_list)
244+
tracer_provider.add_span_processor(sp)
245+
246+
try:
247+
with tracer.start_as_current_span("foo"):
248+
expected_list.append(span_event_start_fmt("SP1", "foo"))
249+
250+
with tracer.start_as_current_span("bar"):
251+
expected_list.append(span_event_start_fmt("SP1", "bar"))
252+
253+
with tracer.start_as_current_span("baz"):
254+
expected_list.append(
255+
span_event_start_fmt("SP1", "baz")
256+
)
257+
258+
expected_list.append(span_event_end_fmt("SP1", "baz"))
259+
expected_list.append(span_event_end_fmt("SP1", "bar"))
260+
expected_list.append(span_event_end_fmt("SP1", "foo"))
261+
except NotImplementedError:
262+
self.fail("_on_ending() should not raise an exception")
263+
264+
self.assertListEqual(spans_calls_list, expected_list)
265+
147266

148267
class MultiSpanProcessorTestBase(abc.ABC):
149268
@abc.abstractmethod
@@ -176,6 +295,22 @@ def test_on_start(self):
176295
)
177296
multi_processor.shutdown()
178297

298+
def test_on_ending(self):
299+
multi_processor = self.create_multi_span_processor()
300+
301+
mocks = [mock.Mock(spec=trace.SpanProcessor) for _ in range(0, 5)]
302+
for mock_processor in mocks:
303+
multi_processor.add_span_processor(mock_processor)
304+
305+
span = self.create_default_span()
306+
# pylint: disable=protected-access
307+
multi_processor._on_ending(span)
308+
309+
for mock_processor in mocks:
310+
# pylint: disable=protected-access
311+
mock_processor._on_ending.assert_called_once_with(span)
312+
multi_processor.shutdown()
313+
179314
def test_on_end(self):
180315
multi_processor = self.create_multi_span_processor()
181316

@@ -219,6 +354,43 @@ def test_force_flush(self):
219354
self.assertEqual(1, mock_processor.force_flush.call_count)
220355
multi_processor.shutdown()
221356

357+
def test_on_ending_not_implemented_does_not_raise(self):
358+
tracer_provider = trace.TracerProvider()
359+
tracer = tracer_provider.get_tracer(__name__)
360+
361+
spans_calls_list = [] # filled by MySpanProcessor
362+
expected_list = [] # filled by hand
363+
364+
multi_processor = self.create_multi_span_processor()
365+
# Does not implement _on_ending
366+
multi_processor.add_span_processor(
367+
MySpanProcessor("SP1", spans_calls_list)
368+
)
369+
370+
tracer_provider.add_span_processor(multi_processor)
371+
372+
try:
373+
with tracer.start_as_current_span("foo"):
374+
expected_list.append(span_event_start_fmt("SP1", "foo"))
375+
376+
with tracer.start_as_current_span("bar"):
377+
expected_list.append(span_event_start_fmt("SP1", "bar"))
378+
379+
with tracer.start_as_current_span("baz"):
380+
expected_list.append(
381+
span_event_start_fmt("SP1", "baz")
382+
)
383+
384+
expected_list.append(span_event_end_fmt("SP1", "baz"))
385+
expected_list.append(span_event_end_fmt("SP1", "bar"))
386+
expected_list.append(span_event_end_fmt("SP1", "foo"))
387+
except NotImplementedError:
388+
# pylint: disable=no-member
389+
self.fail("_on_ending() should not raise an exception")
390+
391+
# pylint: disable=no-member
392+
self.assertListEqual(spans_calls_list, expected_list)
393+
222394

223395
class TestSynchronousMultiSpanProcessor(
224396
MultiSpanProcessorTestBase, unittest.TestCase

opentelemetry-sdk/tests/trace/test_trace.py

Lines changed: 20 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,11 +1446,15 @@ 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

14481455

14491456
class TestSpanProcessor(unittest.TestCase):
1457+
# pylint: disable=too-many-statements
14501458
def test_span_processor(self):
14511459
tracer_provider = trace.TracerProvider()
14521460
tracer = tracer_provider.get_tracer(__name__)
@@ -1478,10 +1486,13 @@ def test_span_processor(self):
14781486
with tracer.start_as_current_span("baz"):
14791487
expected_list.append(span_event_start_fmt("SP1", "baz"))
14801488

1489+
expected_list.append(span_event_ending_fmt("SP1", "baz"))
14811490
expected_list.append(span_event_end_fmt("SP1", "baz"))
14821491

1492+
expected_list.append(span_event_ending_fmt("SP1", "bar"))
14831493
expected_list.append(span_event_end_fmt("SP1", "bar"))
14841494

1495+
expected_list.append(span_event_ending_fmt("SP1", "foo"))
14851496
expected_list.append(span_event_end_fmt("SP1", "foo"))
14861497

14871498
self.assertListEqual(spans_calls_list, expected_list)
@@ -1504,12 +1515,18 @@ def test_span_processor(self):
15041515
expected_list.append(span_event_start_fmt("SP1", "baz"))
15051516
expected_list.append(span_event_start_fmt("SP2", "baz"))
15061517

1518+
expected_list.append(span_event_ending_fmt("SP1", "baz"))
1519+
expected_list.append(span_event_ending_fmt("SP2", "baz"))
15071520
expected_list.append(span_event_end_fmt("SP1", "baz"))
15081521
expected_list.append(span_event_end_fmt("SP2", "baz"))
15091522

1523+
expected_list.append(span_event_ending_fmt("SP1", "bar"))
1524+
expected_list.append(span_event_ending_fmt("SP2", "bar"))
15101525
expected_list.append(span_event_end_fmt("SP1", "bar"))
15111526
expected_list.append(span_event_end_fmt("SP2", "bar"))
15121527

1528+
expected_list.append(span_event_ending_fmt("SP1", "foo"))
1529+
expected_list.append(span_event_ending_fmt("SP2", "foo"))
15131530
expected_list.append(span_event_end_fmt("SP1", "foo"))
15141531
expected_list.append(span_event_end_fmt("SP2", "foo"))
15151532

@@ -1532,10 +1549,13 @@ def test_add_span_processor_after_span_creation(self):
15321549
# add span processor after spans have been created
15331550
tracer_provider.add_span_processor(sp)
15341551

1552+
expected_list.append(span_event_ending_fmt("SP1", "baz"))
15351553
expected_list.append(span_event_end_fmt("SP1", "baz"))
15361554

1555+
expected_list.append(span_event_ending_fmt("SP1", "bar"))
15371556
expected_list.append(span_event_end_fmt("SP1", "bar"))
15381557

1558+
expected_list.append(span_event_ending_fmt("SP1", "foo"))
15391559
expected_list.append(span_event_end_fmt("SP1", "foo"))
15401560

15411561
self.assertListEqual(spans_calls_list, expected_list)

0 commit comments

Comments
 (0)