Skip to content

Commit f213b0f

Browse files
fix(llmobs): send batches of span events [backport 3.3] (#12980)
Backport 76d06b6 from #12890 to 3.3. Update spans to be sent as a batch of events. Each event contains one span, allowing us to send spans up to 1MB (the event size limit). This was a limit we assumed previously, but we were actually under a 1MB limit _per payload/batch of spans_ limit since the payload we sent to EVP intake was being treated as a single event. For script ``` for i in range(200): with LLMObs.agent( "hi" ): LLMObs.annotate( input_data = "b" * 900_000, tags = { "test_run": "after_fix" } ) pass ``` ### Before **3 out of 200 sent** <img width="920" alt="image" src="https://github.com/user-attachments/assets/8a9d681c-87d3-4d86-a969-e4458d6413d6" /> ### After **200 out of 200 sent** <img width="771" alt="image" src="https://github.com/user-attachments/assets/bffce791-9aaa-4882-a4b2-bce370c895be" /> ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) Co-authored-by: lievan <[email protected]>
1 parent 07355d9 commit f213b0f

File tree

4 files changed

+51
-28
lines changed

4 files changed

+51
-28
lines changed

ddtrace/llmobs/_writer.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,14 @@ def encode(self):
212212
return None, 0
213213
events = self._buffer
214214
self._init_buffer()
215-
data = {"_dd.stage": "raw", "_dd.tracer_version": ddtrace.__version__, "event_type": "span", "spans": events}
215+
"""
216+
Send a batch of events, where each event contains a single span in the `spans` field. This allows us to
217+
fully take advantage of EVP event/payload size limits.
218+
"""
219+
data = [
220+
{"_dd.stage": "raw", "_dd.tracer_version": ddtrace.__version__, "event_type": "span", "spans": [event]}
221+
for event in events
222+
]
216223
try:
217224
enc_llm_events = safe_json(data)
218225
logger.debug("encode %d LLMObs span events to be sent", len(events))
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
fixes:
3+
- |
4+
LLM Observability: This fix resolves an issue where large spans traced within a short time interval were dropped despite being under the 1 MB limit.

tests/llmobs/test_llmobs.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,8 @@ def test_utf_non_ascii_io(llmobs, llmobs_backend):
240240
llmobs.annotate(workflow_span, input_data="안녕, 지금 몇 시야?")
241241
events = llmobs_backend.wait_for_num_events(num=1)
242242
assert len(events) == 1
243-
assert events[0]["spans"][0]["meta"]["input"]["messages"][0]["content"] == "안녕, 지금 몇 시야?"
244-
assert events[0]["spans"][1]["meta"]["input"]["value"] == "안녕, 지금 몇 시야?"
243+
assert events[0][0]["spans"][0]["meta"]["input"]["messages"][0]["content"] == "안녕, 지금 몇 시야?"
244+
assert events[0][1]["spans"][0]["meta"]["input"]["value"] == "안녕, 지금 몇 시야?"
245245

246246

247247
def test_non_utf8_inputs_outputs(llmobs, llmobs_backend):
@@ -255,7 +255,7 @@ def test_non_utf8_inputs_outputs(llmobs, llmobs_backend):
255255
events = llmobs_backend.wait_for_num_events(num=1)
256256
assert len(events) == 1
257257
assert (
258-
events[0]["spans"][0]["meta"]["input"]["messages"][0]["content"]
258+
events[0][0]["spans"][0]["meta"]["input"]["messages"][0]["content"]
259259
== "The first Super Bowl (aka First AFL–NFL World Championship Game), was played in 1967."
260260
)
261261

@@ -267,16 +267,16 @@ def test_structured_io_data(llmobs, llmobs_backend):
267267
llmobs.annotate(span, input_data={"data": "test1"}, output_data={"data": "test2"})
268268
events = llmobs_backend.wait_for_num_events(num=1)
269269
assert len(events) == 1
270-
assert events[0]["spans"][0]["meta"]["input"]["value"] == '{"data": "test1"}'
271-
assert events[0]["spans"][0]["meta"]["output"]["value"] == '{"data": "test2"}'
270+
assert events[0][0]["spans"][0]["meta"]["input"]["value"] == '{"data": "test1"}'
271+
assert events[0][0]["spans"][0]["meta"]["output"]["value"] == '{"data": "test2"}'
272272

273273

274274
def test_structured_prompt_data(llmobs, llmobs_backend):
275275
with llmobs.llm() as span:
276276
llmobs.annotate(span, prompt={"template": "test {{value}}"})
277277
events = llmobs_backend.wait_for_num_events(num=1)
278278
assert len(events) == 1
279-
assert events[0]["spans"][0]["meta"]["input"] == {
279+
assert events[0][0]["spans"][0]["meta"]["input"] == {
280280
"prompt": {
281281
"template": "test {{value}}",
282282
"_dd_context_variable_keys": ["context"],
@@ -295,5 +295,5 @@ class CustomObj:
295295
llmobs.annotate(span, input_data=CustomObj(), output_data=CustomObj())
296296
events = llmobs_backend.wait_for_num_events(num=1)
297297
assert len(events) == 1
298-
assert expected_repr in events[0]["spans"][0]["meta"]["input"]["value"]
299-
assert expected_repr in events[0]["spans"][0]["meta"]["output"]["value"]
298+
assert expected_repr in events[0][0]["spans"][0]["meta"]["input"]["value"]
299+
assert expected_repr in events[0][0]["spans"][0]["meta"]["output"]["value"]

tests/llmobs/test_llmobs_span_encoder.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ def test_encode_span(mock_writer_logs):
1515
encoder.put([span])
1616
encoded_llm_events, n_spans = encoder.encode()
1717

18-
expected_llm_events = {
19-
"_dd.stage": "raw",
20-
"_dd.tracer_version": ddtrace.__version__,
21-
"event_type": "span",
22-
"spans": [span],
23-
}
18+
expected_llm_events = [
19+
{
20+
"_dd.stage": "raw",
21+
"_dd.tracer_version": ddtrace.__version__,
22+
"event_type": "span",
23+
"spans": [span],
24+
}
25+
]
2426

2527
assert n_spans == 1
2628
decoded_llm_events = json.loads(encoded_llm_events)
@@ -34,12 +36,20 @@ def test_encode_multiple_spans(mock_writer_logs):
3436
encoder.put(trace)
3537
encoded_llm_events, n_spans = encoder.encode()
3638

37-
expected_llm_events = {
38-
"_dd.stage": "raw",
39-
"_dd.tracer_version": ddtrace.__version__,
40-
"event_type": "span",
41-
"spans": trace,
42-
}
39+
expected_llm_events = [
40+
{
41+
"_dd.stage": "raw",
42+
"_dd.tracer_version": ddtrace.__version__,
43+
"event_type": "span",
44+
"spans": [trace[0]],
45+
},
46+
{
47+
"_dd.stage": "raw",
48+
"_dd.tracer_version": ddtrace.__version__,
49+
"event_type": "span",
50+
"spans": [trace[1]],
51+
},
52+
]
4353

4454
assert n_spans == 2
4555
decoded_llm_events = json.loads(encoded_llm_events)
@@ -53,16 +63,18 @@ def test_encode_span_with_unserializable_fields():
5363
encoder.put([span])
5464
encoded_llm_events, n_spans = encoder.encode()
5565

56-
expected_llm_events = {
57-
"_dd.stage": "raw",
58-
"_dd.tracer_version": ddtrace.__version__,
59-
"event_type": "span",
60-
"spans": [mock.ANY],
61-
}
66+
expected_llm_events = [
67+
{
68+
"_dd.stage": "raw",
69+
"_dd.tracer_version": ddtrace.__version__,
70+
"event_type": "span",
71+
"spans": [mock.ANY],
72+
}
73+
]
6274

6375
assert n_spans == 1
6476
decoded_llm_events = json.loads(encoded_llm_events)
6577
assert decoded_llm_events == expected_llm_events
66-
decoded_llm_span = decoded_llm_events["spans"][0]
78+
decoded_llm_span = decoded_llm_events[0]["spans"][0]
6779
assert decoded_llm_span["meta"]["metadata"]["unserializable"] is not None
6880
assert "<object object at 0x" in decoded_llm_span["meta"]["metadata"]["unserializable"]

0 commit comments

Comments
 (0)