Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package datadog.trace.core.baggage;

import static java.util.Collections.emptyMap;

import datadog.context.Context;
import datadog.context.propagation.CarrierSetter;
import datadog.context.propagation.CarrierVisitor;
Expand All @@ -19,6 +17,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -142,8 +141,7 @@ public <C> Context extract(Context context, C carrier, CarrierVisitor<C> visitor
private class BaggageExtractor implements BiConsumer<String, String> {
private static final char KEY_VALUE_SEPARATOR = '=';
private static final char PAIR_SEPARATOR = ',';
private Baggage extracted;
private String w3cHeader;
@Nullable private Baggage extracted;

/** URL decode value */
private String decode(final String value) {
Expand All @@ -156,41 +154,42 @@ private String decode(final String value) {
return decoded;
}

private Map<String, String> parseBaggageHeaders(String input) {
private Baggage parseBaggageHeaders(String input) {
Map<String, String> baggage = new HashMap<>();
int start = 0;
boolean truncatedCache = false;
String w3cHeader = input;
int pairSeparatorInd = input.indexOf(PAIR_SEPARATOR);
pairSeparatorInd = pairSeparatorInd == -1 ? input.length() : pairSeparatorInd;
int kvSeparatorInd = input.indexOf(KEY_VALUE_SEPARATOR);
while (kvSeparatorInd != -1) {
int end = pairSeparatorInd;
boolean limitReached = baggage.size() >= maxItems || end > maxBytes;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw I think that this is an approximation since end works with nb of chars and not bytes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes: byte limit is enforced against String char (UTF-16 code unit) length: exact for ASCII baggage, conservative on memory for multi-byte UTF-8 input.
Using the exact byte limit would have allocated memory which we try to prevent with this fix.

if (limitReached) {
Comment on lines +166 to +167
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record baggage extraction truncation metrics

When an incoming baggage header exceeds trace.baggage.max.items or trace.baggage.max.bytes, this new limit branch truncates the extracted baggage but never calls BAGGAGE_METRICS.onBaggageTruncatedByItemLimit() or onBaggageTruncatedByByteLimit(). In those scenarios extract() still reports a successful extraction, so telemetry consumers lose the context_header.truncated signal that injection already emits for the same configured limits.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

@PerfectSlayer PerfectSlayer May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those metrics are used for context injection, not extraction… and the team in charge of this code part and metrics already ready the patch.

// if header was not invalidated already, and we go out of range:
// - fully invalidate if it's after the first k/v pair,
// - otherwise ignore from the current k/v separator
w3cHeader = (w3cHeader == null || start == 0) ? null : w3cHeader.substring(0, start - 1);
break;
}
if (kvSeparatorInd > end) {
LOG.debug(
"Dropping baggage headers due to key with no value {}", input.substring(start, end));
BAGGAGE_METRICS.onBaggageMalformed();
return emptyMap();
return null;
}
String key = decode(input.substring(start, kvSeparatorInd).trim());
String value = decode(input.substring(kvSeparatorInd + 1, end).trim());
if (key.isEmpty() || value.isEmpty()) {
LOG.debug("Dropping baggage headers due to empty k/v {}:{}", key, value);
BAGGAGE_METRICS.onBaggageMalformed();
return emptyMap();
return null;
}
baggage.put(key, value);

// need to percent-encode non-ascii headers we pass down
if (UTF_ESCAPER.keyNeedsEncoding(key) || UTF_ESCAPER.valNeedsEncoding(value)) {
truncatedCache = true;
this.w3cHeader = null;
} else if (!truncatedCache && (end > maxBytes || baggage.size() > maxItems)) {
if (start == 0) { // if we go out of range after first k/v pair, there is no cache
this.w3cHeader = null;
} else {
this.w3cHeader = input.substring(0, start - 1); // -1 to ignore the k/v separator
}
truncatedCache = true;
if (w3cHeader != null
&& (UTF_ESCAPER.keyNeedsEncoding(key) || UTF_ESCAPER.valNeedsEncoding(value))) {
w3cHeader = null;
}

kvSeparatorInd = input.indexOf(KEY_VALUE_SEPARATOR, pairSeparatorInd + 1);
Expand All @@ -199,20 +198,16 @@ private Map<String, String> parseBaggageHeaders(String input) {
start = end + 1;
}

if (!truncatedCache) {
this.w3cHeader = input;
}

return baggage;
return baggage.isEmpty() ? null : Baggage.create(baggage, w3cHeader);
}

@Override
public void accept(String key, String value) {
// Only process tags that are relevant to baggage
if (BAGGAGE_KEY.equalsIgnoreCase(key)) {
Map<String, String> baggage = parseBaggageHeaders(value);
if (!baggage.isEmpty()) {
this.extracted = Baggage.create(baggage, this.w3cHeader);
Baggage parsed = parseBaggageHeaders(value);
if (parsed != null) {
this.extracted = parsed;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,74 @@ class BaggagePropagatorTest extends DDSpecification {
"key1=val1,key2=val2" | "key1=val1,key2=val2"
"key1=val1,key2=val2,key3=val3" | "key1=val1,key2=val2"
}

def "test baggage extract items limit"() {
setup:
propagator = new BaggagePropagator(true, true, 2, DEFAULT_TRACE_BAGGAGE_MAX_BYTES) //creating a new instance after injecting config
def headers = [
(BAGGAGE_KEY) : baggageHeader,
]

when:
context = this.propagator.extract(context, headers, ContextVisitors.stringValuesMap())

then: 'parsing stops once the item limit is exceeded'
Baggage.fromContext(context).asMap() == baggageMap

where:
baggageHeader | baggageMap
"key1=val1" | [key1: "val1"]
"key1=val1,key2=val2" | [key1: "val1", key2: "val2"]
"key1=val1,key2=val2,key3=val3" | [key1: "val1", key2: "val2"]
}

def "test baggage extract bytes limit"() {
setup:
propagator = new BaggagePropagator(true, true, DEFAULT_TRACE_BAGGAGE_MAX_ITEMS, 20) //creating a new instance after injecting config
def headers = [
(BAGGAGE_KEY) : baggageHeader,
]

when:
context = this.propagator.extract(context, headers, ContextVisitors.stringValuesMap())

then: 'parsing stops once the byte limit is exceeded'
Baggage.fromContext(context).asMap() == baggageMap

where:
baggageHeader | baggageMap
"key1=val1" | [key1: "val1"]
"key1=val1,key2=val2" | [key1: "val1", key2: "val2"]
"key1=val1,key2=val2,key3=val3" | [key1: "val1", key2: "val2"]
}

def "test baggage extract 0 item limit"() {
setup:
propagator = new BaggagePropagator(true, true, 0, DEFAULT_TRACE_BAGGAGE_MAX_BYTES) //creating a new instance after injecting config
def headers = [
(BAGGAGE_KEY) : "key1=value1",
]

when:
context = this.propagator.extract(context, headers, ContextVisitors.stringValuesMap())

then:
Baggage.fromContext(context) == null
}



def "test baggage extract 0 byte limit"() {
setup:
propagator = new BaggagePropagator(true, true, DEFAULT_TRACE_BAGGAGE_MAX_ITEMS, 0) //creating a new instance after injecting config
def headers = [
(BAGGAGE_KEY) : "key1=value1",
]

when:
context = this.propagator.extract(context, headers, ContextVisitors.stringValuesMap())

then:
Baggage.fromContext(context) == null
}
}
Loading