-
Notifications
You must be signed in to change notification settings - Fork 304
Improve Baggage API #8523
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve Baggage API #8523
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,17 @@ | ||
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; | ||
import datadog.context.propagation.Propagator; | ||
import datadog.trace.api.Config; | ||
import datadog.trace.bootstrap.instrumentation.api.BaggageContext; | ||
import datadog.trace.core.util.EscapedData; | ||
import datadog.trace.bootstrap.instrumentation.api.Baggage; | ||
import datadog.trace.core.util.PercentEscaper; | ||
import datadog.trace.core.util.PercentEscaper.Escaped; | ||
import java.io.UnsupportedEncodingException; | ||
import java.net.URLDecoder; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.function.BiConsumer; | ||
|
@@ -20,7 +21,7 @@ | |
|
||
@ParametersAreNonnullByDefault | ||
public class BaggagePropagator implements Propagator { | ||
private static final Logger log = LoggerFactory.getLogger(BaggagePropagator.class); | ||
private static final Logger LOG = LoggerFactory.getLogger(BaggagePropagator.class); | ||
private static final PercentEscaper UTF_ESCAPER = PercentEscaper.create(); | ||
static final String BAGGAGE_KEY = "baggage"; | ||
private final Config config; | ||
|
@@ -37,13 +38,13 @@ public BaggagePropagator(Config config) { | |
public BaggagePropagator(boolean injectBaggage, boolean extractBaggage) { | ||
this.injectBaggage = injectBaggage; | ||
this.extractBaggage = extractBaggage; | ||
config = Config.get(); | ||
this.config = Config.get(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't worry about the |
||
} | ||
|
||
@Override | ||
public <C> void inject(Context context, C carrier, CarrierSetter<C> setter) { | ||
int maxItems = config.getTraceBaggageMaxItems(); | ||
int maxBytes = config.getTraceBaggageMaxBytes(); | ||
int maxItems = this.config.getTraceBaggageMaxItems(); | ||
int maxBytes = this.config.getTraceBaggageMaxBytes(); | ||
mcculls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
//noinspection ConstantValue | ||
if (!this.injectBaggage | ||
|| maxItems == 0 | ||
|
@@ -54,52 +55,52 @@ public <C> void inject(Context context, C carrier, CarrierSetter<C> setter) { | |
return; | ||
} | ||
|
||
BaggageContext baggageContext = BaggageContext.fromContext(context); | ||
if (baggageContext == null) { | ||
log.debug("BaggageContext instance is missing from the following context {}", context); | ||
Baggage baggage = Baggage.fromContext(context); | ||
if (baggage == null) { | ||
LOG.debug("Baggage instance is missing from the following context {}", context); | ||
return; | ||
} | ||
|
||
String baggageHeader = baggageContext.getW3cBaggageHeader(); | ||
if (baggageHeader != null) { | ||
setter.set(carrier, BAGGAGE_KEY, baggageHeader); | ||
String headerValue = baggage.getW3cHeader(); | ||
if (headerValue != null) { | ||
setter.set(carrier, BAGGAGE_KEY, headerValue); | ||
return; | ||
} | ||
|
||
int processedBaggage = 0; | ||
int processedItems = 0; | ||
int currentBytes = 0; | ||
StringBuilder baggageText = new StringBuilder(); | ||
for (final Map.Entry<String, String> entry : baggageContext.asMap().entrySet()) { | ||
for (final Map.Entry<String, String> entry : baggage.asMap().entrySet()) { | ||
// if there are already baggage items processed, add and allocate bytes for a comma | ||
int extraBytes = 1; | ||
if (processedBaggage != 0) { | ||
if (processedItems != 0) { | ||
baggageText.append(','); | ||
extraBytes++; | ||
} | ||
|
||
EscapedData escapedKey = UTF_ESCAPER.escapeKey(entry.getKey()); | ||
EscapedData escapedVal = UTF_ESCAPER.escapeValue(entry.getValue()); | ||
Escaped escapedKey = UTF_ESCAPER.escapeKey(entry.getKey()); | ||
Escaped escapedVal = UTF_ESCAPER.escapeValue(entry.getValue()); | ||
|
||
baggageText.append(escapedKey.getData()); | ||
baggageText.append(escapedKey.data); | ||
baggageText.append('='); | ||
baggageText.append(escapedVal.getData()); | ||
baggageText.append(escapedVal.data); | ||
|
||
processedBaggage++; | ||
processedItems++; | ||
// reached the max number of baggage items allowed | ||
if (processedBaggage == maxItems) { | ||
if (processedItems == maxItems) { | ||
break; | ||
} | ||
// Drop newest k/v pair if adding it leads to exceeding the limit | ||
if (currentBytes + escapedKey.getSize() + escapedVal.getSize() + extraBytes > maxBytes) { | ||
if (currentBytes + escapedKey.size + escapedVal.size + extraBytes > maxBytes) { | ||
baggageText.setLength(currentBytes); | ||
break; | ||
} | ||
currentBytes += escapedKey.getSize() + escapedVal.getSize() + extraBytes; | ||
currentBytes += escapedKey.size + escapedVal.size + extraBytes; | ||
} | ||
|
||
String baggageString = baggageText.toString(); | ||
baggageContext.setW3cBaggageHeader(baggageString); | ||
setter.set(carrier, BAGGAGE_KEY, baggageString); | ||
headerValue = baggageText.toString(); | ||
baggage.setW3cHeader(headerValue); | ||
setter.set(carrier, BAGGAGE_KEY, headerValue); | ||
} | ||
|
||
@Override | ||
|
@@ -108,57 +109,52 @@ public <C> Context extract(Context context, C carrier, CarrierVisitor<C> visitor | |
if (!this.extractBaggage || context == null || carrier == null || visitor == null) { | ||
return context; | ||
} | ||
BaggageContextExtractor baggageContextExtractor = new BaggageContextExtractor(); | ||
visitor.forEachKeyValue(carrier, baggageContextExtractor); | ||
BaggageContext extractedContext = baggageContextExtractor.extractedContext; | ||
if (extractedContext == null) { | ||
return context; | ||
} | ||
return extractedContext.storeInto(context); | ||
BaggageExtractor baggageExtractor = new BaggageExtractor(); | ||
visitor.forEachKeyValue(carrier, baggageExtractor); | ||
return baggageExtractor.extracted == null ? context : context.with(baggageExtractor.extracted); | ||
} | ||
|
||
public static class BaggageContextExtractor implements BiConsumer<String, String> { | ||
private BaggageContext extractedContext; | ||
private static class BaggageExtractor implements BiConsumer<String, String> { | ||
private static final char KEY_VALUE_SEPARATOR = '='; | ||
private static final char PAIR_SEPARATOR = ','; | ||
Comment on lines
+118
to
+119
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracted few constants as class constant for clarity, avoiding magic string within method body. |
||
private Baggage extracted; | ||
|
||
BaggageContextExtractor() {} | ||
private BaggageExtractor() {} | ||
|
||
/** URL decode value */ | ||
private String decode(final String value) { | ||
String decoded = value; | ||
try { | ||
decoded = URLDecoder.decode(value, "UTF-8"); | ||
} catch (final UnsupportedEncodingException | IllegalArgumentException e) { | ||
log.debug("Failed to decode {}", value); | ||
LOG.debug("Failed to decode {}", value); | ||
} | ||
return decoded; | ||
} | ||
|
||
private Map<String, String> parseBaggageHeaders(String input) { | ||
Map<String, String> baggage = new HashMap<>(); | ||
char keyValueSeparator = '='; | ||
char pairSeparator = ','; | ||
int start = 0; | ||
|
||
int pairSeparatorInd = input.indexOf(pairSeparator); | ||
int pairSeparatorInd = input.indexOf(PAIR_SEPARATOR); | ||
pairSeparatorInd = pairSeparatorInd == -1 ? input.length() : pairSeparatorInd; | ||
int kvSeparatorInd = input.indexOf(keyValueSeparator); | ||
int kvSeparatorInd = input.indexOf(KEY_VALUE_SEPARATOR); | ||
while (kvSeparatorInd != -1) { | ||
int end = pairSeparatorInd; | ||
if (kvSeparatorInd > end) { | ||
log.debug( | ||
LOG.debug( | ||
"Dropping baggage headers due to key with no value {}", input.substring(start, end)); | ||
return Collections.emptyMap(); | ||
return emptyMap(); | ||
} | ||
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); | ||
return Collections.emptyMap(); | ||
LOG.debug("Dropping baggage headers due to empty k/v {}:{}", key, value); | ||
return emptyMap(); | ||
} | ||
baggage.put(key, value); | ||
|
||
kvSeparatorInd = input.indexOf(keyValueSeparator, pairSeparatorInd + 1); | ||
pairSeparatorInd = input.indexOf(pairSeparator, pairSeparatorInd + 1); | ||
kvSeparatorInd = input.indexOf(KEY_VALUE_SEPARATOR, pairSeparatorInd + 1); | ||
pairSeparatorInd = input.indexOf(PAIR_SEPARATOR, pairSeparatorInd + 1); | ||
pairSeparatorInd = pairSeparatorInd == -1 ? input.length() : pairSeparatorInd; | ||
start = end + 1; | ||
} | ||
|
@@ -168,10 +164,10 @@ private Map<String, String> parseBaggageHeaders(String input) { | |
@Override | ||
public void accept(String key, String value) { | ||
// Only process tags that are relevant to baggage | ||
if (key != null && key.equalsIgnoreCase(BAGGAGE_KEY)) { | ||
if (BAGGAGE_KEY.equalsIgnoreCase(key)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would not checking for null here potentially lead to a NullPointerException? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting question. This changes was made to simplify the checke and denotes a common rule for Java object equality check: if you have value V to test against a constant C, you would better write While So you end up always writing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh interesting, I never knew that but it makes sense! |
||
Map<String, String> baggage = parseBaggageHeaders(value); | ||
if (!baggage.isEmpty()) { | ||
extractedContext = BaggageContext.create(baggage, value); | ||
this.extracted = Baggage.create(baggage, value); | ||
} | ||
} | ||
} | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -107,25 +107,24 @@ private static boolean[] createUnsafeOctets(String safeChars) { | |
return octets; | ||
} | ||
|
||
public EscapedData escapeKey(String s) { | ||
public Escaped escapeKey(String s) { | ||
return escape(s, unsafeKeyOctets); | ||
} | ||
|
||
public EscapedData escapeValue(String s) { | ||
public Escaped escapeValue(String s) { | ||
return escape(s, unsafeValOctets); | ||
} | ||
|
||
/** Escape the provided String, using percent-style URL Encoding. */ | ||
public EscapedData escape(String s, boolean[] unsafeOctets) { | ||
int size = 0; | ||
public Escaped escape(String s, boolean[] unsafeOctets) { | ||
int slen = s.length(); | ||
for (int index = 0; index < slen; index++) { | ||
char c = s.charAt(index); | ||
if (c > '~' || c <= ' ' || c <= unsafeOctets.length && unsafeOctets[c]) { | ||
return escapeSlow(s, index, unsafeOctets); | ||
} | ||
} | ||
return new EscapedData(s, slen); | ||
return new Escaped(s, slen); | ||
} | ||
|
||
/* | ||
|
@@ -147,14 +146,14 @@ public EscapedData escape(String s, boolean[] unsafeOctets) { | |
* @throws NullPointerException if {@code string} is null | ||
* @throws IllegalArgumentException if invalid surrogate characters are encountered | ||
*/ | ||
private static EscapedData escapeSlow(String s, int index, boolean[] unsafeOctets) { | ||
private static Escaped escapeSlow(String s, int index, boolean[] unsafeOctets) { | ||
int end = s.length(); | ||
|
||
// Get a destination buffer and setup some loop variables. | ||
char[] dest = new char[1024]; // 1024 from the original guava source | ||
int destIndex = 0; | ||
int unescapedChunkStart = 0; | ||
EscapedData data = new EscapedData("", index); | ||
Escaped result = new Escaped("", index); | ||
|
||
while (index < end) { | ||
int cp = codePointAt(s, index, end); | ||
|
@@ -164,7 +163,7 @@ private static EscapedData escapeSlow(String s, int index, boolean[] unsafeOctet | |
// It is possible for this to return null because nextEscapeIndex() may | ||
// (for performance reasons) yield some false positives but it must never | ||
// give false negatives. | ||
char[] escaped = escape(cp, data, unsafeOctets); | ||
char[] escaped = escape(cp, result, unsafeOctets); | ||
int nextIndex = index + (Character.isSupplementaryCodePoint(cp) ? 2 : 1); | ||
if (escaped != null) { | ||
int charsSkipped = index - unescapedChunkStart; | ||
|
@@ -202,10 +201,11 @@ private static EscapedData escapeSlow(String s, int index, boolean[] unsafeOctet | |
s.getChars(unescapedChunkStart, end, dest, destIndex); | ||
destIndex = endIndex; | ||
} | ||
data.addSize(charsSkipped); // Adding characters in-between characters that want to be encoded | ||
// Adding characters in-between characters that want to be encoded | ||
result.size += charsSkipped; | ||
|
||
data.setData(new String(dest, 0, destIndex)); | ||
return data; | ||
result.data = new String(dest, 0, destIndex); | ||
return result; | ||
} | ||
|
||
private static int nextEscapeIndex(CharSequence csq, int index, int end, boolean[] unsafeOctets) { | ||
|
@@ -221,7 +221,7 @@ private static int nextEscapeIndex(CharSequence csq, int index, int end, boolean | |
/** Escapes the given Unicode code point in UTF-8. */ | ||
@CheckForNull | ||
@SuppressWarnings("UngroupedOverloads") | ||
private static char[] escape(int cp, EscapedData data, boolean[] unsafeOctets) { | ||
private static char[] escape(int cp, Escaped escaped, boolean[] unsafeOctets) { | ||
// We should never get negative values here but if we do it will throw an | ||
// IndexOutOfBoundsException, so at least it will get spotted. | ||
if (cp < unsafeOctets.length && !unsafeOctets[cp]) { | ||
|
@@ -233,7 +233,7 @@ private static char[] escape(int cp, EscapedData data, boolean[] unsafeOctets) { | |
dest[0] = '%'; | ||
dest[2] = UPPER_HEX_DIGITS[cp & 0xF]; | ||
dest[1] = UPPER_HEX_DIGITS[cp >>> 4]; | ||
data.incrementSize(); | ||
escaped.size++; | ||
return dest; | ||
} else if (cp <= 0x7ff) { | ||
// Two byte UTF-8 characters [cp >= 0x80 && cp <= 0x7ff] | ||
|
@@ -248,7 +248,7 @@ private static char[] escape(int cp, EscapedData data, boolean[] unsafeOctets) { | |
dest[2] = UPPER_HEX_DIGITS[cp & 0xF]; | ||
cp >>>= 4; | ||
dest[1] = UPPER_HEX_DIGITS[0xC | cp]; | ||
data.addSize(2); | ||
escaped.size += 2; | ||
return dest; | ||
} else if (cp <= 0xffff) { | ||
// Three byte UTF-8 characters [cp >= 0x800 && cp <= 0xffff] | ||
|
@@ -267,7 +267,7 @@ private static char[] escape(int cp, EscapedData data, boolean[] unsafeOctets) { | |
dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; | ||
cp >>>= 2; | ||
dest[2] = UPPER_HEX_DIGITS[cp]; | ||
data.addSize(3); | ||
escaped.size += 3; | ||
return dest; | ||
} else if (cp <= 0x10ffff) { | ||
char[] dest = new char[12]; | ||
|
@@ -291,7 +291,7 @@ private static char[] escape(int cp, EscapedData data, boolean[] unsafeOctets) { | |
dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; | ||
cp >>>= 2; | ||
dest[2] = UPPER_HEX_DIGITS[cp & 0x7]; | ||
data.addSize(4); | ||
escaped.size += 4; | ||
return dest; | ||
} else { | ||
// If this ever happens it is due to bug in UnicodeEscaper, not bad input. | ||
|
@@ -386,4 +386,14 @@ private static char[] growBuffer(char[] dest, int index, int size) { | |
} | ||
return copy; | ||
} | ||
|
||
public static class Escaped { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went with a very basic structure like object. This whole object thing is a not great but a general mitigation of Java lang as not being able to return multiple values. |
||
public String data; | ||
public int size; | ||
|
||
public Escaped(String data, int size) { | ||
this.data = data; | ||
this.size = size; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static final vars are considered as constants and are uppercased by convention
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I admit to having mixed views on this - I understand the convention of marking static final's as upper-cased constants, but find using
LOG.info(...)
rather glaring in the code.Do we want to codify this in the spotless config and update all the code to use this convention, or just follow it for new files?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. What's the rationale behind your feeling?
LOG
being a constant, it makes sense to me.During reviews, I usually end up looking for the definition of anything I can't find in the immediate scope that is not a constant (
UPPER_CASE
) nor a field member (this.field
).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Past habit of distinguishing between value constants (denoting a fixed simple value) and singletons (which are similar in that the reference doesn't change, but internally things vary.)
I'm in no way consistent about this though, having picked up the use of
INSTANCE
while working in this codebase :)