1919from logfire ._internal .tracer import OPEN_SPANS
2020from logfire ._internal .utils import uniquify_sequence
2121from opentelemetry import context as context_api
22+ from opentelemetry import propagate
2223from opentelemetry import trace as trace_api
2324from opentelemetry .sdk .trace import ReadableSpan
2425from opentelemetry .trace import Tracer
@@ -225,6 +226,15 @@ def log_event(
225226 )
226227
227228
229+ class RunContext (te .TypedDict ):
230+ """Context for transferring and continuing runs in other places."""
231+
232+ run_id : str
233+ run_name : str
234+ project : str
235+ trace_context : dict [str , str ]
236+
237+
228238class RunUpdateSpan (Span ):
229239 def __init__ (
230240 self ,
@@ -274,10 +284,11 @@ def __init__(
274284 * ,
275285 params : AnyDict | None = None ,
276286 metrics : MetricDict | None = None ,
277- run_id : str | None = None ,
278287 tags : t .Sequence [str ] | None = None ,
279288 autolog : bool = True ,
280289 update_frequency : int = 5 ,
290+ run_id : str | ULID | None = None ,
291+ type : SpanType = "run" ,
281292 ) -> None :
282293 self .autolog = autolog
283294 self .project = project
@@ -307,6 +318,8 @@ def __init__(
307318 self ._pending_object_schemas = deepcopy (self ._object_schemas )
308319
309320 self ._context_token : Token [RunSpan | None ] | None = None # contextvars context
321+ self ._remote_context : dict [str , str ] | None = None # remote run trace context
322+ self ._remote_token : object | None = None
310323 self ._file_system = file_system
311324 self ._prefix_path = prefix_path
312325
@@ -315,23 +328,55 @@ def __init__(
315328 SPAN_ATTRIBUTE_PROJECT : project ,
316329 ** attributes ,
317330 }
318- super ().__init__ (name , attributes , tracer , type = "run" , tags = tags )
331+ super ().__init__ (name , attributes , tracer , type = type , tags = tags )
332+
333+ @classmethod
334+ def from_context (
335+ cls ,
336+ context : RunContext ,
337+ tracer : Tracer ,
338+ file_system : AbstractFileSystem ,
339+ prefix_path : str ,
340+ ) -> "RunSpan" :
341+ self = RunSpan (
342+ name = f"run.{ context ['run_id' ]} .fragment" ,
343+ project = context ["project" ],
344+ attributes = {},
345+ tracer = tracer ,
346+ file_system = file_system ,
347+ prefix_path = prefix_path ,
348+ type = "run_fragment" ,
349+ run_id = context ["run_id" ],
350+ )
351+
352+ self ._remote_context = context ["trace_context" ]
353+
354+ return self
319355
320356 def __enter__ (self ) -> te .Self :
321357 if current_run_span .get () is not None :
322358 raise RuntimeError ("You cannot start a run span within another run" )
323359
360+ if self ._remote_context is not None :
361+ otel_context = propagate .extract (carrier = self ._remote_context )
362+ self ._remote_token = context_api .attach (otel_context )
363+ else :
364+ super ().__enter__ ()
365+
324366 self ._context_token = current_run_span .set (self )
325- span = super ().__enter__ ()
326367 self .push_update (force = True )
327- return span
368+
369+ return self
328370
329371 def __exit__ (
330372 self ,
331373 exc_type : type [BaseException ] | None ,
332374 exc_value : BaseException | None ,
333375 traceback : types .TracebackType | None ,
334376 ) -> None :
377+ if self ._remote_context is not None :
378+ super ().__enter__ () # Now we can open our actually span
379+
335380 # When we finally close out the final span, include all the
336381 # full data attributes, so we can skip the update spans during
337382 # db queries later.
@@ -355,6 +400,10 @@ def __exit__(
355400 )
356401
357402 super ().__exit__ (exc_type , exc_value , traceback )
403+
404+ if self ._remote_token is not None :
405+ context_api .detach (self ._remote_token ) # type: ignore [arg-type]
406+
358407 if self ._context_token is not None :
359408 current_run_span .reset (self ._context_token )
360409
@@ -416,7 +465,7 @@ def log_object(
416465
417466 # Create a composite key that represents both data and schema
418467 hash_input = f"{ data_hash } :{ schema_hash } "
419- composite_hash = hashlib .sha1 (hash_input .encode ()).hexdigest ()[:16 ] # noqa: S324
468+ composite_hash = hashlib .sha1 (hash_input .encode ()).hexdigest ()[:16 ] # noqa: S324 # nosec
420469
421470 # Store schema if new
422471 if schema_hash not in self ._object_schemas :
0 commit comments