[OTLP Logs] Fix ArgumentOutOfRangeException serializing large log batches#8858
[OTLP Logs] Fix ArgumentOutOfRangeException serializing large log batches#8858NachoEchevarria wants to merge 3 commits into
Conversation
Execution-Time Benchmarks Report ⏱️Execution-time results for samples comparing This PR (8858) and master. ✅ No regressions detected - check the details below Full Metrics ComparisonFakeDbCommand
HttpMessageHandler
Comparison explanationExecution-time benchmarks measure the whole time it takes to execute a program, and are intended to measure the one-off costs. Cases where the execution time results for the PR are worse than latest master results are highlighted in **red**. The following thresholds were used for comparing the execution times:
Note that these results are based on a single point-in-time result for each branch. For full results, see the dashboard. Graphs show the p99 interval based on the mean and StdDev of the test run, as well as the mean value of the run (shown as a diamond below the graph). Duration chartsFakeDbCommand (.NET Framework 4.8)gantt
title Execution time (ms) FakeDbCommand (.NET Framework 4.8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8858) - mean (70ms) : 68, 72
master - mean (72ms) : 68, 76
section Bailout
This PR (8858) - mean (76ms) : 73, 80
master - mean (75ms) : 72, 77
section CallTarget+Inlining+NGEN
This PR (8858) - mean (1,087ms) : 1039, 1135
master - mean (1,081ms) : 1030, 1133
FakeDbCommand (.NET Core 3.1)gantt
title Execution time (ms) FakeDbCommand (.NET Core 3.1)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8858) - mean (110ms) : 105, 115
master - mean (111ms) : 106, 117
section Bailout
This PR (8858) - mean (115ms) : 109, 120
master - mean (110ms) : 108, 112
section CallTarget+Inlining+NGEN
This PR (8858) - mean (773ms) : 750, 796
master - mean (776ms) : 756, 796
FakeDbCommand (.NET 6)gantt
title Execution time (ms) FakeDbCommand (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8858) - mean (101ms) : 95, 107
master - mean (99ms) : 95, 104
section Bailout
This PR (8858) - mean (98ms) : 95, 102
master - mean (97ms) : 95, 99
section CallTarget+Inlining+NGEN
This PR (8858) - mean (935ms) : 885, 986
master - mean (937ms) : 898, 976
FakeDbCommand (.NET 8)gantt
title Execution time (ms) FakeDbCommand (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8858) - mean (94ms) : 92, 97
master - mean (95ms) : 93, 98
section Bailout
This PR (8858) - mean (99ms) : 91, 106
master - mean (99ms) : 93, 106
section CallTarget+Inlining+NGEN
This PR (8858) - mean (815ms) : 774, 855
master - mean (810ms) : 770, 849
HttpMessageHandler (.NET Framework 4.8)gantt
title Execution time (ms) HttpMessageHandler (.NET Framework 4.8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8858) - mean (204ms) : 200, 208
master - mean (201ms) : 196, 206
section Bailout
This PR (8858) - mean (208ms) : 203, 214
master - mean (206ms) : 203, 209
section CallTarget+Inlining+NGEN
This PR (8858) - mean (1,218ms) : 1169, 1267
master - mean (1,205ms) : 1170, 1240
HttpMessageHandler (.NET Core 3.1)gantt
title Execution time (ms) HttpMessageHandler (.NET Core 3.1)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8858) - mean (296ms) : 289, 303
master - mean (288ms) : 281, 295
section Bailout
This PR (8858) - mean (300ms) : 287, 312
master - mean (289ms) : 284, 295
section CallTarget+Inlining+NGEN
This PR (8858) - mean (998ms) : 974, 1022
master - mean (975ms) : 949, 1000
HttpMessageHandler (.NET 6)gantt
title Execution time (ms) HttpMessageHandler (.NET 6)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8858) - mean (295ms) : 285, 305
master - mean (285ms) : 279, 290
section Bailout
This PR (8858) - mean (297ms) : 287, 307
master - mean (285ms) : 280, 289
section CallTarget+Inlining+NGEN
This PR (8858) - mean (1,193ms) : 1160, 1226
master - mean (1,170ms) : 1134, 1207
HttpMessageHandler (.NET 8)gantt
title Execution time (ms) HttpMessageHandler (.NET 8)
dateFormat x
axisFormat %Q
todayMarker off
section Baseline
This PR (8858) - mean (291ms) : 276, 305
master - mean (282ms) : 275, 289
section Bailout
This PR (8858) - mean (293ms) : 280, 306
master - mean (283ms) : 277, 289
section CallTarget+Inlining+NGEN
This PR (8858) - mean (1,092ms) : 977, 1207
master - mean (1,049ms) : 1006, 1092
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||
|
|
||
| var buffer = new byte[64 * 1024]; | ||
| var buffer = new byte[InitialBufferSize]; |
There was a problem hiding this comment.
I know not introduced here, but I wonder if we should be using an array pool for this? 🤔 If so, then the caller would need to own the pool, so we would have to move all this logic up into the exporter, and convert this into an API like this?
public static boot TrySerializeLogs(IReadOnlyList<LogPoint> logs, byte[] buffer, ResourceTags settings, int startPosition = 0)There was a problem hiding this comment.
I think we should do it, but probably, in a subsequent PR, WDYT?
There was a problem hiding this comment.
Maybe, but it would mean changing most of the code you added here, seeing as all the retry logic would need to move up into OtlpExporter 🤔 I'm fine if you think it's better for a separate PR, it's just it feels like that would undo this one 😄
There was a problem hiding this comment.
I created a draft alternative PR with the pool array. https://github.com/DataDog/dd-trace-dotnet/pull/8862/changes
| // Upper bound for a serialized batch. The buffer grows up to this cap; a batch that still | ||
| // doesn't fit is dropped rather than allocating without bound. The intake rejects payloads | ||
| // far larger than this anyway, and it matches the default trace payload cap (DD_TRACE_BUFFER_SIZE). | ||
| private const int MaxBufferSize = 10 * 1024 * 1024; |
There was a problem hiding this comment.
Just FYI, for our own log submission, we have a max buffer size of 3MB... 10MB feels like a lot but 🤷♂️ 😅
There was a problem hiding this comment.
3MB seems fine and is aligned with the rest of the code, thanks!
BenchmarksBenchmark execution time: 2026-07-01 14:20:42 Comparing candidate commit 206a3f2 in PR branch Found 0 performance improvements and 1 performance regressions! Performance is the same for 71 metrics, 0 unstable metrics, 59 known flaky benchmarks, 67 flaky benchmarks without significant changes.
|
Summary of changes
Fix an
ArgumentOutOfRangeExceptionwhen serializing OTLP log batches that exceed 64 KB, and bound the serialization buffer.Seen here.
Reason for change
OtlpLogsSerializer.SerializeLogswrote into a fixed 64 KB buffer with no bounds checking or resizing. Once a batch's serialized size crossed 64 KB, the next write threwArgumentOutOfRangeException(surfacing inWriteLogRecorddue to inlining), the batch failed to send, and it was dropped.This is rare in practice — error tracking shows very few occurrences — but when it does happen it's a hard failure that silently drops the batch, so it's worth guarding against.
Implementation details
ArgumentException/IndexOutOfRangeExceptionthe writers already throw; since overflow is the rare case, exception-driven detection keeps the common path allocation-free rather than pre-sizing every batch.nullso it's dropped rather than allocating without bound.OtlpExporterlogs a warning and reports success on a dropped batch, so the batching sink doesn't trip its circuit breaker for this non-transient case.Test coverage
Added
OtlpLogsSerializerTests: small/empty batches, grow-and-succeed (the original crash repro), oversized batch returns null, and start-position handling.Other details