@@ -145,6 +145,70 @@ def from_map(cls, maxlen, mapping):
145
145
Link = namedtuple ("Link" , ("context" , "attributes" ))
146
146
147
147
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
+ # Used when no SpanProcessor has been added to the Tracer
182
+ _NO_OP_SPAN_PROCESSOR = SpanProcessor ()
183
+
184
+
185
+ class MultiSpanProcessor (SpanProcessor ):
186
+ """Implementation of :class:`SpanProcessor` that forwards all received
187
+ events to a list of `SpanProcessor`.
188
+ """
189
+
190
+ def __init__ (self ):
191
+ # use a tuple to avoid race conditions when adding a new span and
192
+ # interating throw it on "on_start" and "on_end".
193
+ self ._span_processors = ()
194
+
195
+ def add_span_processor (self , span_processor : SpanProcessor ):
196
+ """Adds a SpanProcessor to the list handled by this instance."""
197
+ self ._span_processors = self ._span_processors + (span_processor ,)
198
+
199
+ def on_start (self , span : "Span" ) -> None :
200
+ for sp in self ._span_processors :
201
+ sp .on_start (span )
202
+
203
+ def on_end (self , span : "Span" ) -> None :
204
+ for sp in self ._span_processors :
205
+ sp .on_end (span )
206
+
207
+ def on_shutdown (self ) -> None :
208
+ for sp in self ._span_processors :
209
+ sp .shutdown ()
210
+
211
+
148
212
class Span (trace_api .Span ):
149
213
"""See `opentelemetry.trace.Span`.
150
214
@@ -161,6 +225,8 @@ class Span(trace_api.Span):
161
225
attributes: The span's attributes to be exported
162
226
events: Timestamped events to be exported
163
227
links: Links to other spans to be exported
228
+ span_processor: `SpanProcessor` to invoke when starting and ending
229
+ this `Span`.
164
230
"""
165
231
166
232
# Initialize these lazily assuming most spans won't have them.
@@ -179,6 +245,7 @@ def __init__(
179
245
attributes : types .Attributes = None , # TODO
180
246
events : typing .Sequence [Event ] = None , # TODO
181
247
links : typing .Sequence [Link ] = None , # TODO
248
+ span_processor : SpanProcessor = _NO_OP_SPAN_PROCESSOR ,
182
249
) -> None :
183
250
184
251
self .name = name
@@ -190,6 +257,7 @@ def __init__(
190
257
self .attributes = attributes
191
258
self .events = events
192
259
self .links = links
260
+ self .span_processor = span_processor
193
261
194
262
if attributes is None :
195
263
self .attributes = Span .empty_attributes
@@ -247,10 +315,12 @@ def add_link(
247
315
def start (self ):
248
316
if self .start_time is None :
249
317
self .start_time = util .time_ns ()
318
+ self .span_processor .on_start (self )
250
319
251
320
def end (self ):
252
321
if self .end_time is None :
253
322
self .end_time = util .time_ns ()
323
+ self .span_processor .on_end (self )
254
324
255
325
def update_name (self , name : str ) -> None :
256
326
self .name = name
@@ -286,6 +356,8 @@ def __init__(self, name: str = "") -> None:
286
356
if name :
287
357
slot_name = "{}.current_span" .format (name )
288
358
self ._current_span_slot = Context .register_slot (slot_name )
359
+ self ._active_span_processor = _NO_OP_SPAN_PROCESSOR
360
+ self ._lock = threading .Lock ()
289
361
290
362
def get_current_span (self ):
291
363
"""See `opentelemetry.trace.Tracer.get_current_span`."""
@@ -325,7 +397,12 @@ def create_span(
325
397
parent_context .trace_options ,
326
398
parent_context .trace_state ,
327
399
)
328
- return Span (name = name , context = context , parent = parent )
400
+ return Span (
401
+ name = name ,
402
+ context = context ,
403
+ parent = parent ,
404
+ span_processor = self ._active_span_processor ,
405
+ )
329
406
330
407
@contextmanager
331
408
def use_span (self , span : "Span" ) -> typing .Iterator ["Span" ]:
@@ -339,5 +416,15 @@ def use_span(self, span: "Span") -> typing.Iterator["Span"]:
339
416
self ._current_span_slot .set (span_snapshot )
340
417
span .end ()
341
418
419
+ def add_span_processor (self , span_processor : SpanProcessor ) -> None :
420
+ """Registers a new :class:`SpanProcessor` for this `Tracer`.
421
+
422
+ The span processors are invoked in the same order they are registered.
423
+ """
424
+ with self ._lock :
425
+ if self ._active_span_processor is _NO_OP_SPAN_PROCESSOR :
426
+ self ._active_span_processor = MultiSpanProcessor ()
427
+ self ._active_span_processor .add_span_processor (span_processor )
428
+
342
429
343
430
tracer = Tracer ()
0 commit comments