Skip to content

Commit b098984

Browse files
mccullsclaude
andcommitted
Limit growth of OTLP trace buffer
* Proactively flush OTLP traces between scheduled flushes to try to keep payload size below 5 MiB * Add hard limit of 64 MiB to OTLP payloads, as recommended in https://opentelemetry.io/docs/specs/otlp/ Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent af2d214 commit b098984

11 files changed

Lines changed: 294 additions & 11 deletions

File tree

components/json/src/main/java/datadog/json/JsonWriter.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public final class JsonWriter implements Flushable, AutoCloseable {
1919
private final JsonStructure structure;
2020

2121
private boolean requireComma;
22+
private int bytesWritten;
2223

2324
/** Creates a writer with structure check. */
2425
public JsonWriter() {
@@ -245,6 +246,11 @@ public void flush() {
245246
}
246247
}
247248

249+
/** Approximate number of bytes written so far. */
250+
public int sizeInBytes() {
251+
return this.bytesWritten;
252+
}
253+
248254
@Override
249255
public void close() {
250256
try {
@@ -268,13 +274,15 @@ private void endsValue() {
268274
private void write(char ch) {
269275
try {
270276
this.writer.write(ch);
277+
this.bytesWritten++;
271278
} catch (IOException ignored) {
272279
}
273280
}
274281

275282
private void writeStringLiteral(String str) {
276283
try {
277284
this.writer.write('"');
285+
int count = 1;
278286

279287
for (int i = 0; i < str.length(); ++i) {
280288
char c = str.charAt(i);
@@ -286,33 +294,40 @@ private void writeStringLiteral(String str) {
286294
this.writer.write(HEX_DIGITS[(c >>> 8) & 0xF]);
287295
this.writer.write(HEX_DIGITS[(c >>> 4) & 0xF]);
288296
this.writer.write(HEX_DIGITS[c & 0xF]);
297+
count += 6;
289298
} else {
290299
switch (c) {
291300
case '"': // Quotation mark
292301
case '\\': // Reverse solidus
293302
case '/': // Solidus
294303
this.writer.write('\\');
295304
this.writer.write(c);
305+
count += 2;
296306
break;
297307
case '\b': // Backspace
298308
this.writer.write('\\');
299309
this.writer.write('b');
310+
count += 2;
300311
break;
301312
case '\f': // Form feed
302313
this.writer.write('\\');
303314
this.writer.write('f');
315+
count += 2;
304316
break;
305317
case '\n': // Line feed
306318
this.writer.write('\\');
307319
this.writer.write('n');
320+
count += 2;
308321
break;
309322
case '\r': // Carriage return
310323
this.writer.write('\\');
311324
this.writer.write('r');
325+
count += 2;
312326
break;
313327
case '\t': // Horizontal tab
314328
this.writer.write('\\');
315329
this.writer.write('t');
330+
count += 2;
316331
break;
317332
default:
318333
if (c < 0x20) {
@@ -322,22 +337,28 @@ private void writeStringLiteral(String str) {
322337
this.writer.write('0');
323338
this.writer.write(HEX_DIGITS[(c >>> 4) & 0xF]);
324339
this.writer.write(HEX_DIGITS[c & 0xF]);
340+
count += 6;
325341
} else {
326342
this.writer.write(c);
343+
count += 1;
327344
}
328345
break;
329346
}
330347
}
331348
}
332349

333350
this.writer.write('"');
351+
count += 1;
352+
353+
this.bytesWritten += count;
334354
} catch (IOException ignored) {
335355
}
336356
}
337357

338358
private void writeStringRaw(String str) {
339359
try {
340360
this.writer.write(str);
361+
this.bytesWritten += str.length(); // exact if ASCII, estimate otherwise
341362
} catch (IOException ignored) {
342363
}
343364
}

components/json/src/test/java/datadog/json/JsonWriterTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,54 @@ void testCompleteObject() {
165165
}
166166
}
167167

168+
@Test
169+
void testSizeInBytes() {
170+
try (JsonWriter writer = new JsonWriter()) {
171+
assertEquals(0, writer.sizeInBytes(), "Check empty writer size");
172+
173+
writer.beginArray();
174+
assertSizeInBytes(writer, "Check size after plain ASCII string");
175+
writer.value("bar");
176+
177+
assertSizeInBytes(writer, "Check size after escaped characters");
178+
writer.value("\"\\/");
179+
180+
assertSizeInBytes(writer, "Check size after named escapes");
181+
writer.value("\b\f\n\r\t");
182+
183+
assertSizeInBytes(writer, "Check size after control character escape");
184+
writer.value("\u0001");
185+
186+
assertSizeInBytes(writer, "Check size after non-ASCII character escape");
187+
writer.value("café");
188+
189+
assertSizeInBytes(writer, "Check size after int value");
190+
writer.value(3);
191+
192+
assertSizeInBytes(writer, "Check size after long value");
193+
writer.value(3456789123L);
194+
195+
assertSizeInBytes(writer, "Check size after float value");
196+
writer.value(3.142f);
197+
198+
assertSizeInBytes(writer, "Check size after double value");
199+
writer.value(PI);
200+
201+
assertSizeInBytes(writer, "Check size after boolean value");
202+
writer.value(true);
203+
204+
assertSizeInBytes(writer, "Check size after null value");
205+
writer.nullValue();
206+
207+
writer.endArray();
208+
assertSizeInBytes(writer, "Check final size matches written bytes");
209+
}
210+
}
211+
212+
private static void assertSizeInBytes(JsonWriter writer, String message) {
213+
assertEquals(writer.toByteArray().length, writer.sizeInBytes(), message);
214+
}
215+
168216
@Test
169217
void testCompleteArray() {
170218
try (JsonWriter writer = new JsonWriter()) {

dd-trace-core/src/main/java/datadog/trace/common/writer/OtlpPayloadDispatcher.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@
77
import java.util.Collection;
88
import java.util.Collections;
99
import java.util.List;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
1012

1113
final class OtlpPayloadDispatcher implements PayloadDispatcher {
14+
private static final Logger log = LoggerFactory.getLogger(OtlpPayloadDispatcher.class);
15+
16+
private static final int FLUSH_THRESHOLD_BYTES = 5 << 20; // 5 MiB
17+
1218
private final OtlpTraceCollector collector;
1319
private final OtlpSender sender;
1420

@@ -20,13 +26,21 @@ final class OtlpPayloadDispatcher implements PayloadDispatcher {
2026
@Override
2127
public void addTrace(List<? extends CoreSpan<?>> trace) {
2228
collector.addTrace(trace);
29+
// flush proactively to keep payload size bounded
30+
if (collector.sizeInBytes() >= FLUSH_THRESHOLD_BYTES) {
31+
flush();
32+
}
2333
}
2434

2535
@Override
2636
public void flush() {
27-
OtlpPayload payload = collector.collectTraces();
28-
if (payload != OtlpPayload.EMPTY) {
29-
sender.send(payload);
37+
try {
38+
OtlpPayload payload = collector.collectTraces();
39+
if (payload != OtlpPayload.EMPTY) {
40+
sender.send(payload);
41+
}
42+
} catch (Throwable e) {
43+
log.debug("Failed to send OTLP payload", e);
3044
}
3145
}
3246

dd-trace-core/src/main/java/datadog/trace/core/otlp/common/OtlpProtoBuffer.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,23 @@
1818
* @see GrowableBuffer
1919
*/
2020
public final class OtlpProtoBuffer {
21+
// hard limit to avoid unbounded buffering; matches OTLP spec's recommended default
22+
public static final int MAX_CAPACITY_BYTES = 64 << 20; // 64 MiB
23+
2124
private final int initialCapacity;
2225
private ByteBuffer buffer;
2326
private int remaining;
2427

2528
public OtlpProtoBuffer(int requiredCapacity) {
2629
this.initialCapacity = nextPowerOfTwo(requiredCapacity);
30+
if (this.initialCapacity > MAX_CAPACITY_BYTES) {
31+
throw new IllegalArgumentException(
32+
"OTLP payload initial capacity of "
33+
+ this.initialCapacity
34+
+ " bytes exceeds maximum buffer size of "
35+
+ MAX_CAPACITY_BYTES
36+
+ " bytes");
37+
}
2738
this.buffer = ByteBuffer.allocate(initialCapacity);
2839
this.remaining = initialCapacity;
2940
}
@@ -96,6 +107,11 @@ public ByteBuffer flip() {
96107
return buffer;
97108
}
98109

110+
/** Returns the number of bytes currently recorded in the buffer. */
111+
public int sizeInBytes() {
112+
return buffer.capacity() - remaining;
113+
}
114+
99115
/**
100116
* Returns an {@link OtlpPayload} containing the protobuf encoded content.
101117
*
@@ -123,10 +139,21 @@ private void checkCapacity(int required) {
123139
ByteBuffer oldBuffer = flip();
124140
int oldSize = oldBuffer.remaining();
125141
// round up to next multiple of initialCapacity that can accommodate required
126-
int newSize = (oldSize + required + initialCapacity - 1) & -initialCapacity;
127-
ByteBuffer newBuffer = ByteBuffer.allocate(newSize);
142+
// (uses long arithmetic so overflow can be detected before allocating)
143+
long newSize = ((long) oldSize + required + initialCapacity - 1) & -initialCapacity;
144+
if (newSize > MAX_CAPACITY_BYTES) {
145+
throw new IllegalStateException(
146+
"OTLP payload exceeds maximum buffer size of "
147+
+ MAX_CAPACITY_BYTES
148+
+ " bytes: "
149+
+ oldSize
150+
+ " bytes buffered, "
151+
+ required
152+
+ " more requested");
153+
}
154+
ByteBuffer newBuffer = ByteBuffer.allocate((int) newSize);
128155
// copy over old content so it stays at the far end
129-
remaining = newSize - oldSize;
156+
remaining = (int) newSize - oldSize;
130157
newBuffer.position(remaining);
131158
newBuffer.put(oldBuffer);
132159
buffer = newBuffer;

dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceCollector.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ public abstract class OtlpTraceCollector {
1515
/** Collects all spans added since the last collection. */
1616
public abstract OtlpPayload collectTraces();
1717

18+
/** Returns the number of bytes buffered since the last collection. */
19+
public abstract int sizeInBytes();
20+
1821
protected final boolean shouldExport(CoreSpan<?> span) {
1922
return span.samplingPriority() > 0 // trace-level sampling priority
2023
|| span.getTag(SPAN_SAMPLING_MECHANISM_TAG) != null; // span-level sampling priority

dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceJsonCollector.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static datadog.trace.core.otlp.common.OtlpCommonJson.writeScopeAndSchema;
44
import static datadog.trace.core.otlp.common.OtlpPayload.JSON_CONTENT_TYPE;
5+
import static datadog.trace.core.otlp.common.OtlpProtoBuffer.MAX_CAPACITY_BYTES;
56
import static datadog.trace.core.otlp.common.OtlpResourceJson.TRACE_RESOURCE_FRAGMENT;
67
import static datadog.trace.core.otlp.trace.OtlpTraceJson.writeSpan;
78

@@ -54,11 +55,22 @@ public void addTrace(List<? extends CoreSpan<?>> spans) {
5455
payloadStarted = true;
5556
}
5657

57-
for (CoreSpan<?> span : spans) {
58-
visitSpan(span);
58+
try {
59+
for (CoreSpan<?> span : spans) {
60+
visitSpan(span);
61+
}
62+
} catch (Throwable e) {
63+
// reset the buffer for subsequent traces
64+
stop();
65+
throw e;
5966
}
6067
}
6168

69+
@Override
70+
public int sizeInBytes() {
71+
return writer == null ? 0 : writer.sizeInBytes();
72+
}
73+
6274
/**
6375
* Marshals the traces collected so far into a JSON payload.
6476
*
@@ -178,5 +190,10 @@ private void completeSpan() {
178190
// reset temporary elements for next span
179191
currentSpan = null;
180192
currentSpanLinks = Collections.emptyList();
193+
194+
if (writer.sizeInBytes() > MAX_CAPACITY_BYTES) {
195+
throw new IllegalStateException(
196+
"OTLP payload exceeds maximum buffer size of " + MAX_CAPACITY_BYTES + " bytes");
197+
}
181198
}
182199
}

dd-trace-core/src/main/java/datadog/trace/core/otlp/trace/OtlpTraceProtoCollector.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,23 @@ public void addTrace(List<? extends CoreSpan<?>> spans) {
5656
payloadStarted = true;
5757
}
5858

59-
// OtlpProtoBuffer collects spans in reverse
60-
for (int i = spans.size() - 1; i >= 0; i--) {
61-
visitSpan(spans.get(i));
59+
try {
60+
// OtlpProtoBuffer collects spans in reverse
61+
for (int i = spans.size() - 1; i >= 0; i--) {
62+
visitSpan(spans.get(i));
63+
}
64+
} catch (Throwable e) {
65+
// reset the buffer for subsequent traces
66+
stop();
67+
throw e;
6268
}
6369
}
6470

71+
@Override
72+
public int sizeInBytes() {
73+
return protobuf.sizeInBytes();
74+
}
75+
6576
/**
6677
* Marshals the traces collected so far into a chunked payload.
6778
*

0 commit comments

Comments
 (0)