88from abc import ABC , abstractmethod
99from enum import Enum
1010from functools import cached_property
11- from typing import Any , Dict , Generic , Optional , Type , TypeVar , Union
12-
11+ from typing import Any , Callable , Dict , Generic , List , Optional , Type , TypeVar , Union
12+
13+ from opentelemetry import context as context_api
14+ from opentelemetry import trace
15+ from opentelemetry .instrumentation .instrumentor import BaseInstrumentor # type: ignore
16+ from opentelemetry .sdk .trace import Span , SpanProcessor , TracerProvider
17+ from opentelemetry .sdk .trace .export import BatchSpanProcessor , SpanExporter
18+ from opentelemetry .trace import Tracer
1319from pydantic import BaseModel , Field
1420
21+ from uipath .tracing import TracingManager
22+
1523from ._logging import LogsInterceptor
1624
1725logger = logging .getLogger (__name__ )
@@ -144,6 +152,7 @@ class UiPathRuntimeContext(BaseModel):
144152 input : Optional [str ] = None
145153 input_json : Optional [Any ] = None
146154 job_id : Optional [str ] = None
155+ execution_id : Optional [str ] = None
147156 trace_id : Optional [str ] = None
148157 trace_context : Optional [UiPathTraceContext ] = None
149158 tracing_enabled : Union [bool , str ] = False
@@ -160,7 +169,6 @@ class UiPathRuntimeContext(BaseModel):
160169 input_file : Optional [str ] = None
161170 is_eval_run : bool = False
162171 log_handler : Optional [logging .Handler ] = None
163-
164172 model_config = {"arbitrary_types_allowed" : True }
165173
166174 @classmethod
@@ -489,6 +497,28 @@ def __init__(self, runtime_class: Type[T], context_class: Type[C]):
489497
490498 self .runtime_class = runtime_class
491499 self .context_class = context_class
500+ self .tracer_provider : TracerProvider = TracerProvider ()
501+ self .tracer_span_processors : List [SpanProcessor ] = []
502+ trace .set_tracer_provider (self .tracer_provider )
503+
504+ def add_span_exporter (
505+ self , span_exporter : SpanExporter
506+ ) -> "UiPathRuntimeFactory[T, C]" :
507+ """Add a span processor to the tracer provider."""
508+ span_processor = UiPathExecutionTraceProcessor (span_exporter )
509+ self .tracer_span_processors .append (span_processor )
510+ self .tracer_provider .add_span_processor (span_processor )
511+ return self
512+
513+ def add_instrumentor (
514+ self ,
515+ instrumentor_class : Type [BaseInstrumentor ],
516+ get_current_span_func : Callable [[], Optional [Span ]],
517+ ) -> "UiPathRuntimeFactory[T, C]" :
518+ """Add and instrument immediately."""
519+ instrumentor_class ().instrument (tracer_provider = self .tracer_provider )
520+ TracingManager .register_current_span_provider (get_current_span_func )
521+ return self
492522
493523 def new_context (self , ** kwargs ) -> C :
494524 """Create a new context instance."""
@@ -501,4 +531,42 @@ def from_context(self, context: C) -> T:
501531 async def execute (self , context : C ) -> Optional [UiPathRuntimeResult ]:
502532 """Execute runtime with context."""
503533 async with self .from_context (context ) as runtime :
504- return await runtime .execute ()
534+ result = await runtime .execute ()
535+ for span_processor in self .tracer_span_processors :
536+ span_processor .force_flush ()
537+ return result
538+
539+ async def execute_in_root_span (
540+ self , context : C , root_span : str = "root"
541+ ) -> Optional [UiPathRuntimeResult ]:
542+ """Execute runtime with context."""
543+ async with self .from_context (context ) as runtime :
544+ tracer : Tracer = trace .get_tracer ("uipath-runtime" )
545+ with tracer .start_as_current_span (
546+ root_span ,
547+ attributes = {"execution.id" : context .execution_id }
548+ if context .execution_id
549+ else {},
550+ ):
551+ result = await runtime .execute ()
552+ for span_processor in self .tracer_span_processors :
553+ span_processor .force_flush ()
554+ return result
555+
556+
557+ class UiPathExecutionTraceProcessor (BatchSpanProcessor ):
558+ def on_start (
559+ self , span : Span , parent_context : Optional [context_api .Context ] = None
560+ ):
561+ """Called when a span is started."""
562+ if parent_context :
563+ parent_span = trace .get_current_span (parent_context )
564+ else :
565+ parent_span = trace .get_current_span ()
566+
567+ if parent_span and parent_span .is_recording ():
568+ run_id = parent_span .attributes .get ("execution.id" ) # type: ignore[attr-defined]
569+ if run_id :
570+ span .set_attribute ("execution.id" , run_id )
571+
572+ super ().on_start (span , parent_context )
0 commit comments