Skip to content

Commit 5cf82e8

Browse files
committed
fixup
1 parent a7cb2d0 commit 5cf82e8

6 files changed

Lines changed: 121 additions & 161 deletions

File tree

src/uipath/_cli/_dev/_terminal/__init__.py

Lines changed: 11 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
from uuid import uuid4
88

99
from dotenv import load_dotenv
10-
from opentelemetry import trace
11-
from opentelemetry.sdk.trace import Tracer, TracerProvider
1210
from textual.app import App, ComposeResult
1311
from textual.binding import Binding
1412
from textual.containers import Container, Horizontal
@@ -17,7 +15,6 @@
1715
from ..._runtime._contracts import (
1816
UiPathRuntimeContext,
1917
UiPathRuntimeFactory,
20-
UiPathTraceContext,
2118
)
2219
from ..._utils._console import ConsoleLogger
2320
from ._components._details import RunDetailsPanel
@@ -27,7 +24,6 @@
2724
from ._models._messages import LogMessage, TraceMessage
2825
from ._traces._exporter import RunContextExporter
2926
from ._traces._logger import RunContextLogHandler
30-
from ._traces._processor import RunContextProcessor
3127

3228
console = ConsoleLogger()
3329
load_dotenv(override=True)
@@ -57,10 +53,12 @@ def __init__(
5753
self.initial_input: str = '{\n "message": "Hello World"\n}'
5854
self.runs: Dict[str, ExecutionRun] = {}
5955
self.runtime_factory = runtime_factory
60-
self.trace_provider: TracerProvider = TracerProvider()
61-
self.trace_processor: RunContextProcessor = RunContextProcessor()
62-
self.trace_provider.add_span_processor(self.trace_processor)
63-
trace.set_tracer_provider(self.trace_provider)
56+
self.runtime_factory.add_span_exporter(
57+
RunContextExporter(
58+
on_trace=self._handle_trace_message,
59+
on_log=self._handle_log_message,
60+
)
61+
)
6462

6563
def compose(self) -> ComposeResult:
6664
with Horizontal():
@@ -145,10 +143,7 @@ async def _execute_runtime(self, run: ExecutionRun):
145143
entrypoint=run.entrypoint,
146144
input=run.input_data,
147145
trace_id=str(uuid4()),
148-
tracing_enabled=False,
149-
trace_context=UiPathTraceContext(
150-
enabled=False,
151-
),
146+
execution_id=run.id,
152147
logs_min_level=env.get("LOG_LEVEL", "INFO"),
153148
log_handler=RunContextLogHandler(
154149
run_id=run.id, on_log=self._handle_log_message
@@ -157,21 +152,11 @@ async def _execute_runtime(self, run: ExecutionRun):
157152

158153
self._add_info_log(run, f"Starting execution: {run.entrypoint}")
159154

160-
trace_exporter = RunContextExporter(
161-
run_id=run.id,
162-
on_trace=self._handle_trace_message,
163-
on_log=self._handle_log_message,
164-
)
165-
self.trace_processor.register_exporter(run.id, trace_exporter)
166-
tracer: Tracer = trace.get_tracer("uipath-dev-terminal")
167-
168-
with tracer.start_as_current_span("root", attributes={"run.id": run.id}):
169-
result = await self.runtime_factory.execute(context)
170-
run.output_data = result.output
171-
if run.output_data:
172-
self._add_info_log(run, f"Execution result: {run.output_data}")
155+
result = await self.runtime_factory.execute_in_root_span(context)
173156

174-
self.trace_processor.unregister_exporter(run.id)
157+
run.output_data = result.output
158+
if run.output_data:
159+
self._add_info_log(run, f"Execution result: {run.output_data}")
175160

176161
self._add_info_log(run, "✅ Execution completed successfully")
177162
run.status = "completed"

src/uipath/_cli/_dev/_terminal/_traces/_exporter.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@ class RunContextExporter(SpanExporter):
1313

1414
def __init__(
1515
self,
16-
run_id: str,
1716
on_trace: Callable[[TraceMessage], None],
1817
on_log: Callable[[LogMessage], None],
1918
):
20-
self.run_id = run_id
2119
self.on_trace = on_trace
2220
self.on_log = on_log
2321
self.logger = logging.getLogger(__name__)
@@ -54,14 +52,15 @@ def _export_span(self, span: Span):
5452
span_id = f"{span_context.span_id:016x}" # 16-char hex string
5553
trace_id = f"{span_context.trace_id:032x}" # 32-char hex string
5654

55+
run_id = span.attributes.get("execution.id")
5756
# Get parent span ID if available
5857
parent_span_id = None
5958
if hasattr(span, "parent") and span.parent:
6059
parent_span_id = f"{span.parent.span_id:016x}"
6160

6261
# Create trace message with all required fields
6362
trace_msg = TraceMessage(
64-
run_id=self.run_id,
63+
run_id=run_id,
6564
span_name=span.name,
6665
span_id=span_id,
6766
parent_span_id=parent_span_id,
@@ -80,7 +79,7 @@ def _export_span(self, span: Span):
8079
for event in span.events:
8180
log_level = self._determine_log_level(event, span.status)
8281
log_msg = LogMessage(
83-
run_id=self.run_id,
82+
run_id=run_id,
8483
level=log_level,
8584
message=event.name,
8685
timestamp=datetime.fromtimestamp(event.timestamp / 1_000_000_000),

src/uipath/_cli/_dev/_terminal/_traces/_processor.py

Lines changed: 0 additions & 39 deletions
This file was deleted.

src/uipath/_cli/_runtime/_contracts.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@
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
11+
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
1212

13+
from opentelemetry import context as context_api
14+
from opentelemetry import trace
15+
from opentelemetry.sdk.trace import Span, SpanProcessor, TracerProvider
16+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter
17+
from opentelemetry.trace import Tracer
1318
from pydantic import BaseModel, Field
1419

1520
from ._logging import LogsInterceptor
@@ -136,6 +141,8 @@ class UiPathTraceContext(BaseModel):
136141
enabled: Union[bool, str] = False
137142
reference_id: Optional[str] = None
138143

144+
model_config = {"arbitrary_types_allowed": True}
145+
139146

140147
class UiPathRuntimeContext(BaseModel):
141148
"""Context information passed throughout the runtime execution."""
@@ -144,6 +151,7 @@ class UiPathRuntimeContext(BaseModel):
144151
input: Optional[str] = None
145152
input_json: Optional[Any] = None
146153
job_id: Optional[str] = None
154+
execution_id: Optional[str] = None
147155
trace_id: Optional[str] = None
148156
trace_context: Optional[UiPathTraceContext] = None
149157
tracing_enabled: Union[bool, str] = False
@@ -488,6 +496,15 @@ def __init__(self, runtime_class: Type[T], context_class: Type[C]):
488496

489497
self.runtime_class = runtime_class
490498
self.context_class = context_class
499+
self.tracer_provider: TracerProvider = TracerProvider()
500+
self.tracer_span_processors: List[SpanProcessor] = []
501+
trace.set_tracer_provider(self.tracer_provider)
502+
503+
def add_span_exporter(self, span_exporter: SpanExporter) -> None:
504+
"""Add a span processor to the tracer provider."""
505+
span_processor = UiPathExecutionTraceProcessor(span_exporter)
506+
self.tracer_span_processors.append(span_processor)
507+
self.tracer_provider.add_span_processor(span_processor)
491508

492509
def new_context(self, **kwargs) -> C:
493510
"""Create a new context instance."""
@@ -500,4 +517,39 @@ def from_context(self, context: C) -> T:
500517
async def execute(self, context: C) -> Optional[UiPathRuntimeResult]:
501518
"""Execute runtime with context."""
502519
async with self.from_context(context) as runtime:
503-
return await runtime.execute()
520+
result = await runtime.execute()
521+
for span_processor in self.tracer_span_processors:
522+
span_processor.force_flush()
523+
return result
524+
525+
async def execute_in_root_span(
526+
self, context: C, root_span: str = "root"
527+
) -> Optional[UiPathRuntimeResult]:
528+
"""Execute runtime with context."""
529+
async with self.from_context(context) as runtime:
530+
tracer: Tracer = trace.get_tracer("uipath-runtime")
531+
with tracer.start_as_current_span(
532+
root_span, attributes={"execution.id": context.execution_id}
533+
):
534+
result = await runtime.execute()
535+
for span_processor in self.tracer_span_processors:
536+
span_processor.force_flush()
537+
return result
538+
539+
540+
class UiPathExecutionTraceProcessor(BatchSpanProcessor):
541+
def on_start(
542+
self, span: Span, parent_context: Optional[context_api.Context] = None
543+
):
544+
"""Called when a span is started."""
545+
if parent_context:
546+
parent_span = trace.get_current_span(parent_context)
547+
else:
548+
parent_span = trace.get_current_span()
549+
550+
if parent_span and parent_span.is_recording():
551+
run_id = parent_span.attributes.get("execution.id")
552+
if run_id:
553+
span.set_attribute("execution.id", run_id)
554+
555+
super().on_start(span, parent_context)

src/uipath/_cli/_runtime/_runtime.py

Lines changed: 20 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,8 @@
88
from dataclasses import asdict, is_dataclass
99
from typing import Any, Dict, Optional, Type, TypeVar, cast, get_type_hints
1010

11-
from opentelemetry import trace
12-
from opentelemetry.sdk.trace import TracerProvider
13-
from opentelemetry.sdk.trace.export import BatchSpanProcessor
1411
from pydantic import BaseModel
1512

16-
from uipath.tracing import LlmOpsHttpExporter
17-
1813
from ._contracts import (
1914
UiPathBaseRuntime,
2015
UiPathErrorCategory,
@@ -42,19 +37,6 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
4237
"""
4338
await self.validate()
4439

45-
shutdown_provider = False
46-
trace_provider: Optional[TracerProvider] = None
47-
span_processor: Optional[BatchSpanProcessor] = None
48-
49-
if self.context.trace_context.enabled:
50-
trace_provider = trace.get_tracer_provider()
51-
if not trace_provider:
52-
trace_provider = TracerProvider()
53-
trace.set_tracer_provider(trace_provider)
54-
shutdown_provider = True
55-
span_processor = BatchSpanProcessor(LlmOpsHttpExporter())
56-
trace_provider.add_span_processor(span_processor)
57-
5840
try:
5941
if self.context.entrypoint is None:
6042
return None
@@ -83,13 +65,6 @@ async def execute(self) -> Optional[UiPathRuntimeResult]:
8365
UiPathErrorCategory.SYSTEM,
8466
) from e
8567

86-
finally:
87-
if self.context.trace_context.enabled:
88-
await span_processor.force_flush()
89-
# Only shutdown if we created the provider
90-
if shutdown_provider:
91-
await trace_provider.shutdown()
92-
9368
async def validate(self) -> None:
9469
"""Validate runtime inputs."""
9570
if not self.context.entrypoint:
@@ -127,12 +102,6 @@ async def cleanup(self) -> None:
127102

128103
async def _execute_python_script(self, script_path: str, input_data: Any) -> Any:
129104
"""Execute the Python script with the given input."""
130-
# parent_span = trace.get_current_span()
131-
# ctx = trace.set_span_in_context(parent_span)
132-
133-
# print(f"Before module execution - Current span: {parent_span}")
134-
# print(f"Current span attributes: {getattr(parent_span, 'attributes', {})}")
135-
136105
spec = importlib.util.spec_from_file_location("dynamic_module", script_path)
137106
if not spec or not spec.loader:
138107
raise UiPathRuntimeError(
@@ -144,16 +113,9 @@ async def _execute_python_script(self, script_path: str, input_data: Any) -> Any
144113

145114
module = importlib.util.module_from_spec(spec)
146115

147-
# Attach the context BEFORE any module operations
148-
# token = context_api.attach(ctx)
149-
150116
try:
151-
# print("Executing module with context attached")
152117
spec.loader.exec_module(module)
153118

154-
# active_span = trace.get_current_span()
155-
# print(f"After module execution - Active span: {active_span}")
156-
157119
# Execute the function while context is still attached
158120
for func_name in ["main", "run", "execute"]:
159121
if hasattr(module, func_name):
@@ -209,26 +171,26 @@ async def _execute_python_script(self, script_path: str, input_data: Any) -> Any
209171
UiPathErrorCategory.USER,
210172
) from e
211173

212-
# Case 3: Dict parameter
213-
else:
214-
try:
215-
result = (
216-
await main_func(input_data)
217-
if is_async
218-
else main_func(input_data)
219-
)
220-
return (
221-
self._convert_from_class(result)
222-
if result is not None
223-
else {}
224-
)
225-
except Exception as e:
226-
raise UiPathRuntimeError(
227-
"FUNCTION_EXECUTION_ERROR",
228-
f"Error executing {func_name} function with dictionary input",
229-
f"Error: {str(e)}",
230-
UiPathErrorCategory.USER,
231-
) from e
174+
# Case 3: Dict parameter
175+
else:
176+
try:
177+
result = (
178+
await main_func(input_data)
179+
if is_async
180+
else main_func(input_data)
181+
)
182+
return (
183+
self._convert_from_class(result)
184+
if result is not None
185+
else {}
186+
)
187+
except Exception as e:
188+
raise UiPathRuntimeError(
189+
"FUNCTION_EXECUTION_ERROR",
190+
f"Error executing {func_name} function with dictionary input",
191+
f"Error: {str(e)}",
192+
UiPathErrorCategory.USER,
193+
) from e
232194

233195
# If we get here, no main function was found
234196
raise UiPathRuntimeError(
@@ -249,10 +211,6 @@ async def _execute_python_script(self, script_path: str, input_data: Any) -> Any
249211
UiPathErrorCategory.USER,
250212
) from e
251213

252-
# finally:
253-
# Only detach the context at the very end, after all operations
254-
# context_api.detach(token)
255-
256214
def _convert_to_class(self, data: Dict[str, Any], cls: Type[T]) -> T:
257215
"""Convert a dictionary to either a dataclass, Pydantic model, or regular class instance."""
258216
# Handle Pydantic models

0 commit comments

Comments
 (0)