Skip to content

Commit c0dd1c8

Browse files
authored
fix(sdk): respect truncation otel environment variable (#3212)
1 parent d82510e commit c0dd1c8

File tree

2 files changed

+150
-13
lines changed

2 files changed

+150
-13
lines changed

packages/traceloop-sdk/tests/test_tasks.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,129 @@ def dataclass_task(data: TestDataClass):
153153
"field1": "value1",
154154
"field2": 123,
155155
}
156+
157+
158+
def test_json_truncation_with_otel_limit(exporter, monkeypatch):
159+
"""Test that JSON input/output is truncated when OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT is set"""
160+
# Set environment variable to a small limit for testing
161+
monkeypatch.setenv("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", "50")
162+
163+
@task(name="truncation_task")
164+
def truncation_task(long_input):
165+
# Return a long output that will also be truncated
166+
return "This is a very long output string that should definitely exceed the 50 character limit"
167+
168+
# Call with a long input that will be truncated
169+
long_input = "This is a very long input string that should definitely exceed the 50 character limit"
170+
truncation_task(long_input)
171+
172+
spans = exporter.get_finished_spans()
173+
task_span = spans[0]
174+
175+
# Check that input was truncated
176+
input_json = task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT]
177+
assert len(input_json) == 50
178+
assert input_json.startswith('{"args": ["This is a very long input string that s')
179+
180+
# Check that output was truncated
181+
output_json = task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT]
182+
assert len(output_json) == 50
183+
assert output_json.startswith('"This is a very long output string that should def')
184+
185+
186+
def test_json_no_truncation_without_otel_limit(exporter, monkeypatch):
187+
"""Test that JSON input/output is not truncated when OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT is not set"""
188+
# Ensure environment variable is not set
189+
monkeypatch.delenv("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", raising=False)
190+
191+
@task(name="no_truncation_task")
192+
def no_truncation_task(long_input):
193+
return "This is a very long output string that would be truncated if limits were set but should remain intact"
194+
195+
long_input = "This is a very long input string that would be truncated if limits were set but should remain intact"
196+
result = no_truncation_task(long_input)
197+
198+
spans = exporter.get_finished_spans()
199+
task_span = spans[0]
200+
201+
# Check that input was not truncated
202+
input_data = json.loads(task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT])
203+
assert input_data["args"][0] == long_input
204+
205+
# Check that output was not truncated
206+
output_data = json.loads(task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT])
207+
assert output_data == result
208+
209+
210+
def test_json_truncation_with_invalid_otel_limit(exporter, monkeypatch):
211+
"""Test that invalid OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT values are ignored"""
212+
# Set environment variable to invalid value
213+
monkeypatch.setenv("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", "not_a_number")
214+
215+
@task(name="invalid_limit_task")
216+
def invalid_limit_task(test_input):
217+
return "This output should not be truncated because the limit is invalid"
218+
219+
test_input = "This input should not be truncated because the limit is invalid"
220+
result = invalid_limit_task(test_input)
221+
222+
spans = exporter.get_finished_spans()
223+
task_span = spans[0]
224+
225+
# Check that input was not truncated (since invalid limit should be ignored)
226+
input_data = json.loads(task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT])
227+
assert input_data["args"][0] == test_input
228+
229+
# Check that output was not truncated
230+
output_data = json.loads(task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT])
231+
assert output_data == result
232+
233+
234+
@pytest.mark.asyncio
235+
async def test_async_json_truncation_with_otel_limit(exporter, monkeypatch):
236+
"""Test that JSON truncation works with async tasks"""
237+
# Set environment variable to a small limit for testing
238+
monkeypatch.setenv("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", "40")
239+
240+
@task(name="async_truncation_task")
241+
async def async_truncation_task(long_input):
242+
await asyncio.sleep(0.1) # Simulate async work
243+
return "This is a long async output that should be truncated"
244+
245+
long_input = "This is a long async input that should be truncated"
246+
await async_truncation_task(long_input)
247+
248+
spans = exporter.get_finished_spans()
249+
task_span = spans[0]
250+
251+
# Check that input was truncated
252+
input_json = task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT]
253+
assert len(input_json) == 40
254+
255+
# Check that output was truncated
256+
output_json = task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT]
257+
assert len(output_json) == 40
258+
259+
260+
def test_json_truncation_preserves_short_content(exporter, monkeypatch):
261+
"""Test that short content is not affected by truncation limits"""
262+
# Set environment variable to a limit larger than our content
263+
monkeypatch.setenv("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT", "1000")
264+
265+
@task(name="short_content_task")
266+
def short_content_task(short_input):
267+
return "short output"
268+
269+
short_input = "short input"
270+
result = short_content_task(short_input)
271+
272+
spans = exporter.get_finished_spans()
273+
task_span = spans[0]
274+
275+
# Check that short input was preserved completely
276+
input_data = json.loads(task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_INPUT])
277+
assert input_data["args"][0] == short_input
278+
279+
# Check that short output was preserved completely
280+
output_data = json.loads(task_span.attributes[SpanAttributes.TRACELOOP_ENTITY_OUTPUT])
281+
assert output_data == result

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

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,20 @@
3535
F = TypeVar("F", bound=Callable[P, R | Awaitable[R]])
3636

3737

38-
def _is_json_size_valid(json_str: str) -> bool:
39-
"""Check if JSON string size is less than 1MB"""
40-
return len(json_str) < 1_000_000
38+
def _truncate_json_if_needed(json_str: str) -> str:
39+
"""
40+
Truncate JSON string if it exceeds OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT;
41+
truncation may yield an invalid JSON string, which is expected for logging purposes.
42+
"""
43+
limit_str = os.getenv("OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT")
44+
if limit_str:
45+
try:
46+
limit = int(limit_str)
47+
if limit > 0 and len(json_str) > limit:
48+
return json_str[:limit]
49+
except ValueError:
50+
pass
51+
return json_str
4152

4253

4354
# Async Decorators - Deprecated
@@ -163,11 +174,11 @@ def _handle_span_input(span, args, kwargs, cls=None):
163174
json_input = json.dumps(
164175
{"args": args, "kwargs": kwargs}, **({"cls": cls} if cls else {})
165176
)
166-
if _is_json_size_valid(json_input):
167-
span.set_attribute(
168-
SpanAttributes.TRACELOOP_ENTITY_INPUT,
169-
json_input,
170-
)
177+
truncated_json = _truncate_json_if_needed(json_input)
178+
span.set_attribute(
179+
SpanAttributes.TRACELOOP_ENTITY_INPUT,
180+
truncated_json,
181+
)
171182
except TypeError as e:
172183
Telemetry().log_exception(e)
173184

@@ -177,11 +188,11 @@ def _handle_span_output(span, res, cls=None):
177188
try:
178189
if _should_send_prompts():
179190
json_output = json.dumps(res, **({"cls": cls} if cls else {}))
180-
if _is_json_size_valid(json_output):
181-
span.set_attribute(
182-
SpanAttributes.TRACELOOP_ENTITY_OUTPUT,
183-
json_output,
184-
)
191+
truncated_json = _truncate_json_if_needed(json_output)
192+
span.set_attribute(
193+
SpanAttributes.TRACELOOP_ENTITY_OUTPUT,
194+
truncated_json,
195+
)
185196
except TypeError as e:
186197
Telemetry().log_exception(e)
187198

0 commit comments

Comments
 (0)