Skip to content

Commit b47d44a

Browse files
committed
Propagate sampling metadata across trace chunks
1 parent ca4153b commit b47d44a

3 files changed

Lines changed: 163 additions & 0 deletions

File tree

dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,6 +1310,9 @@ void write(final SpanList trace) {
13101310
spanToSample.forceKeep(forceKeep);
13111311
boolean published = forceKeep || traceCollector.sample(spanToSample);
13121312
if (published) {
1313+
if (rootSpan != null && writtenTrace.get(0) != rootSpan) {
1314+
writtenTrace.get(0).spanContext().copyDecisionMakerFrom(rootSpan.spanContext());
1315+
}
13131316
if (!apmTracingEnabled) {
13141317
// Stamp the billing marker on every span of each exported chunk so the intake does not bill
13151318
// APM host usage.

dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,6 +1393,10 @@ public PropagationTags getPropagationTags() {
13931393
return getRootSpanContextOrThis().propagationTags;
13941394
}
13951395

1396+
void copyDecisionMakerFrom(DDSpanContext source) {
1397+
propagationTags.updateAndLockDecisionMaker(source.getPropagationTags());
1398+
}
1399+
13961400
/** TraceSegment Implementation */
13971401
@Override
13981402
public void setTagTop(String key, Object value, boolean sanitize) {
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package datadog.trace.core;
2+
3+
import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP;
4+
import static datadog.trace.api.sampling.SamplingMechanism.REMOTE_ADAPTIVE_RULE;
5+
import static datadog.trace.common.sampling.RuleBasedTraceSampler.SAMPLING_RULE_RATE;
6+
import static java.util.Collections.singletonList;
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
import static org.junit.jupiter.api.Assertions.assertTrue;
9+
10+
import datadog.communication.serialization.ByteBufferConsumer;
11+
import datadog.communication.serialization.FlushingBuffer;
12+
import datadog.communication.serialization.msgpack.MsgPackWriter;
13+
import datadog.trace.common.writer.ListWriter;
14+
import datadog.trace.common.writer.ddagent.TraceMapperV0_4;
15+
import java.nio.ByteBuffer;
16+
import java.util.ArrayList;
17+
import java.util.HashMap;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.concurrent.TimeoutException;
21+
import org.junit.jupiter.api.Test;
22+
import org.msgpack.core.MessagePack;
23+
import org.msgpack.core.MessageUnpacker;
24+
25+
/**
26+
* Verifies that trace-level sampling metadata is preserved when a root and a later child are
27+
* exported in separate chunks (e.g. during partial sampling).
28+
*/
29+
class LateSpanSamplingMechanismSerializationTest extends DDCoreJavaSpecification {
30+
31+
@Override
32+
protected boolean useStrictTraceWrites() {
33+
return false;
34+
}
35+
36+
@Test
37+
void rootlessLateChunkRetainsAdaptiveSamplingMechanism()
38+
throws InterruptedException, TimeoutException {
39+
SerializingWriter writer = new SerializingWriter();
40+
CoreTracer tracer =
41+
tracerBuilder()
42+
.writer(writer)
43+
// Keep the partial-flush threshold above the trace size so the pending-trace idle
44+
// timeout controls the first write.
45+
.partialFlushMinSpans(1000)
46+
.build();
47+
48+
try {
49+
DDSpan root = (DDSpan) tracer.buildSpan("test", "root").start();
50+
DDSpan lateChild =
51+
(DDSpan) tracer.buildSpan("test", "late-child").asChildOf(root.spanContext()).start();
52+
PendingTrace trace = (PendingTrace) root.spanContext().getTraceCollector();
53+
54+
root.setSamplingPriority(USER_KEEP, REMOTE_ADAPTIVE_RULE);
55+
root.setMetric(SAMPLING_RULE_RATE, 0.25);
56+
57+
// Root finishes while the child is still running -> root is buffered, not yet written.
58+
root.finish();
59+
assertTrue(writer.isEmpty());
60+
61+
// Write the buffered root as the PendingTraceBuffer worker does after its idle timeout.
62+
trace.write();
63+
writer.waitForTraces(1);
64+
65+
// The child finishes after the root was written, so it is exported in a root-less chunk.
66+
lateChild.finish();
67+
trace.write();
68+
writer.waitForTraces(2);
69+
70+
assertEquals(singletonList(root), writer.get(0));
71+
assertEquals(singletonList(lateChild), writer.get(1));
72+
73+
SerializedChunk rootChunk = writer.serializedChunks.get(0);
74+
SerializedChunk lateChunk = writer.serializedChunks.get(1);
75+
76+
assertEquals(
77+
(int) USER_KEEP, rootChunk.metrics.get(DDSpanContext.PRIORITY_SAMPLING_KEY).intValue());
78+
assertEquals("-12", rootChunk.meta.get("_dd.p.dm"));
79+
80+
assertEquals(
81+
(int) USER_KEEP, lateChunk.metrics.get(DDSpanContext.PRIORITY_SAMPLING_KEY).intValue());
82+
// The late chunk's decision maker must match its propagated trace-level sampling priority.
83+
assertEquals("-12", lateChunk.meta.get("_dd.p.dm"));
84+
} finally {
85+
tracer.close();
86+
}
87+
}
88+
89+
private static final class SerializingWriter extends ListWriter {
90+
private final List<SerializedChunk> serializedChunks = new ArrayList<>();
91+
92+
@Override
93+
public void write(List<DDSpan> trace) {
94+
serializedChunks.add(serialize(trace));
95+
super.write(trace);
96+
}
97+
98+
private static SerializedChunk serialize(List<DDSpan> trace) {
99+
TraceMapperV0_4 mapper = new TraceMapperV0_4();
100+
CaptureBuffer capture = new CaptureBuffer();
101+
MsgPackWriter packer = new MsgPackWriter(new FlushingBuffer(16 * 1024, capture));
102+
assertTrue(packer.format(trace, mapper));
103+
packer.flush();
104+
105+
try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(capture.bytes)) {
106+
assertEquals(1, unpacker.unpackArrayHeader());
107+
int fieldCount = unpacker.unpackMapHeader();
108+
Map<String, Number> metrics = new HashMap<>();
109+
Map<String, String> meta = new HashMap<>();
110+
111+
for (int i = 0; i < fieldCount; i++) {
112+
String field = unpacker.unpackString();
113+
if ("metrics".equals(field)) {
114+
int size = unpacker.unpackMapHeader();
115+
for (int j = 0; j < size; j++) {
116+
metrics.put(unpacker.unpackString(), unpacker.unpackValue().asNumberValue().toInt());
117+
}
118+
} else if ("meta".equals(field)) {
119+
int size = unpacker.unpackMapHeader();
120+
for (int j = 0; j < size; j++) {
121+
meta.put(unpacker.unpackString(), unpacker.unpackString());
122+
}
123+
} else {
124+
unpacker.unpackValue();
125+
}
126+
}
127+
return new SerializedChunk(metrics, meta);
128+
} catch (Exception e) {
129+
throw new IllegalStateException("Unable to decode serialized trace chunk", e);
130+
} finally {
131+
mapper.reset();
132+
}
133+
}
134+
}
135+
136+
private static final class CaptureBuffer implements ByteBufferConsumer {
137+
private byte[] bytes;
138+
139+
@Override
140+
public void accept(int messageCount, ByteBuffer buffer) {
141+
assertEquals(1, messageCount);
142+
bytes = new byte[buffer.remaining()];
143+
buffer.get(bytes);
144+
}
145+
}
146+
147+
private static final class SerializedChunk {
148+
private final Map<String, Number> metrics;
149+
private final Map<String, String> meta;
150+
151+
private SerializedChunk(Map<String, Number> metrics, Map<String, String> meta) {
152+
this.metrics = metrics;
153+
this.meta = meta;
154+
}
155+
}
156+
}

0 commit comments

Comments
 (0)