@@ -74,12 +74,22 @@ class should return the workflow interceptor subclass from
74
74
def __init__ (
75
75
self ,
76
76
tracer : Optional [opentelemetry .trace .Tracer ] = None ,
77
+ * ,
78
+ always_create_workflow_spans : bool = False ,
77
79
) -> None :
78
80
"""Initialize a OpenTelemetry tracing interceptor.
79
81
80
82
Args:
81
83
tracer: The tracer to use. Defaults to
82
84
:py:func:`opentelemetry.trace.get_tracer`.
85
+ always_create_workflow_spans: When false, the default, spans are
86
+ only created in workflows when an overarching span from the
87
+ client is present. In cases of starting a workflow elsewhere,
88
+ e.g. CLI or schedules, a client-created span is not present and
89
+ workflow spans will not be created. Setting this to true will
90
+ create spans in workflows no matter what, but there is a risk of
91
+ them being orphans since they may not have a parent span after
92
+ replaying.
83
93
"""
84
94
self .tracer = tracer or opentelemetry .trace .get_tracer (__name__ )
85
95
# To customize any of this, users must subclass. We intentionally don't
@@ -90,6 +100,7 @@ def __init__(
90
100
self .text_map_propagator : opentelemetry .propagators .textmap .TextMapPropagator = default_text_map_propagator
91
101
# TODO(cretz): Should I be using the configured one at the client and activity level?
92
102
self .payload_converter = temporalio .converter .PayloadConverter .default
103
+ self ._always_create_workflow_spans = always_create_workflow_spans
93
104
94
105
def intercept_client (
95
106
self , next : temporalio .client .OutboundInterceptor
@@ -165,10 +176,15 @@ def _start_as_current_span(
165
176
166
177
def _completed_workflow_span (
167
178
self , params : _CompletedWorkflowSpanParams
168
- ) -> _CarrierDict :
179
+ ) -> Optional [ _CarrierDict ] :
169
180
# Carrier to context, start span, set span as current on context,
170
181
# context back to carrier
171
182
183
+ # If the parent is missing and user hasn't said to always create, do not
184
+ # create
185
+ if params .parent_missing and not self ._always_create_workflow_spans :
186
+ return None
187
+
172
188
# Extract the context
173
189
context = self .text_map_propagator .extract (params .context )
174
190
# Create link if there is a span present
@@ -286,7 +302,7 @@ class _InputWithHeaders(Protocol):
286
302
287
303
class _WorkflowExternFunctions (TypedDict ):
288
304
__temporal_opentelemetry_completed_span : Callable [
289
- [_CompletedWorkflowSpanParams ], _CarrierDict
305
+ [_CompletedWorkflowSpanParams ], Optional [ _CarrierDict ]
290
306
]
291
307
292
308
@@ -299,6 +315,7 @@ class _CompletedWorkflowSpanParams:
299
315
link_context : Optional [_CarrierDict ]
300
316
exception : Optional [Exception ]
301
317
kind : opentelemetry .trace .SpanKind
318
+ parent_missing : bool
302
319
303
320
304
321
_interceptor_context_key = opentelemetry .context .create_key (
@@ -529,17 +546,13 @@ def _completed_span(
529
546
exception : Optional [Exception ] = None ,
530
547
kind : opentelemetry .trace .SpanKind = opentelemetry .trace .SpanKind .INTERNAL ,
531
548
) -> None :
532
- # If there is no span on the context, we do not create a span
533
- if opentelemetry .trace .get_current_span () is opentelemetry .trace .INVALID_SPAN :
534
- return None
535
-
536
549
# If we are replaying and they don't want a span on replay, no span
537
550
if temporalio .workflow .unsafe .is_replaying () and not new_span_even_on_replay :
538
551
return None
539
552
540
553
# Create the span. First serialize current context to carrier.
541
- context_carrier : _CarrierDict = {}
542
- self .text_map_propagator .inject (context_carrier )
554
+ new_context_carrier : _CarrierDict = {}
555
+ self .text_map_propagator .inject (new_context_carrier )
543
556
# Invoke
544
557
info = temporalio .workflow .info ()
545
558
attributes : Dict [str , opentelemetry .util .types .AttributeValue ] = {
@@ -548,25 +561,27 @@ def _completed_span(
548
561
}
549
562
if additional_attributes :
550
563
attributes .update (additional_attributes )
551
- context_carrier = self ._extern_functions [
564
+ updated_context_carrier = self ._extern_functions [
552
565
"__temporal_opentelemetry_completed_span"
553
566
](
554
567
_CompletedWorkflowSpanParams (
555
- context = context_carrier ,
568
+ context = new_context_carrier ,
556
569
name = span_name ,
557
570
# Always set span attributes as workflow ID and run ID
558
571
attributes = attributes ,
559
572
time_ns = temporalio .workflow .time_ns (),
560
573
link_context = link_context_carrier ,
561
574
exception = exception ,
562
575
kind = kind ,
576
+ parent_missing = opentelemetry .trace .get_current_span ()
577
+ is opentelemetry .trace .INVALID_SPAN ,
563
578
)
564
579
)
565
580
566
581
# Add to outbound if needed
567
- if add_to_outbound :
582
+ if add_to_outbound and updated_context_carrier :
568
583
add_to_outbound .headers = self ._context_carrier_to_headers (
569
- context_carrier , add_to_outbound .headers
584
+ updated_context_carrier , add_to_outbound .headers
570
585
)
571
586
572
587
def _set_on_context (
0 commit comments