Skip to content
This repository was archived by the owner on Mar 4, 2026. It is now read-only.

Commit d9c5af7

Browse files
committed
fix: refactor span attribute setting to rm code duplication
1 parent fbe7206 commit d9c5af7

2 files changed

Lines changed: 149 additions & 118 deletions

File tree

src/uipath/core/tracing/_utils.py

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
from dataclasses import asdict, is_dataclass
77
from datetime import datetime, timezone
88
from enum import Enum
9-
from typing import Any, Mapping
9+
from typing import Any, Mapping, Optional
1010
from zoneinfo import ZoneInfo
1111

12+
from opentelemetry.trace import Span
1213
from pydantic import BaseModel
1314

1415

@@ -30,7 +31,7 @@ def get_supported_params(
3031
return supported
3132

3233

33-
def _simple_serialize_defaults(obj):
34+
def _simple_serialize_defaults(obj: Any) -> Any:
3435
# Handle Pydantic BaseModel instances
3536
if hasattr(obj, "model_dump") and not isinstance(obj, type):
3637
return obj.model_dump(exclude_none=True, mode="json")
@@ -56,8 +57,8 @@ def _simple_serialize_defaults(obj):
5657
return _simple_serialize_defaults(obj.value)
5758

5859
if isinstance(obj, (set, tuple)):
59-
if hasattr(obj, "_asdict") and callable(obj._asdict):
60-
return obj._asdict()
60+
if hasattr(obj, "_asdict") and callable(obj._asdict): # pyright: ignore[reportAttributeAccessIssue]
61+
return obj._asdict() # pyright: ignore[reportAttributeAccessIssue]
6162
return list(obj)
6263

6364
if isinstance(obj, datetime):
@@ -120,3 +121,68 @@ def format_args_for_trace(
120121
return result
121122
except Exception:
122123
return {"args": args, "kwargs": kwargs}
124+
125+
126+
def set_span_input_attributes(
127+
span: Span,
128+
trace_name: str,
129+
wrapped_func: Callable,
130+
args: Any,
131+
kwargs: Any,
132+
span_type: str,
133+
run_type: Optional[str],
134+
input_processor: Optional[Callable[..., Any]],
135+
) -> None:
136+
"""Set span attributes for metadata and inputs before function execution.
137+
138+
This should be called BEFORE the wrapped function executes to ensure
139+
input context is captured even if the function raises an exception.
140+
141+
Args:
142+
span: The OpenTelemetry span to set attributes on
143+
trace_name: Name of the trace/span
144+
wrapped_func: The function being traced
145+
args: Positional arguments passed to the function
146+
kwargs: Keyword arguments passed to the function
147+
span_type: Span type categorization (set to "TOOL" for OpenInference tool calls)
148+
run_type: Optional run type categorization
149+
input_processor: Optional function to process inputs before recording
150+
"""
151+
is_tool = span_type and span_type.upper() == "TOOL"
152+
if is_tool:
153+
span.set_attribute("openinference.span.kind", "TOOL")
154+
span.set_attribute("tool.name", trace_name)
155+
span.set_attribute("span_type", "TOOL")
156+
else:
157+
span.set_attribute("span_type", span_type)
158+
159+
if run_type is not None:
160+
span.set_attribute("run_type", run_type)
161+
162+
inputs = format_args_for_trace_json(
163+
inspect.signature(wrapped_func), *args, **kwargs
164+
)
165+
if input_processor:
166+
processed_inputs = input_processor(json.loads(inputs))
167+
inputs = json.dumps(processed_inputs, default=str)
168+
span.set_attribute("input.mime_type", "application/json")
169+
span.set_attribute("input.value", inputs)
170+
171+
172+
def set_span_output_attributes(
173+
span: Span,
174+
result: Any,
175+
output_processor: Optional[Callable[..., Any]],
176+
) -> None:
177+
"""Set span attributes for outputs after function execution.
178+
179+
This should be called AFTER the wrapped function executes successfully.
180+
181+
Args:
182+
span: The OpenTelemetry span to set attributes on
183+
result: The result from the function execution
184+
output_processor: Optional function to process outputs before recording
185+
"""
186+
output = output_processor(result) if output_processor else result
187+
span.set_attribute("output.value", format_object_for_trace_json(output))
188+
span.set_attribute("output.mime_type", "application/json")

src/uipath/core/tracing/decorators.py

Lines changed: 79 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Tracing decorators for function instrumentation."""
22

33
import inspect
4-
import json
54
import logging
65
import random
76
from functools import wraps
@@ -12,9 +11,9 @@
1211
from opentelemetry.trace.status import StatusCode
1312

1413
from uipath.core.tracing._utils import (
15-
format_args_for_trace_json,
16-
format_object_for_trace_json,
1714
get_supported_params,
15+
set_span_input_attributes,
16+
set_span_output_attributes,
1817
)
1918
from uipath.core.tracing.span_utils import UiPathSpanUtils
2019

@@ -45,7 +44,7 @@ def _opentelemetry_traced(
4544
recording: If False, span is not recorded
4645
"""
4746

48-
def decorator(func):
47+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
4948
trace_name = name or func.__name__
5049

5150
def get_span():
@@ -78,38 +77,30 @@ def get_span():
7877

7978
# --------- Sync wrapper ---------
8079
@wraps(func)
81-
def sync_wrapper(*args, **kwargs):
80+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
8281
span_cm, span = get_span()
8382
try:
84-
# Check if this should be treated as a tool call
85-
is_tool = span_type and span_type.upper() == "TOOL"
86-
87-
if is_tool:
88-
# Set OpenInference tool call attributes
89-
span.set_attribute("openinference.span.kind", "TOOL")
90-
span.set_attribute("tool.name", trace_name)
91-
span.set_attribute("span_type", "TOOL")
92-
else:
93-
span.set_attribute("span_type", span_type or "function_call_sync")
94-
95-
if run_type is not None:
96-
span.set_attribute("run_type", run_type)
97-
98-
inputs = format_args_for_trace_json(
99-
inspect.signature(func), *args, **kwargs
83+
# Set input attributes BEFORE execution
84+
set_span_input_attributes(
85+
span,
86+
trace_name=trace_name,
87+
wrapped_func=func,
88+
args=args,
89+
kwargs=kwargs,
90+
run_type=run_type,
91+
span_type=span_type or "function_call_sync",
92+
input_processor=input_processor,
10093
)
101-
if input_processor:
102-
processed_inputs = input_processor(json.loads(inputs))
103-
inputs = json.dumps(processed_inputs, default=str)
104-
105-
span.set_attribute("input.mime_type", "application/json")
106-
span.set_attribute("input.value", inputs)
10794

95+
# Execute the function
10896
result = func(*args, **kwargs)
109-
output = output_processor(result) if output_processor else result
11097

111-
span.set_attribute("output.value", format_object_for_trace_json(output))
112-
span.set_attribute("output.mime_type", "application/json")
98+
# Set output attributes AFTER execution
99+
set_span_output_attributes(
100+
span,
101+
result=result,
102+
output_processor=output_processor,
103+
)
113104
return result
114105
except Exception as e:
115106
span.record_exception(e)
@@ -121,38 +112,30 @@ def sync_wrapper(*args, **kwargs):
121112

122113
# --------- Async wrapper ---------
123114
@wraps(func)
124-
async def async_wrapper(*args, **kwargs):
115+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
125116
span_cm, span = get_span()
126117
try:
127-
# Check if this should be treated as a tool call
128-
is_tool = span_type and span_type.upper() == "TOOL"
129-
130-
if is_tool:
131-
# Set OpenInference tool call attributes
132-
span.set_attribute("openinference.span.kind", "TOOL")
133-
span.set_attribute("tool.name", trace_name)
134-
span.set_attribute("span_type", "TOOL")
135-
else:
136-
span.set_attribute("span_type", span_type or "function_call_async")
137-
138-
if run_type is not None:
139-
span.set_attribute("run_type", run_type)
140-
141-
inputs = format_args_for_trace_json(
142-
inspect.signature(func), *args, **kwargs
118+
# Set input attributes BEFORE execution
119+
set_span_input_attributes(
120+
span,
121+
trace_name=trace_name,
122+
wrapped_func=func,
123+
args=args,
124+
kwargs=kwargs,
125+
run_type=run_type,
126+
span_type=span_type or "function_call_async",
127+
input_processor=input_processor,
143128
)
144-
if input_processor:
145-
processed_inputs = input_processor(json.loads(inputs))
146-
inputs = json.dumps(processed_inputs, default=str)
147-
148-
span.set_attribute("input.mime_type", "application/json")
149-
span.set_attribute("input.value", inputs)
150129

130+
# Execute the function
151131
result = await func(*args, **kwargs)
152-
output = output_processor(result) if output_processor else result
153132

154-
span.set_attribute("output.value", format_object_for_trace_json(output))
155-
span.set_attribute("output.mime_type", "application/json")
133+
# Set output attributes AFTER execution
134+
set_span_output_attributes(
135+
span,
136+
result=result,
137+
output_processor=output_processor,
138+
)
156139
return result
157140
except Exception as e:
158141
span.record_exception(e)
@@ -164,42 +147,34 @@ async def async_wrapper(*args, **kwargs):
164147

165148
# --------- Generator wrapper ---------
166149
@wraps(func)
167-
def generator_wrapper(*args, **kwargs):
150+
def generator_wrapper(*args: Any, **kwargs: Any) -> Any:
168151
span_cm, span = get_span()
169152
try:
170-
# Check if this should be treated as a tool call
171-
is_tool = span_type and span_type.upper() == "TOOL"
172-
173-
if is_tool:
174-
# Set OpenInference tool call attributes
175-
span.set_attribute("openinference.span.kind", "TOOL")
176-
span.set_attribute("tool.name", trace_name)
177-
span.set_attribute("span_type", "TOOL")
178-
else:
179-
span.set_attribute(
180-
"span_type", span_type or "function_call_generator_sync"
181-
)
182-
183-
if run_type is not None:
184-
span.set_attribute("run_type", run_type)
185-
186-
inputs = format_args_for_trace_json(
187-
inspect.signature(func), *args, **kwargs
153+
# Set input attributes BEFORE execution
154+
set_span_input_attributes(
155+
span,
156+
trace_name=trace_name,
157+
wrapped_func=func,
158+
args=args,
159+
kwargs=kwargs,
160+
run_type=run_type,
161+
span_type=span_type or "function_call_generator_sync",
162+
input_processor=input_processor,
188163
)
189-
if input_processor:
190-
processed_inputs = input_processor(json.loads(inputs))
191-
inputs = json.dumps(processed_inputs, default=str)
192-
span.set_attribute("input.mime_type", "application/json")
193-
span.set_attribute("input.value", inputs)
194164

165+
# Execute the generator and collect outputs
195166
outputs = []
196167
for item in func(*args, **kwargs):
197168
outputs.append(item)
198169
span.add_event(f"Yielded: {item}")
199170
yield item
200-
output = output_processor(outputs) if output_processor else outputs
201-
span.set_attribute("output.value", format_object_for_trace_json(output))
202-
span.set_attribute("output.mime_type", "application/json")
171+
172+
# Set output attributes AFTER execution
173+
set_span_output_attributes(
174+
span,
175+
result=outputs,
176+
output_processor=output_processor,
177+
)
203178
except Exception as e:
204179
span.record_exception(e)
205180
span.set_status(StatusCode.ERROR, str(e))
@@ -210,44 +185,34 @@ def generator_wrapper(*args, **kwargs):
210185

211186
# --------- Async generator wrapper ---------
212187
@wraps(func)
213-
async def async_generator_wrapper(*args, **kwargs):
188+
async def async_generator_wrapper(*args: Any, **kwargs: Any) -> Any:
214189
span_cm, span = get_span()
215190
try:
216-
# Check if this should be treated as a tool call
217-
is_tool = span_type and span_type.upper() == "TOOL"
218-
219-
if is_tool:
220-
# Set OpenInference tool call attributes
221-
span.set_attribute("openinference.span.kind", "TOOL")
222-
span.set_attribute("tool.name", trace_name)
223-
span.set_attribute("span_type", "TOOL")
224-
else:
225-
span.set_attribute(
226-
"span_type", span_type or "function_call_generator_async"
227-
)
228-
229-
if run_type is not None:
230-
span.set_attribute("run_type", run_type)
231-
232-
inputs = format_args_for_trace_json(
233-
inspect.signature(func), *args, **kwargs
191+
# Set input attributes BEFORE execution
192+
set_span_input_attributes(
193+
span,
194+
trace_name=trace_name,
195+
wrapped_func=func,
196+
args=args,
197+
kwargs=kwargs,
198+
run_type=run_type,
199+
span_type=span_type or "function_call_generator_async",
200+
input_processor=input_processor,
234201
)
235-
if input_processor:
236-
processed_inputs = input_processor(json.loads(inputs))
237-
inputs = json.dumps(processed_inputs, default=str)
238-
239-
span.set_attribute("input.mime_type", "application/json")
240-
span.set_attribute("input.value", inputs)
241202

203+
# Execute the generator and collect outputs
242204
outputs = []
243205
async for item in func(*args, **kwargs):
244206
outputs.append(item)
245207
span.add_event(f"Yielded: {item}")
246208
yield item
247209

248-
output = output_processor(outputs) if output_processor else outputs
249-
span.set_attribute("output.value", format_object_for_trace_json(output))
250-
span.set_attribute("output.mime_type", "application/json")
210+
# Set output attributes AFTER execution
211+
set_span_output_attributes(
212+
span,
213+
result=outputs,
214+
output_processor=output_processor,
215+
)
251216
except Exception as e:
252217
span.record_exception(e)
253218
span.set_status(StatusCode.ERROR, str(e))
@@ -294,11 +259,11 @@ def traced(
294259
"""
295260

296261
# Apply default processors selectively based on hide flags
297-
def _default_input_processor(inputs):
262+
def _default_input_processor(inputs: Any) -> Any:
298263
"""Default input processor that doesn't log any actual input data."""
299264
return {"redacted": "Input data not logged for privacy/security"}
300265

301-
def _default_output_processor(outputs):
266+
def _default_output_processor(outputs: Any) -> Any:
302267
"""Default output processor that doesn't log any actual output data."""
303268
return {"redacted": "Output data not logged for privacy/security"}
304269

@@ -319,7 +284,7 @@ def _default_output_processor(outputs):
319284

320285
tracer_impl = _opentelemetry_traced
321286

322-
def decorator(func):
287+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
323288
# Check which parameters are supported by the tracer_impl
324289
supported_params = get_supported_params(tracer_impl, params)
325290

0 commit comments

Comments
 (0)