Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 42acdb9

Browse files
mauriciovasquezbernalOberon00
authored andcommitted
sdk/trace: add SpanProcessor (open-telemetry#115)
SpanProcessor is an interface that allows to register hooks for Span start and end invocations. This commit adds the SpanProcessor interface to the SDK as well as the MultiSpanProcessor that allows to register multiple processors.
1 parent 15039de commit 42acdb9

File tree

2 files changed

+198
-1
lines changed

2 files changed

+198
-1
lines changed

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

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,68 @@ def from_map(cls, maxlen, mapping):
145145
Link = namedtuple("Link", ("context", "attributes"))
146146

147147

148+
class SpanProcessor:
149+
"""Interface which allows hooks for SDK's `Span`s start and end method
150+
invocations.
151+
152+
Span processors can be registered directly using
153+
:func:`~Tracer:add_span_processor` and they are invoked in the same order
154+
as they were registered.
155+
"""
156+
157+
def on_start(self, span: "Span") -> None:
158+
"""Called when a :class:`Span` is started.
159+
160+
This method is called synchronously on the thread that starts the
161+
span, therefore it should not block or throw an exception.
162+
163+
Args:
164+
span: The :class:`Span` that just started.
165+
"""
166+
167+
def on_end(self, span: "Span") -> None:
168+
"""Called when a :class:`Span` is ended.
169+
170+
This method is called synchronously on the thread that ends the
171+
span, therefore it should not block or throw an exception.
172+
173+
Args:
174+
span: The :class:`Span` that just ended.
175+
"""
176+
177+
def shutdown(self) -> None:
178+
"""Called when a :class:`Tracer` is shutdown."""
179+
180+
181+
class MultiSpanProcessor(SpanProcessor):
182+
"""Implementation of :class:`SpanProcessor` that forwards all received
183+
events to a list of `SpanProcessor`.
184+
"""
185+
186+
def __init__(self):
187+
# use a tuple to avoid race conditions when adding a new span and
188+
# iterating through it on "on_start" and "on_end".
189+
self._span_processors = ()
190+
self._lock = threading.Lock()
191+
192+
def add_span_processor(self, span_processor: SpanProcessor):
193+
"""Adds a SpanProcessor to the list handled by this instance."""
194+
with self._lock:
195+
self._span_processors = self._span_processors + (span_processor,)
196+
197+
def on_start(self, span: "Span") -> None:
198+
for sp in self._span_processors:
199+
sp.on_start(span)
200+
201+
def on_end(self, span: "Span") -> None:
202+
for sp in self._span_processors:
203+
sp.on_end(span)
204+
205+
def shutdown(self) -> None:
206+
for sp in self._span_processors:
207+
sp.shutdown()
208+
209+
148210
class Span(trace_api.Span):
149211
"""See `opentelemetry.trace.Span`.
150212
@@ -161,6 +223,8 @@ class Span(trace_api.Span):
161223
attributes: The span's attributes to be exported
162224
events: Timestamped events to be exported
163225
links: Links to other spans to be exported
226+
span_processor: `SpanProcessor` to invoke when starting and ending
227+
this `Span`.
164228
"""
165229

166230
# Initialize these lazily assuming most spans won't have them.
@@ -179,6 +243,7 @@ def __init__(
179243
attributes: types.Attributes = None, # TODO
180244
events: typing.Sequence[Event] = None, # TODO
181245
links: typing.Sequence[Link] = None, # TODO
246+
span_processor: SpanProcessor = SpanProcessor(),
182247
) -> None:
183248

184249
self.name = name
@@ -190,6 +255,7 @@ def __init__(
190255
self.attributes = attributes
191256
self.events = events
192257
self.links = links
258+
self.span_processor = span_processor
193259

194260
if attributes is None:
195261
self.attributes = Span.empty_attributes
@@ -247,10 +313,12 @@ def add_link(
247313
def start(self):
248314
if self.start_time is None:
249315
self.start_time = util.time_ns()
316+
self.span_processor.on_start(self)
250317

251318
def end(self):
252319
if self.end_time is None:
253320
self.end_time = util.time_ns()
321+
self.span_processor.on_end(self)
254322

255323
def update_name(self, name: str) -> None:
256324
self.name = name
@@ -286,6 +354,7 @@ def __init__(self, name: str = "") -> None:
286354
if name:
287355
slot_name = "{}.current_span".format(name)
288356
self._current_span_slot = Context.register_slot(slot_name)
357+
self._active_span_processor = MultiSpanProcessor()
289358

290359
def get_current_span(self):
291360
"""See `opentelemetry.trace.Tracer.get_current_span`."""
@@ -325,7 +394,12 @@ def create_span(
325394
parent_context.trace_options,
326395
parent_context.trace_state,
327396
)
328-
return Span(name=name, context=context, parent=parent)
397+
return Span(
398+
name=name,
399+
context=context,
400+
parent=parent,
401+
span_processor=self._active_span_processor,
402+
)
329403

330404
@contextmanager
331405
def use_span(self, span: "Span") -> typing.Iterator["Span"]:
@@ -339,5 +413,15 @@ def use_span(self, span: "Span") -> typing.Iterator["Span"]:
339413
self._current_span_slot.set(span_snapshot)
340414
span.end()
341415

416+
def add_span_processor(self, span_processor: SpanProcessor) -> None:
417+
"""Registers a new :class:`SpanProcessor` for this `Tracer`.
418+
419+
The span processors are invoked in the same order they are registered.
420+
"""
421+
422+
# no lock here because MultiSpanProcessor.add_span_processor is
423+
# thread safe
424+
self._active_span_processor.add_span_processor(span_processor)
425+
342426

343427
tracer = Tracer()

opentelemetry-sdk/tests/trace/test_trace.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,116 @@ class TestSpan(unittest.TestCase):
198198
def test_basic_span(self):
199199
span = trace.Span("name", mock.Mock(spec=trace_api.SpanContext))
200200
self.assertEqual(span.name, "name")
201+
202+
203+
def span_event_start_fmt(span_processor_name, span_name):
204+
return span_processor_name + ":" + span_name + ":start"
205+
206+
207+
def span_event_end_fmt(span_processor_name, span_name):
208+
return span_processor_name + ":" + span_name + ":end"
209+
210+
211+
class MySpanProcessor(trace.SpanProcessor):
212+
def __init__(self, name, span_list):
213+
self.name = name
214+
self.span_list = span_list
215+
216+
def on_start(self, span: "trace.Span") -> None:
217+
self.span_list.append(span_event_start_fmt(self.name, span.name))
218+
219+
def on_end(self, span: "trace.Span") -> None:
220+
self.span_list.append(span_event_end_fmt(self.name, span.name))
221+
222+
223+
class TestSpanProcessor(unittest.TestCase):
224+
def test_span_processor(self):
225+
tracer = trace.Tracer()
226+
227+
spans_calls_list = [] # filled by MySpanProcessor
228+
expected_list = [] # filled by hand
229+
230+
# Span processors are created but not added to the tracer yet
231+
sp1 = MySpanProcessor("SP1", spans_calls_list)
232+
sp2 = MySpanProcessor("SP2", spans_calls_list)
233+
234+
with tracer.start_span("foo"):
235+
with tracer.start_span("bar"):
236+
with tracer.start_span("baz"):
237+
pass
238+
239+
# at this point lists must be empty
240+
self.assertEqual(len(spans_calls_list), 0)
241+
242+
# add single span processor
243+
tracer.add_span_processor(sp1)
244+
245+
with tracer.start_span("foo"):
246+
expected_list.append(span_event_start_fmt("SP1", "foo"))
247+
248+
with tracer.start_span("bar"):
249+
expected_list.append(span_event_start_fmt("SP1", "bar"))
250+
251+
with tracer.start_span("baz"):
252+
expected_list.append(span_event_start_fmt("SP1", "baz"))
253+
254+
expected_list.append(span_event_end_fmt("SP1", "baz"))
255+
256+
expected_list.append(span_event_end_fmt("SP1", "bar"))
257+
258+
expected_list.append(span_event_end_fmt("SP1", "foo"))
259+
260+
self.assertListEqual(spans_calls_list, expected_list)
261+
262+
spans_calls_list.clear()
263+
expected_list.clear()
264+
265+
# go for multiple span processors
266+
tracer.add_span_processor(sp2)
267+
268+
with tracer.start_span("foo"):
269+
expected_list.append(span_event_start_fmt("SP1", "foo"))
270+
expected_list.append(span_event_start_fmt("SP2", "foo"))
271+
272+
with tracer.start_span("bar"):
273+
expected_list.append(span_event_start_fmt("SP1", "bar"))
274+
expected_list.append(span_event_start_fmt("SP2", "bar"))
275+
276+
with tracer.start_span("baz"):
277+
expected_list.append(span_event_start_fmt("SP1", "baz"))
278+
expected_list.append(span_event_start_fmt("SP2", "baz"))
279+
280+
expected_list.append(span_event_end_fmt("SP1", "baz"))
281+
expected_list.append(span_event_end_fmt("SP2", "baz"))
282+
283+
expected_list.append(span_event_end_fmt("SP1", "bar"))
284+
expected_list.append(span_event_end_fmt("SP2", "bar"))
285+
286+
expected_list.append(span_event_end_fmt("SP1", "foo"))
287+
expected_list.append(span_event_end_fmt("SP2", "foo"))
288+
289+
# compare if two lists are the same
290+
self.assertListEqual(spans_calls_list, expected_list)
291+
292+
def test_add_span_processor_after_span_creation(self):
293+
tracer = trace.Tracer()
294+
295+
spans_calls_list = [] # filled by MySpanProcessor
296+
expected_list = [] # filled by hand
297+
298+
# Span processors are created but not added to the tracer yet
299+
sp = MySpanProcessor("SP1", spans_calls_list)
300+
301+
with tracer.start_span("foo"):
302+
with tracer.start_span("bar"):
303+
with tracer.start_span("baz"):
304+
# add span processor after spans have been created
305+
tracer.add_span_processor(sp)
306+
307+
expected_list.append(span_event_end_fmt("SP1", "baz"))
308+
309+
expected_list.append(span_event_end_fmt("SP1", "bar"))
310+
311+
expected_list.append(span_event_end_fmt("SP1", "foo"))
312+
313+
self.assertListEqual(spans_calls_list, expected_list)

0 commit comments

Comments
 (0)