diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index 21ddcd20c8c..f3b58a88f71 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -1,5 +1,6 @@ package datadog.trace.api; +import static datadog.trace.api.TracePropagationStyle.BAGGAGE; import static datadog.trace.api.TracePropagationStyle.DATADOG; import static datadog.trace.api.TracePropagationStyle.TRACECONTEXT; import static java.util.Arrays.asList; @@ -78,9 +79,11 @@ public final class ConfigDefaults { static final int DEFAULT_PARTIAL_FLUSH_MIN_SPANS = 1000; static final boolean DEFAULT_PROPAGATION_EXTRACT_LOG_HEADER_NAMES_ENABLED = false; static final Set DEFAULT_TRACE_PROPAGATION_STYLE = - new LinkedHashSet<>(asList(DATADOG, TRACECONTEXT)); + new LinkedHashSet<>(asList(DATADOG, TRACECONTEXT, BAGGAGE)); static final Set DEFAULT_PROPAGATION_STYLE = new LinkedHashSet<>(asList(PropagationStyle.DATADOG)); + static final int DEFAULT_TRACE_BAGGAGE_MAX_ITEMS = 64; + static final int DEFAULT_TRACE_BAGGAGE_MAX_BYTES = 8192; static final boolean DEFAULT_JMX_FETCH_ENABLED = true; static final boolean DEFAULT_TRACE_AGENT_V05_ENABLED = false; diff --git a/dd-trace-api/src/main/java/datadog/trace/api/TracePropagationStyle.java b/dd-trace-api/src/main/java/datadog/trace/api/TracePropagationStyle.java index 192978cc388..af850871650 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/TracePropagationStyle.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/TracePropagationStyle.java @@ -21,6 +21,9 @@ public enum TracePropagationStyle { // W3C trace context propagation style // https://www.w3.org/TR/trace-context-1/ TRACECONTEXT, + // W3C baggage support + // https://www.w3.org/TR/baggage/ + BAGGAGE, // None does not extract or inject NONE; diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java index b3426d8d989..e4cebef3308 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/TracerConfig.java @@ -91,6 +91,8 @@ public final class TracerConfig { public static final String TRACE_PROPAGATION_STYLE_EXTRACT = "trace.propagation.style.extract"; public static final String TRACE_PROPAGATION_STYLE_INJECT = "trace.propagation.style.inject"; public static final String TRACE_PROPAGATION_EXTRACT_FIRST = "trace.propagation.extract.first"; + public static final String TRACE_BAGGAGE_MAX_ITEMS = "trace.baggage.max.items"; + public static final String TRACE_BAGGAGE_MAX_BYTES = "trace.baggage.max.bytes"; public static final String ENABLE_TRACE_AGENT_V05 = "trace.agent.v0.5.enabled"; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java index 1c40901945b..4e043347faf 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreTracer.java @@ -5,6 +5,7 @@ import static datadog.trace.api.DDTags.DJM_ENABLED; import static datadog.trace.api.DDTags.DSM_ENABLED; import static datadog.trace.api.DDTags.PROFILING_CONTEXT_ENGINE; +import static datadog.trace.bootstrap.instrumentation.api.AgentPropagation.BAGGAGE_CONCERN; import static datadog.trace.bootstrap.instrumentation.api.AgentPropagation.DSM_CONCERN; import static datadog.trace.bootstrap.instrumentation.api.AgentPropagation.TRACING_CONCERN; import static datadog.trace.bootstrap.instrumentation.api.AgentPropagation.XRAY_TRACING_CONCERN; @@ -77,6 +78,7 @@ import datadog.trace.common.writer.WriterFactory; import datadog.trace.common.writer.ddintake.DDIntakeTraceInterceptor; import datadog.trace.context.TraceScope; +import datadog.trace.core.baggage.BaggagePropagator; import datadog.trace.core.datastreams.DataStreamsMonitoring; import datadog.trace.core.datastreams.DefaultDataStreamsMonitoring; import datadog.trace.core.flare.TracerFlarePoller; @@ -719,6 +721,9 @@ private CoreTracer( if (config.isDataStreamsEnabled()) { Propagators.register(DSM_CONCERN, this.dataStreamsMonitoring.propagator()); } + if (config.isBaggagePropagationEnabled()) { + Propagators.register(BAGGAGE_CONCERN, new BaggagePropagator(config)); + } this.tagInterceptor = null == tagInterceptor ? new TagInterceptor(new RuleFlags(config)) : tagInterceptor; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/baggage/BaggagePropagator.java b/dd-trace-core/src/main/java/datadog/trace/core/baggage/BaggagePropagator.java new file mode 100644 index 00000000000..67f8702291e --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/baggage/BaggagePropagator.java @@ -0,0 +1,179 @@ +package datadog.trace.core.baggage; + +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.core.util.PercentEscaper; +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; +import javax.annotation.ParametersAreNonnullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ParametersAreNonnullByDefault +public class BaggagePropagator implements Propagator { + 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; + private final boolean injectBaggage; + private final boolean extractBaggage; + + public BaggagePropagator(Config config) { + this.injectBaggage = config.isBaggageInject(); + this.extractBaggage = config.isBaggageExtract(); + this.config = config; + } + + // use primarily for testing purposes + public BaggagePropagator(boolean injectBaggage, boolean extractBaggage) { + this.injectBaggage = injectBaggage; + this.extractBaggage = extractBaggage; + config = Config.get(); + } + + @Override + public void inject(Context context, C carrier, CarrierSetter setter) { + int maxItems = config.getTraceBaggageMaxItems(); + int maxBytes = config.getTraceBaggageMaxBytes(); + //noinspection ConstantValue + if (!this.injectBaggage + || maxItems == 0 + || maxBytes == 0 + || context == null + || carrier == null + || setter == null) { + return; + } + + BaggageContext baggageContext = BaggageContext.fromContext(context); + if (baggageContext == null) { + log.debug("BaggageContext instance is missing from the following context {}", context); + return; + } + + String baggageHeader = baggageContext.getW3cBaggageHeader(); + if (baggageHeader != null) { + setter.set(carrier, BAGGAGE_KEY, baggageHeader); + return; + } + + int processedBaggage = 0; + int currentBytes = 0; + StringBuilder baggageText = new StringBuilder(); + for (final Map.Entry entry : baggageContext.asMap().entrySet()) { + // if there are already baggage items processed, add and allocate bytes for a comma + int extraBytes = 1; + if (processedBaggage != 0) { + baggageText.append(','); + extraBytes++; + } + + EscapedData escapedKey = UTF_ESCAPER.escapeKey(entry.getKey()); + EscapedData escapedVal = UTF_ESCAPER.escapeValue(entry.getValue()); + + baggageText.append(escapedKey.getData()); + baggageText.append('='); + baggageText.append(escapedVal.getData()); + + processedBaggage++; + // reached the max number of baggage items allowed + if (processedBaggage == maxItems) { + break; + } + // Drop newest k/v pair if adding it leads to exceeding the limit + if (currentBytes + escapedKey.getSize() + escapedVal.getSize() + extraBytes > maxBytes) { + baggageText.setLength(currentBytes); + break; + } + currentBytes += escapedKey.getSize() + escapedVal.getSize() + extraBytes; + } + + String baggageString = baggageText.toString(); + baggageContext.setW3cBaggageHeader(baggageString); + setter.set(carrier, BAGGAGE_KEY, baggageString); + } + + @Override + public Context extract(Context context, C carrier, CarrierVisitor visitor) { + //noinspection ConstantValue + 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); + } + + public static class BaggageContextExtractor implements BiConsumer { + private BaggageContext extractedContext; + + BaggageContextExtractor() {} + + /** 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); + } + return decoded; + } + + private Map parseBaggageHeaders(String input) { + Map baggage = new HashMap<>(); + char keyValueSeparator = '='; + char pairSeparator = ','; + int start = 0; + + int pairSeparatorInd = input.indexOf(pairSeparator); + pairSeparatorInd = pairSeparatorInd == -1 ? input.length() : pairSeparatorInd; + int kvSeparatorInd = input.indexOf(keyValueSeparator); + while (kvSeparatorInd != -1) { + int end = pairSeparatorInd; + if (kvSeparatorInd > end) { + log.debug( + "Dropping baggage headers due to key with no value {}", input.substring(start, end)); + return Collections.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(); + } + baggage.put(key, value); + + kvSeparatorInd = input.indexOf(keyValueSeparator, pairSeparatorInd + 1); + pairSeparatorInd = input.indexOf(pairSeparator, pairSeparatorInd + 1); + pairSeparatorInd = pairSeparatorInd == -1 ? input.length() : pairSeparatorInd; + start = end + 1; + } + return baggage; + } + + @Override + public void accept(String key, String value) { + // Only process tags that are relevant to baggage + if (key != null && key.equalsIgnoreCase(BAGGAGE_KEY)) { + Map baggage = parseBaggageHeaders(value); + if (!baggage.isEmpty()) { + extractedContext = BaggageContext.create(baggage, value); + } + } + } + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/HttpCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/HttpCodec.java index f1e65298d24..9835d2e1c03 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/HttpCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/HttpCodec.java @@ -125,6 +125,8 @@ private static Map createInjectors( case TRACECONTEXT: result.put(style, W3CHttpCodec.newInjector(reverseBaggageMapping)); break; + case BAGGAGE: + break; default: log.debug("No implementation found to inject propagation style: {}", style); break; @@ -159,6 +161,8 @@ public static Extractor createExtractor( case TRACECONTEXT: extractors.add(W3CHttpCodec.newExtractor(config, traceConfigSupplier)); break; + case BAGGAGE: + break; default: log.debug("No implementation found to extract propagation style: {}", style); break; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/util/EscapedData.java b/dd-trace-core/src/main/java/datadog/trace/core/util/EscapedData.java new file mode 100644 index 00000000000..0ff428d2f60 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/util/EscapedData.java @@ -0,0 +1,36 @@ +package datadog.trace.core.util; + +public class EscapedData { + private String data; + private int size; + + public EscapedData(String data, int size) { + this.data = data; + this.size = size; + } + + public EscapedData() { + this.data = ""; + this.size = 0; + } + + public String getData() { + return data; + } + + public int getSize() { + return size; + } + + public void setData(String data) { + this.data = data; + } + + public void incrementSize() { + size++; + } + + public void addSize(int delta) { + size += delta; + } +} diff --git a/dd-trace-core/src/main/java/datadog/trace/core/util/PercentEscaper.java b/dd-trace-core/src/main/java/datadog/trace/core/util/PercentEscaper.java new file mode 100644 index 00000000000..8bd6a7b21d5 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/util/PercentEscaper.java @@ -0,0 +1,389 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Includes work from: +/* + * Copyright (C) 2008 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package datadog.trace.core.util; + +import javax.annotation.CheckForNull; + +/** + * Note: This class is based on code from guava. It is comprised of code from three classes: + * + * + * + *

Escapes some set of Java characters using a UTF-8 based percent encoding scheme. The set of + * safe characters (those which remain unescaped) can be specified on construction. + * + *

This class is primarily used for creating URI escapers in {@code UrlEscapers} but can be used + * directly if required. While URI escapers impose specific semantics on which characters are + * considered 'safe', this class has a minimal set of restrictions. + * + *

When escaping a String, the following rules apply: + * + *

    + *
  • All specified safe characters remain unchanged. + *
  • If {@code plusForSpace} was specified, the space character " " is converted into a plus + * sign {@code "+"}. + *
  • All other characters are converted into one or more bytes using UTF-8 encoding and each + * byte is then represented by the 3-character string "%XX", where "XX" is the two-digit, + * uppercase, hexadecimal representation of the byte value. + *
+ * + *

For performance reasons the only currently supported character encoding of this class is + * UTF-8. + * + *

Note: This escaper produces uppercase hexadecimal sequences. + * + *

This class is internal and is hence not for public use. Its APIs are unstable and can change + * at any time. + * + * @author David Beaumont + * @since 15.0 + */ +public final class PercentEscaper { + + /** The amount of padding (chars) to use when growing the escape buffer. */ + private static final int DEST_PAD = 32; + + private static final String UNSAFE_CHARACTERS_KEY = "\",;\\()/:<=>?@[]{} "; + private static final String UNSAFE_CHARACTERS_VALUE = "\",;\\ "; + + // Percent escapers output upper case hex digits (uri escapers require this). + private static final char[] UPPER_HEX_DIGITS = "0123456789ABCDEF".toCharArray(); + + /** + * Arrays of flags where for any {@code char c} if {@code safeOctets[c]} is true then {@code c} + * should remain unmodified in the output. If {@code c >= safeOctets.length} then it should be + * escaped. + */ + private static final boolean[] unsafeKeyOctets = createUnsafeOctets(UNSAFE_CHARACTERS_KEY); + + private static final boolean[] unsafeValOctets = createUnsafeOctets(UNSAFE_CHARACTERS_VALUE); + + /** The default {@link PercentEscaper} which will *not* replace spaces with plus signs. */ + public static PercentEscaper create() { + return new PercentEscaper(); + } + + /** + * Creates a boolean array with entries corresponding to the character values specified in + * safeChars set to true. The array is as small as is required to hold the given character + * information. + */ + private static boolean[] createUnsafeOctets(String safeChars) { + int maxChar = -1; + char[] safeCharArray = safeChars.toCharArray(); + for (char c : safeCharArray) { + maxChar = Math.max(c, maxChar); + } + boolean[] octets = new boolean[maxChar + 1]; + for (char c : safeCharArray) { + octets[c] = true; + } + return octets; + } + + public EscapedData escapeKey(String s) { + return escape(s, unsafeKeyOctets); + } + + public EscapedData 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; + 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); + } + + /* + * Overridden for performance. For unescaped strings this improved the performance of the uri + * escaper from ~760ns to ~400ns as measured by {@link CharEscapersBenchmark}. + */ + /** + * Returns the escaped form of a given literal string, starting at the given index. This method is + * called by the {@link #escape(String, boolean[])} method when it discovers that escaping is + * required. It is protected to allow subclasses to override the fastpath escaping function to + * inline their escaping test. + * + *

This method is not reentrant and may only be invoked by the top level {@link #escape(String, + * boolean[])} method. + * + * @param s the literal string to be escaped + * @param index the index to start escaping from + * @return the escaped form of {@code string} + * @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) { + 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); + + while (index < end) { + int cp = codePointAt(s, index, end); + if (cp < 0) { + throw new IllegalArgumentException("Trailing high surrogate at end of input"); + } + // 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); + int nextIndex = index + (Character.isSupplementaryCodePoint(cp) ? 2 : 1); + if (escaped != null) { + int charsSkipped = index - unescapedChunkStart; + + // This is the size needed to add the replacement, not the full + // size needed by the string. We only regrow when we absolutely must. + int sizeNeeded = destIndex + charsSkipped + escaped.length; + if (dest.length < sizeNeeded) { + int destLength = sizeNeeded + (end - index) + DEST_PAD; + dest = growBuffer(dest, destIndex, destLength); + } + // If we have skipped any characters, we need to copy them now. + if (charsSkipped > 0) { + s.getChars(unescapedChunkStart, index, dest, destIndex); + destIndex += charsSkipped; + } + if (escaped.length > 0) { + System.arraycopy(escaped, 0, dest, destIndex, escaped.length); + destIndex += escaped.length; + } + // If we dealt with an escaped character, reset the unescaped range. + unescapedChunkStart = nextIndex; + } + index = nextEscapeIndex(s, nextIndex, end, unsafeOctets); + } + + // Process trailing unescaped characters - no need to account for escaped + // length or padding the allocation. + int charsSkipped = end - unescapedChunkStart; + if (charsSkipped > 0) { + int endIndex = destIndex + charsSkipped; + if (dest.length < endIndex) { + dest = growBuffer(dest, destIndex, endIndex); + } + s.getChars(unescapedChunkStart, end, dest, destIndex); + destIndex = endIndex; + } + data.addSize(charsSkipped); // Adding characters in-between characters that want to be encoded + + data.setData(new String(dest, 0, destIndex)); + return data; + } + + private static int nextEscapeIndex(CharSequence csq, int index, int end, boolean[] unsafeOctets) { + for (; index < end; index++) { + char c = csq.charAt(index); + if (c <= unsafeOctets.length && unsafeOctets[c]) { + break; + } + } + return index; + } + + /** Escapes the given Unicode code point in UTF-8. */ + @CheckForNull + @SuppressWarnings("UngroupedOverloads") + private static char[] escape(int cp, EscapedData data, 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]) { + return null; + } else if (cp <= 0x7F) { + // Single byte UTF-8 characters + // Start with "%--" and fill in the blanks + char[] dest = new char[3]; + dest[0] = '%'; + dest[2] = UPPER_HEX_DIGITS[cp & 0xF]; + dest[1] = UPPER_HEX_DIGITS[cp >>> 4]; + data.incrementSize(); + return dest; + } else if (cp <= 0x7ff) { + // Two byte UTF-8 characters [cp >= 0x80 && cp <= 0x7ff] + // Start with "%--%--" and fill in the blanks + char[] dest = new char[6]; + dest[0] = '%'; + dest[3] = '%'; + dest[5] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; + cp >>>= 2; + dest[2] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[1] = UPPER_HEX_DIGITS[0xC | cp]; + data.addSize(2); + return dest; + } else if (cp <= 0xffff) { + // Three byte UTF-8 characters [cp >= 0x800 && cp <= 0xffff] + // Start with "%E-%--%--" and fill in the blanks + char[] dest = new char[9]; + dest[0] = '%'; + dest[1] = 'E'; + dest[3] = '%'; + dest[6] = '%'; + dest[8] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[7] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; + cp >>>= 2; + dest[5] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; + cp >>>= 2; + dest[2] = UPPER_HEX_DIGITS[cp]; + data.addSize(3); + return dest; + } else if (cp <= 0x10ffff) { + char[] dest = new char[12]; + // Four byte UTF-8 characters [cp >= 0xffff && cp <= 0x10ffff] + // Start with "%F-%--%--%--" and fill in the blanks + dest[0] = '%'; + dest[1] = 'F'; + dest[3] = '%'; + dest[6] = '%'; + dest[9] = '%'; + dest[11] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[10] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; + cp >>>= 2; + dest[8] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[7] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; + cp >>>= 2; + dest[5] = UPPER_HEX_DIGITS[cp & 0xF]; + cp >>>= 4; + dest[4] = UPPER_HEX_DIGITS[0x8 | (cp & 0x3)]; + cp >>>= 2; + dest[2] = UPPER_HEX_DIGITS[cp & 0x7]; + data.addSize(4); + return dest; + } else { + // If this ever happens it is due to bug in UnicodeEscaper, not bad input. + throw new IllegalArgumentException("Invalid unicode character value " + cp); + } + } + + /** + * Returns the Unicode code point of the character at the given index. + * + *

Unlike {@link Character#codePointAt(CharSequence, int)} or {@link String#codePointAt(int)} + * this method will never fail silently when encountering an invalid surrogate pair. + * + *

The behaviour of this method is as follows: + * + *

    + *
  1. If {@code index >= end}, {@link IndexOutOfBoundsException} is thrown. + *
  2. If the character at the specified index is not a surrogate, it is returned. + *
  3. If the first character was a high surrogate value, then an attempt is made to read the + * next character. + *
      + *
    1. If the end of the sequence was reached, the negated value of the trailing high + * surrogate is returned. + *
    2. If the next character was a valid low surrogate, the code point value of the + * high/low surrogate pair is returned. + *
    3. If the next character was not a low surrogate value, then {@link + * IllegalArgumentException} is thrown. + *
    + *
  4. If the first character was a low surrogate value, {@link IllegalArgumentException} is + * thrown. + *
+ * + * @param seq the sequence of characters from which to decode the code point + * @param index the index of the first character to decode + * @param end the index beyond the last valid character to decode + * @return the Unicode code point for the given index or the negated value of the trailing high + * surrogate character at the end of the sequence + */ + private static int codePointAt(CharSequence seq, int index, int end) { + if (index < end) { + char c1 = seq.charAt(index++); + if (c1 < Character.MIN_HIGH_SURROGATE || c1 > Character.MAX_LOW_SURROGATE) { + // Fast path (first test is probably all we need to do) + return c1; + } else if (c1 <= Character.MAX_HIGH_SURROGATE) { + // If the high surrogate was the last character, return its inverse + if (index == end) { + return -c1; + } + // Otherwise look for the low surrogate following it + char c2 = seq.charAt(index); + if (Character.isLowSurrogate(c2)) { + return Character.toCodePoint(c1, c2); + } + throw new IllegalArgumentException( + "Expected low surrogate but got char '" + + c2 + + "' with value " + + (int) c2 + + " at index " + + index + + " in '" + + seq + + "'"); + } else { + throw new IllegalArgumentException( + "Unexpected low surrogate character '" + + c1 + + "' with value " + + (int) c1 + + " at index " + + (index - 1) + + " in '" + + seq + + "'"); + } + } + throw new IndexOutOfBoundsException("Index exceeds specified range"); + } + + /** + * Helper method to grow the character buffer as needed, this only happens once in a while so it's + * ok if it's in a method call. If the index passed in is 0 then no copying will be done. + */ + private static char[] growBuffer(char[] dest, int index, int size) { + if (size < 0) { // overflow - should be OutOfMemoryError but GWT/j2cl don't support it + throw new AssertionError("Cannot increase internal buffer any further"); + } + char[] copy = new char[size]; + if (index > 0) { + System.arraycopy(dest, 0, copy, 0, index); + } + return copy; + } +} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/baggage/BaggagePropagatorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/baggage/BaggagePropagatorTest.groovy new file mode 100644 index 00000000000..56a16cc376f --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/baggage/BaggagePropagatorTest.groovy @@ -0,0 +1,167 @@ +package datadog.trace.core.baggage + +import datadog.context.Context +import datadog.context.propagation.CarrierSetter +import datadog.context.propagation.CarrierVisitor +import datadog.trace.bootstrap.instrumentation.api.BaggageContext +import datadog.trace.bootstrap.instrumentation.api.ContextVisitors +import datadog.trace.core.test.DDCoreSpecification + +import java.util.function.BiConsumer + +import static datadog.trace.core.baggage.BaggagePropagator.BAGGAGE_KEY + +class BaggagePropagatorTest extends DDCoreSpecification { + BaggagePropagator propagator + CarrierSetter setter + Map carrier + Context context + + + static class MapCarrierAccessor + implements CarrierSetter>, CarrierVisitor> { + @Override + void set(Map carrier, String key, String value) { + if (carrier != null && key != null && value != null) { + carrier.put(key, value) + } + } + + @Override + void forEachKeyValue(Map carrier, BiConsumer visitor) { + carrier.forEach(visitor) + } + } + + def setup() { + this.propagator = new BaggagePropagator(true, true) + setter = new MapCarrierAccessor() + carrier = [:] + context = Context.root() + } + + def 'test baggage propagator context injection'() { + setup: + context = BaggageContext.create(baggageMap).storeInto(context) + + when: + this.propagator.inject(context, carrier, setter) + + then: + assert carrier[BAGGAGE_KEY] == baggageHeader + + where: + baggageMap | baggageHeader + ["key1": "val1", "key2": "val2", "foo": "bar"] | "key1=val1,key2=val2,foo=bar" + ['",;\\()/:<=>?@[]{}': '",;\\'] | "%22%2C%3B%5C%28%29%2F%3A%3C%3D%3E%3F%40%5B%5D%7B%7D=%22%2C%3B%5C" + [key1: "val1"] | "key1=val1" + [key1: "val1", key2: "val2"] | "key1=val1,key2=val2" + [serverNode: "DF 28"] | "serverNode=DF%2028" + [userId: "Amélie"] | "userId=Am%C3%A9lie" + ["user!d(me)": "false"] | "user!d%28me%29=false" + ["abcdefg": "hijklmnopq♥"] | "abcdefg=hijklmnopq%E2%99%A5" + } + + def "test baggage item limit"() { + setup: + injectSysConfig("trace.baggage.max.items", '2') + propagator = new BaggagePropagator(true, true) //creating a new instance after injecting config + context = BaggageContext.create(baggage).storeInto(context) + + when: + this.propagator.inject(context, carrier, setter) + + then: + assert carrier[BAGGAGE_KEY] == baggageHeader + + where: + baggage | baggageHeader + [key1: "val1", key2: "val2"] | "key1=val1,key2=val2" + [key1: "val1", key2: "val2", key3: "val3"] | "key1=val1,key2=val2" + } + + def "test baggage bytes limit"() { + setup: + injectSysConfig("trace.baggage.max.bytes", '20') + propagator = new BaggagePropagator(true, true) //creating a new instance after injecting config + context = BaggageContext.create(baggage).storeInto(context) + + when: + this.propagator.inject(context, carrier, setter) + + then: + assert carrier[BAGGAGE_KEY] == baggageHeader + + where: + baggage | baggageHeader + [key1: "val1", key2: "val2"] | "key1=val1,key2=val2" + [key1: "val1", key2: "val2", key3: "val3"] | "key1=val1,key2=val2" + ["abcdefg": "hijklmnopq♥"] | "" + } + + def 'test tracing propagator context extractor'() { + setup: + def headers = [ + (BAGGAGE_KEY) : baggageHeader, + ] + + when: + context = this.propagator.extract(context, headers, ContextVisitors.stringValuesMap()) + + then: + BaggageContext.fromContext(context).asMap() == baggageMap + + where: + baggageHeader | baggageMap + "key1=val1,key2=val2,foo=bar" | ["key1": "val1", "key2": "val2", "foo": "bar"] + "%22%2C%3B%5C%28%29%2F%3A%3C%3D%3E%3F%40%5B%5D%7B%7D=%22%2C%3B%5C" | ['",;\\()/:<=>?@[]{}': '",;\\'] + } + + def "extract invalid baggage headers"() { + setup: + def headers = [ + (BAGGAGE_KEY) : baggageHeader, + ] + + when: + context = this.propagator.extract(context, headers, ContextVisitors.stringValuesMap()) + + then: + BaggageContext.fromContext(context) == null + + where: + baggageHeader | _ + "no-equal-sign,foo=gets-dropped-because-previous-pair-is-malformed" | _ + "foo=gets-dropped-because-subsequent-pair-is-malformed,=" | _ + "=no-key" | _ + "no-value=" | _ + "" | _ + ",," | _ + "=" | _ + } + + def "testing baggage cache"(){ + setup: + def headers = [ + (BAGGAGE_KEY) : baggageHeader, + ] + + when: + context = this.propagator.extract(context, headers, ContextVisitors.stringValuesMap()) + + then: + BaggageContext baggageContext = BaggageContext.fromContext(context) + baggageContext.asMap() == baggageMap + + when: + this.propagator.inject(context, carrier, setter) + + then: + assert carrier[BAGGAGE_KEY] == baggageHeader + + where: + baggageHeader | baggageMap + "key1=val1,key2=val2,foo=bar" | ["key1": "val1", "key2": "val2", "foo": "bar"] + "%22%2C%3B%5C%28%29%2F%3A%3C%3D%3E%3F%40%5B%5D%7B%7D=%22%2C%3B%5C" | ['",;\\()/:<=>?@[]{}': '",;\\'] + } +} diff --git a/internal-api/build.gradle b/internal-api/build.gradle index 60b3581db97..5dbe7d16de6 100644 --- a/internal-api/build.gradle +++ b/internal-api/build.gradle @@ -181,6 +181,8 @@ excludedClassesCoverage += [ "datadog.trace.api.iast.Taintable", "datadog.trace.api.Stateful", "datadog.trace.api.Stateful.1", + // BaggageContext class tested in BaggagePropagatorTest in dd-trace-core + 'datadog.trace.bootstrap.instrumentation.api.BaggageContext', // a stub "datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration", "datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration.NoOp", diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index 004a609898e..827811a9811 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -190,6 +190,8 @@ public static String getHostName() { private final Set tracePropagationStylesToExtract; private final Set tracePropagationStylesToInject; private final boolean tracePropagationExtractFirst; + private final int traceBaggageMaxItems; + private final int traceBaggageMaxBytes; private final int clockSyncPeriod; private final boolean logsInjectionEnabled; @@ -992,6 +994,12 @@ private Config(final ConfigProvider configProvider, final InstrumenterConfig ins tracePropagationStylesToExtract = extract.isEmpty() ? DEFAULT_TRACE_PROPAGATION_STYLE : extract; tracePropagationStylesToInject = inject.isEmpty() ? DEFAULT_TRACE_PROPAGATION_STYLE : inject; + + traceBaggageMaxItems = + configProvider.getInteger(TRACE_BAGGAGE_MAX_ITEMS, DEFAULT_TRACE_BAGGAGE_MAX_ITEMS); + traceBaggageMaxBytes = + configProvider.getInteger(TRACE_BAGGAGE_MAX_BYTES, DEFAULT_TRACE_BAGGAGE_MAX_BYTES); + // These setting are here for backwards compatibility until they can be removed in a major // release of the tracer propagationStylesToExtract = @@ -2252,6 +2260,26 @@ public boolean isTracePropagationExtractFirst() { return tracePropagationExtractFirst; } + public boolean isBaggageExtract() { + return tracePropagationStylesToExtract.contains(TracePropagationStyle.BAGGAGE); + } + + public boolean isBaggageInject() { + return tracePropagationStylesToInject.contains(TracePropagationStyle.BAGGAGE); + } + + public boolean isBaggagePropagationEnabled() { + return isBaggageInject() || isBaggageExtract(); + } + + public int getTraceBaggageMaxItems() { + return traceBaggageMaxItems; + } + + public int getTraceBaggageMaxBytes() { + return traceBaggageMaxBytes; + } + public int getClockSyncPeriod() { return clockSyncPeriod; } diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentPropagation.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentPropagation.java index e138d72d042..a25c0abfee5 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentPropagation.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentPropagation.java @@ -12,7 +12,9 @@ public final class AgentPropagation { public static final Concern TRACING_CONCERN = named("tracing"); + public static final Concern BAGGAGE_CONCERN = named("baggage"); public static final Concern XRAY_TRACING_CONCERN = named("tracing-xray"); + // TODO DSM propagator should run after the other propagators as it stores the pathway context // TODO into the span context for now. Remove priority after the migration is complete. public static final Concern DSM_CONCERN = withPriority("data-stream-monitoring", 110); diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/BaggageContext.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/BaggageContext.java new file mode 100644 index 00000000000..99048f7a107 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/BaggageContext.java @@ -0,0 +1,74 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import datadog.context.Context; +import datadog.context.ContextKey; +import datadog.context.ImplicitContextKeyed; +import java.util.HashMap; +import java.util.Map; + +public class BaggageContext implements ImplicitContextKeyed { + private static final ContextKey CONTEXT_KEY = ContextKey.named("baggage-key"); + + private final Map baggage; + private String baggageString; + private boolean updatedCache; + + public BaggageContext empty() { + return create(new HashMap<>(), ""); + } + + public static BaggageContext create(Map baggage) { + return new BaggageContext(baggage); + } + + private BaggageContext(Map baggage) { + this.baggage = baggage; + this.baggageString = ""; + updatedCache = false; + } + + public static BaggageContext create(Map baggage, String w3cHeader) { + return new BaggageContext(baggage, w3cHeader); + } + + private BaggageContext(Map baggage, String baggageString) { + this.baggage = baggage; + this.baggageString = baggageString; + updatedCache = true; + } + + public void addW3CBaggage(String key, String value) { + baggage.put(key, value); + updatedCache = false; + } + + public void removeW3CBaggage(String key) { + baggage.remove(key); + updatedCache = false; + } + + public void setW3cBaggageHeader(String w3cHeader) { + this.baggageString = w3cHeader; + updatedCache = true; + } + + public String getW3cBaggageHeader() { + if (updatedCache) { + return baggageString; + } + return null; + } + + public Map asMap() { + return new HashMap<>(baggage); + } + + public static BaggageContext fromContext(Context context) { + return context.get(CONTEXT_KEY); + } + + @Override + public Context storeInto(Context context) { + return context.with(CONTEXT_KEY, this); + } +} diff --git a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy index b3e2a3d9b09..df5509b13ef 100644 --- a/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy +++ b/internal-api/src/test/groovy/datadog/trace/api/ConfigTest.groovy @@ -26,6 +26,7 @@ import static datadog.trace.api.TracePropagationStyle.B3SINGLE import static datadog.trace.api.TracePropagationStyle.DATADOG import static datadog.trace.api.TracePropagationStyle.HAYSTACK import static datadog.trace.api.TracePropagationStyle.TRACECONTEXT +import static datadog.trace.api.TracePropagationStyle.BAGGAGE import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_AGENTLESS_ENABLED import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_ENABLED import static datadog.trace.api.config.DebuggerConfig.DYNAMIC_INSTRUMENTATION_CLASSFILE_DUMP_ENABLED @@ -645,8 +646,8 @@ class ConfigTest extends DDSpecification { config.splitByTags == [].toSet() config.propagationStylesToExtract.toList() == [PropagationStyle.DATADOG] config.propagationStylesToInject.toList() == [PropagationStyle.DATADOG] - config.tracePropagationStylesToExtract.toList() == [DATADOG, TRACECONTEXT] - config.tracePropagationStylesToInject.toList() == [DATADOG, TRACECONTEXT] + config.tracePropagationStylesToExtract.toList() == [DATADOG, TRACECONTEXT, BAGGAGE] + config.tracePropagationStylesToInject.toList() == [DATADOG, TRACECONTEXT, BAGGAGE] config.longRunningTraceEnabled == false } @@ -2403,21 +2404,21 @@ class ConfigTest extends DDSpecification { where: // spotless:off - pse | psi | tps | tpse | tpsi | ePSE | ePSI | eTPSE | eTPSI - PropagationStyle.DATADOG | PropagationStyle.B3 | null | null | null | [PropagationStyle.DATADOG] | [PropagationStyle.B3] | [DATADOG] | [B3SINGLE, B3MULTI] - PropagationStyle.B3 | PropagationStyle.DATADOG | null | null | null | [PropagationStyle.B3] | [PropagationStyle.DATADOG] | [B3SINGLE, B3MULTI] | [DATADOG] - PropagationStyle.B3 | PropagationStyle.DATADOG | HAYSTACK | null | null | [PropagationStyle.B3] | [PropagationStyle.DATADOG] | [HAYSTACK] | [HAYSTACK] - PropagationStyle.B3 | PropagationStyle.DATADOG | HAYSTACK | B3SINGLE | null | [PropagationStyle.B3] | [PropagationStyle.DATADOG] | [B3SINGLE] | [HAYSTACK] - PropagationStyle.B3 | PropagationStyle.DATADOG | HAYSTACK | null | B3MULTI | [PropagationStyle.B3] | [PropagationStyle.DATADOG] | [HAYSTACK] | [B3MULTI] - PropagationStyle.B3 | PropagationStyle.DATADOG | HAYSTACK | B3SINGLE | B3MULTI | [PropagationStyle.B3] | [PropagationStyle.DATADOG] | [B3SINGLE] | [B3MULTI] - PropagationStyle.B3 | PropagationStyle.DATADOG | null | B3SINGLE | B3MULTI | [PropagationStyle.B3] | [PropagationStyle.DATADOG] | [B3SINGLE] | [B3MULTI] - null | null | HAYSTACK | null | null | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [HAYSTACK] | [HAYSTACK] - null | null | HAYSTACK | B3SINGLE | B3MULTI | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [B3SINGLE] | [B3MULTI] - null | null | null | B3SINGLE | B3MULTI | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [B3SINGLE] | [B3MULTI] - null | null | null | null | null | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [DATADOG, TRACECONTEXT] | [DATADOG, TRACECONTEXT] - null | null | null | null | null | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [DATADOG, TRACECONTEXT] | [DATADOG, TRACECONTEXT] - null | null | null | "b3 single header" | null | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [B3SINGLE] | [DATADOG, TRACECONTEXT] - null | null | null | "b3" | null | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [B3MULTI] | [DATADOG, TRACECONTEXT] + pse | psi | tps | tpse | tpsi | ePSE | ePSI | eTPSE | eTPSI + PropagationStyle.DATADOG | PropagationStyle.B3 | null | null | null | [PropagationStyle.DATADOG] | [PropagationStyle.B3] | [DATADOG] | [B3SINGLE, B3MULTI] + PropagationStyle.B3 | PropagationStyle.DATADOG | null | null | null | [PropagationStyle.B3] | [PropagationStyle.DATADOG] | [B3SINGLE, B3MULTI] | [DATADOG] + PropagationStyle.B3 | PropagationStyle.DATADOG | HAYSTACK | null | null | [PropagationStyle.B3] | [PropagationStyle.DATADOG] | [HAYSTACK] | [HAYSTACK] + PropagationStyle.B3 | PropagationStyle.DATADOG | HAYSTACK | B3SINGLE | null | [PropagationStyle.B3] | [PropagationStyle.DATADOG] | [B3SINGLE] | [HAYSTACK] + PropagationStyle.B3 | PropagationStyle.DATADOG | HAYSTACK | null | B3MULTI | [PropagationStyle.B3] | [PropagationStyle.DATADOG] | [HAYSTACK] | [B3MULTI] + PropagationStyle.B3 | PropagationStyle.DATADOG | HAYSTACK | B3SINGLE | B3MULTI | [PropagationStyle.B3] | [PropagationStyle.DATADOG] | [B3SINGLE] | [B3MULTI] + PropagationStyle.B3 | PropagationStyle.DATADOG | null | B3SINGLE | B3MULTI | [PropagationStyle.B3] | [PropagationStyle.DATADOG] | [B3SINGLE] | [B3MULTI] + null | null | HAYSTACK | null | null | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [HAYSTACK] | [HAYSTACK] + null | null | HAYSTACK | B3SINGLE | B3MULTI | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [B3SINGLE] | [B3MULTI] + null | null | null | B3SINGLE | B3MULTI | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [B3SINGLE] | [B3MULTI] + null | null | null | null | null | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [DATADOG, TRACECONTEXT, BAGGAGE] | [DATADOG, TRACECONTEXT, BAGGAGE] + null | null | null | null | null | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [DATADOG, TRACECONTEXT, BAGGAGE] | [DATADOG, TRACECONTEXT, BAGGAGE] + null | null | null | "b3 single header" | null | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [B3SINGLE] | [DATADOG, TRACECONTEXT, BAGGAGE] + null | null | null | "b3" | null | [PropagationStyle.DATADOG] | [PropagationStyle.DATADOG] | [B3MULTI] | [DATADOG, TRACECONTEXT, BAGGAGE] // spotless:on }