Skip to content

Commit be36de0

Browse files
committed
Handled unpickled exception
1 parent e66894f commit be36de0

File tree

1 file changed

+71
-3
lines changed
  • packages/traceloop-sdk/traceloop/sdk/decorators

1 file changed

+71
-3
lines changed

packages/traceloop-sdk/traceloop/sdk/decorators/base.py

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,79 @@ def _setup_span(entity_name, tlp_span_kind, version):
172172
return span, ctx, ctx_token
173173

174174

175+
def _sanitize_for_serialization(obj):
176+
"""
177+
Recursively sanitize objects for JSON serialization by replacing
178+
unpicklable objects with string representations.
179+
"""
180+
import dataclasses
181+
import copy
182+
183+
# Handle None
184+
if obj is None:
185+
return None
186+
187+
# Handle primitive types
188+
if isinstance(obj, (str, int, float, bool)):
189+
return obj
190+
191+
# Handle lists and tuples
192+
if isinstance(obj, (list, tuple)):
193+
return type(obj)(
194+
_sanitize_for_serialization(item) for item in obj
195+
)
196+
197+
# Handle dictionaries
198+
if isinstance(obj, dict):
199+
return {
200+
key: _sanitize_for_serialization(value)
201+
for key, value in obj.items()
202+
}
203+
204+
# Handle dataclasses - try to convert, catch unpicklable objects
205+
if dataclasses.is_dataclass(obj):
206+
try:
207+
# Try shallow copy first to test if it's picklable
208+
copy.copy(obj)
209+
# If successful, try to convert to dict
210+
obj_dict = {}
211+
for field in dataclasses.fields(obj):
212+
field_value = getattr(obj, field.name)
213+
# Check if the field value is picklable
214+
try:
215+
copy.deepcopy(field_value)
216+
obj_dict[field.name] = _sanitize_for_serialization(
217+
field_value
218+
)
219+
except (TypeError, ValueError, AttributeError):
220+
# If not picklable, use string representation
221+
obj_dict[field.name] = (
222+
f"<unpicklable: {type(field_value).__name__}>"
223+
)
224+
return obj_dict
225+
except (TypeError, ValueError, AttributeError):
226+
return f"<unpicklable dataclass: {type(obj).__name__}>"
227+
228+
# For other objects, try to serialize them
229+
try:
230+
copy.deepcopy(obj)
231+
# If deepcopy succeeds, return as-is for JSON encoder
232+
return obj
233+
except (TypeError, ValueError, AttributeError):
234+
# If not picklable, return a string representation
235+
return f"<unpicklable: {type(obj).__name__}>"
236+
237+
175238
def _handle_span_input(span, args, kwargs, cls=None):
176239
"""Handles entity input logging in JSON for both sync and async functions"""
177240
try:
178241
if _should_send_prompts():
242+
243+
# Create a sanitized copy of args and kwargs that excludes unpicklable objects
244+
sanitized_args = _sanitize_for_serialization(args)
245+
sanitized_kwargs = _sanitize_for_serialization(kwargs)
179246
json_input = json.dumps(
180-
{"args": args, "kwargs": kwargs}, **({"cls": cls} if cls else {})
247+
{"args": sanitized_args, "kwargs": sanitized_kwargs}, **({"cls": cls} if cls else {})
181248
)
182249
truncated_json = _truncate_json_if_needed(json_input)
183250
span.set_attribute(
@@ -192,13 +259,14 @@ def _handle_span_output(span, res, cls=None):
192259
"""Handles entity output logging in JSON for both sync and async functions"""
193260
try:
194261
if _should_send_prompts():
195-
json_output = json.dumps(res, **({"cls": cls} if cls else {}))
262+
sanitized_res = _sanitize_for_serialization(res)
263+
json_output = json.dumps(sanitized_res, **({"cls": cls} if cls else {}))
196264
truncated_json = _truncate_json_if_needed(json_output)
197265
span.set_attribute(
198266
SpanAttributes.TRACELOOP_ENTITY_OUTPUT,
199267
truncated_json,
200268
)
201-
except TypeError as e:
269+
except (TypeError, ValueError) as e:
202270
Telemetry().log_exception(e)
203271

204272

0 commit comments

Comments
 (0)