Skip to content

Commit f705e0a

Browse files
committed
feat: add trace instrumentation to the runtime factory
1 parent bdc1a26 commit f705e0a

3 files changed

Lines changed: 76 additions & 5 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ dependencies = [
88
"click>=8.1.8",
99
"httpx>=0.28.1",
1010
"opentelemetry-sdk>=1.31.1",
11+
"opentelemetry-instrumentation>=0.52b1",
1112
"pydantic>=2.11.1",
1213
"python-dotenv>=1.0.1",
1314
"tenacity>=9.0.0",

src/uipath/_cli/_runtime/_contracts.py

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,18 @@
88
from abc import ABC, abstractmethod
99
from enum import Enum
1010
from 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
1319
from pydantic import BaseModel, Field
1420

21+
from uipath.tracing import TracingManager
22+
1523
from ._logging import LogsInterceptor
1624

1725
logger = 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)

uv.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)