5757)
5858from aws_durable_execution_sdk_python_otel .deterministic_id_generator import (
5959 DeterministicIdGenerator ,
60+ _to_otel_trace_id ,
6061 derive_workflow_span_id ,
6162 operation_id_to_span_id ,
6263)
@@ -104,40 +105,39 @@ def __init__(self, config: OtelPluginConfig | None = None) -> None:
104105 )
105106 self ._workflow_span_name = self ._config .workflow_span_name
106107
107- self ._id_generator = DeterministicIdGenerator ()
108- result = create_tracer_provider (
109- self ._config ,
110- id_generator = self ._id_generator ,
111- )
108+ result = create_tracer_provider (self ._config )
112109 self ._provider = result .tracer_provider
113110 # GLOBAL (ADOT) mode parents the Invocation span to the ambient Lambda
114111 # invocation span instead of the Workflow span (see
115112 # _start_invocation_span).
116113 self ._provider_source = result .source
117114
118- # Deterministic stitching requires an SDK provider exposing id_generator.
119- from opentelemetry .sdk .trace import TracerProvider as SdkTracerProvider
115+ self ._tracer : Tracer = self ._provider .get_tracer (self ._config .instrument_name )
116+
117+ # Deterministic stitching is scoped to this instrumentation tracer so
118+ # unrelated tracers on the same provider keep their original generator.
119+ from opentelemetry .sdk .trace import Tracer as SdkTracer
120120
121- if isinstance (self ._provider , SdkTracerProvider ):
122- self ._id_generator = DeterministicIdGenerator .install_on_provider (
123- self ._provider
121+ self ._id_generator = DeterministicIdGenerator ()
122+ if isinstance (self ._tracer , SdkTracer ):
123+ self ._id_generator = DeterministicIdGenerator .install_on_tracer (
124+ self ._tracer
124125 )
125126 else :
126127 logger .warning (
127- "ExecutionOtelPlugin expected an SDK TracerProvider but got %s; "
128+ "ExecutionOtelPlugin expected an SDK Tracer but got %s; "
128129 "spans will not use deterministic IDs." ,
129- type (self ._provider ).__name__ ,
130+ type (self ._tracer ).__name__ ,
130131 )
131132
132- self ._tracer : Tracer = self ._provider .get_tracer (self ._config .instrument_name )
133-
134133 try :
135134 register_standalone_instrumentations (self ._config , result )
136135 except Exception :
137136 logger .exception ("Failed to register standalone instrumentations" )
138137
139138 # Per-invocation state.
140139 self ._execution_arn = ""
140+ self ._execution_trace_id : int | None = None
141141 self ._extracted_context : Context | None = None
142142 self ._workflow_span : Span | None = None
143143 self ._invocation_span : Span | None = None
@@ -205,8 +205,10 @@ def _resolve_parent(self, parent_id: str | None) -> Span | None:
205205 def on_invocation_start (self , info : InvocationStartInfo ) -> None :
206206 logger .debug ("Durable invocation started: %s" , info )
207207 self ._execution_arn = info .execution_arn or ""
208+ self ._execution_trace_id = _to_otel_trace_id (
209+ self ._execution_arn , info .execution_start_time
210+ )
208211 self ._extracted_context = self ._context_extractor (info )
209- self ._id_generator .set_trace_id (self ._execution_arn , info .execution_start_time )
210212
211213 self ._start_workflow_span (info )
212214 # Create the Invocation span in both modes. In default-provider mode it
@@ -224,23 +226,23 @@ def _start_workflow_span(self, info: InvocationStartInfo) -> None:
224226 if not self ._execution_arn :
225227 logger .warning ("No execution ARN; skipping Workflow span creation" )
226228 return
227- self ._id_generator .set_next_span_id (
228- derive_workflow_span_id (self ._execution_arn )
229- )
230229 start_time = _to_otel_timestamp (
231230 info .execution_start_time
232231 ) or _to_otel_timestamp (datetime .datetime .now (datetime .UTC ))
233232 # Empty context => root span with no parent.
234- self ._workflow_span = self ._tracer .start_span (
235- name = self ._workflow_span_name ,
236- kind = SpanKind .INTERNAL ,
237- attributes = {"durable.execution.arn" : self ._execution_arn },
238- start_time = start_time ,
239- context = Context (),
240- )
233+ with self ._id_generator .use_ids (
234+ trace_id = self ._execution_trace_id ,
235+ span_id = derive_workflow_span_id (self ._execution_arn ),
236+ ):
237+ self ._workflow_span = self ._tracer .start_span (
238+ name = self ._workflow_span_name ,
239+ kind = SpanKind .INTERNAL ,
240+ attributes = {"durable.execution.arn" : self ._execution_arn },
241+ start_time = start_time ,
242+ context = Context (),
243+ )
241244
242245 def _start_invocation_span (self , info : InvocationStartInfo ) -> None :
243- self ._id_generator .set_next_span_id (None )
244246 attributes : dict [str , Any ]
245247 if self ._provider_source is ProviderSource .GLOBAL :
246248 # Default-provider mode: parent the Invocation span to the ambient
@@ -264,12 +266,15 @@ def _start_invocation_span(self, info: InvocationStartInfo) -> None:
264266 "durable.execution.arn" : self ._execution_arn ,
265267 "durable.invocation.first" : info .is_first_invocation ,
266268 }
267- self ._invocation_span = self ._tracer .start_span (
268- name = "Invocation" ,
269- kind = SpanKind .INTERNAL ,
270- attributes = attributes ,
271- context = parent_ctx ,
272- )
269+ with self ._id_generator .use_ids (
270+ trace_id = self ._execution_trace_id , span_id = None
271+ ):
272+ self ._invocation_span = self ._tracer .start_span (
273+ name = "Invocation" ,
274+ kind = SpanKind .INTERNAL ,
275+ attributes = attributes ,
276+ context = parent_ctx ,
277+ )
273278 self ._set_span (_INVOCATION_KEY , self ._invocation_span )
274279
275280 def on_invocation_end (self , info : InvocationEndInfo ) -> None :
@@ -330,6 +335,7 @@ def on_invocation_end(self, info: InvocationEndInfo) -> None:
330335
331336 def _reset_state (self ) -> None :
332337 self ._execution_arn = ""
338+ self ._execution_trace_id = None
333339 self ._extracted_context = None
334340 self ._workflow_span = None
335341 self ._invocation_span = None
@@ -399,27 +405,26 @@ def _start_span(
399405 key = span_key if span_key is not None else operation_id
400406 with self ._lock :
401407 links = self ._build_invocation_links ()
402- if deterministic :
403- # Operation spans always use the deterministic logical-operation
404- # span ID so a suspended-then-completed operation exports a
405- # single span (on completion) with a stable ID across invocations.
406- self ._id_generator .set_next_span_id (
407- operation_id_to_span_id (self ._execution_arn , operation_id )
408- )
409- else :
410- self ._id_generator .set_next_span_id (None )
408+ span_id = (
409+ operation_id_to_span_id (self ._execution_arn , operation_id )
410+ if deterministic
411+ else None
412+ )
411413
412414 if parent is None :
413415 parent_ctx = self ._extracted_context or Context ()
414416 else :
415417 parent_ctx = trace .set_span_in_context (parent , self ._extracted_context )
416- span = self ._tracer .start_span (
417- name = name ,
418- attributes = self ._operation_attributes (info ),
419- start_time = _to_otel_timestamp (start_time ),
420- context = parent_ctx ,
421- links = links ,
422- )
418+ with self ._id_generator .use_ids (
419+ trace_id = self ._execution_trace_id , span_id = span_id
420+ ):
421+ span = self ._tracer .start_span (
422+ name = name ,
423+ attributes = self ._operation_attributes (info ),
424+ start_time = _to_otel_timestamp (start_time ),
425+ context = parent_ctx ,
426+ links = links ,
427+ )
423428 self ._operation_spans [key ] = span
424429 return span
425430
0 commit comments