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 af1be8804ee..26678aca032 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..2e9e1cf3b79 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, + // OTEL 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 a09c39c49e4..326e11dfb71 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.TRACING_CONCERN; import static datadog.trace.common.metrics.MetricsAggregatorFactory.createMetricsAggregator; import static datadog.trace.util.AgentThreadFactory.AGENT_THREAD_GROUP; @@ -76,6 +77,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.DataStreamContextInjector; import datadog.trace.core.datastreams.DataStreamsMonitoring; import datadog.trace.core.datastreams.DefaultDataStreamsMonitoring; @@ -723,6 +725,7 @@ private CoreTracer( new CorePropagation(builtExtractor, injector, injectors, dataStreamContextInjector); Propagators.register(TRACING_CONCERN, new TracingPropagator(injector, extractor)); + Propagators.register(BAGGAGE_CONCERN, new BaggagePropagator()); this.tagInterceptor = null == tagInterceptor ? new TagInterceptor(new RuleFlags(config)) : tagInterceptor; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/baggage/BaggageHttpCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/baggage/BaggageHttpCodec.java new file mode 100644 index 00000000000..402da477aaa --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/baggage/BaggageHttpCodec.java @@ -0,0 +1,194 @@ +// package datadog.trace.core.baggage; +// +// import static datadog.trace.api.TracePropagationStyle.BAGGAGE; +// +// import datadog.trace.api.Config; +// import datadog.trace.api.TraceConfig; +// import datadog.trace.api.TracePropagationStyle; +// import datadog.trace.bootstrap.instrumentation.api.AgentPropagation; +// import datadog.trace.bootstrap.instrumentation.api.TagContext; +// import datadog.trace.core.DDSpanContext; +// import java.nio.charset.StandardCharsets; +// import java.util.Arrays; +// import java.util.Collections; +// import java.util.HashMap; +// import java.util.HashSet; +// import java.util.Map; +// import java.util.Set; +// import java.util.function.Supplier; +// +// import datadog.trace.core.propagation.ContextInterpreter; +// import datadog.trace.core.propagation.HttpCodec; +// import datadog.trace.core.propagation.TagContextExtractor; +// import org.slf4j.Logger; +// import org.slf4j.LoggerFactory; +// +/// ** A codec designed for HTTP transport via headers using Datadog headers */ +// class BaggageHttpCodec { +// private static final Logger log = LoggerFactory.getLogger(BaggageHttpCodec.class); +// +// static final String BAGGAGE_KEY = "baggage"; +// private static final int MAX_CHARACTER_SIZE = 4; +// +// private static final Set UNSAFE_CHARACTERS_KEY = +// new HashSet<>( +// Arrays.asList( +// '\"', ',', ';', '\\', '(', ')', '/', ':', '<', '=', '>', '?', '@', '[', ']', '{', +// '}')); +// private static final Set UNSAFE_CHARACTERS_VALUE = +// new HashSet<>(Arrays.asList('\"', ',', ';', '\\')); +// +// private BaggageHttpCodec() { +// // This class should not be created. This also makes code coverage checks happy. +// } +// +// public static HttpCodec.Injector newInjector(Map invertedBaggageMapping) { +// return new Injector(invertedBaggageMapping); +// } +// +// private static class Injector implements HttpCodec.Injector { +// +// private final Map invertedBaggageMapping; +// +// public Injector(Map invertedBaggageMapping) { +// assert invertedBaggageMapping != null; +// this.invertedBaggageMapping = invertedBaggageMapping; +// } +// +// private int encodeKey(String key, StringBuilder builder) { +// return encode(key, builder, UNSAFE_CHARACTERS_KEY); +// } +// +// private int encodeValue(String key, StringBuilder builder) { +// return encode(key, builder, UNSAFE_CHARACTERS_VALUE); +// } +// +// private int encode(String input, StringBuilder builder, Set UNSAFE_CHARACTERS) { +// int size = 0; +// for (int i = 0; i < input.length(); i++) { +// char c = input.charAt(i); +// if (UNSAFE_CHARACTERS.contains(c) || c > 126 || c <= 32) { // encode character +// byte[] bytes = +// Character.toString(c) +// .getBytes(StandardCharsets.UTF_8); // not most efficient but what URLEncoder +// does +// for (byte b : bytes) { +// builder.append(String.format("%%%02X", b & 0xFF)); +// size += 1; +// } +// } else { +// builder.append(c); +// size += 1; +// } +// } +// return size; +// } +// +// @Override +// public void inject( +// final DDSpanContext context, final C carrier, final AgentPropagation.Setter setter) { +// Config config = Config.get(); +// +// StringBuilder baggageText = new StringBuilder(); +// int processedBaggage = 0; +// int currentBytes = 0; +// int maxItems = config.getTraceBaggageMaxItems(); +// int maxBytes = config.getTraceBaggageMaxBytes(); +// for (final Map.Entry entry : context.baggageItems()) { +// if (processedBaggage >= maxItems) { +// break; +// } +// int additionalCharacters = 1; // accounting for potential comma and colon +// if (processedBaggage != 0) { +// additionalCharacters = 2; // allocating space for comma +// } +// +// int byteSize = 1; // default include size of '=' +// if (additionalCharacters == 2) { +// baggageText.append(','); +// byteSize += 1; +// } +// +// byteSize += encodeKey(entry.getKey(), baggageText); +// baggageText.append('='); +// byteSize += encodeValue(entry.getValue(), baggageText); +// +// if (currentBytes + byteSize > maxBytes) { +// baggageText.setLength(currentBytes); +// break; +// } +// currentBytes += byteSize; +// processedBaggage++; +// } +// +// setter.set(carrier, BAGGAGE_KEY, baggageText.toString()); +// } +// } +// +// public static HttpCodec.Extractor newExtractor( +// Config config, Supplier traceConfigSupplier) { +// return new TagContextExtractor( +// traceConfigSupplier, () -> new BaggageContextInterpreter(config)); +// } +// +// private static class BaggageContextInterpreter extends ContextInterpreter { +// +// private BaggageContextInterpreter(Config config) { +// super(config); +// } +// +// @Override +// public TracePropagationStyle style() { +// return BAGGAGE; +// } +// +// 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) { // value is missing +// return Collections.emptyMap(); +// } +// String key = HttpCodec.decode(input.substring(start, kvSeparatorInd).trim()); +// String value = HttpCodec.decode(input.substring(kvSeparatorInd + 1, end).trim()); +// if (key.isEmpty() || value.isEmpty()) { +// 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 boolean accept(String key, String value) { +// if (null == key || key.isEmpty()) { +// return true; +// } +// if (LOG_EXTRACT_HEADER_NAMES) { +// log.debug("Header: {}", key); +// } +// +// if (key.equalsIgnoreCase(BAGGAGE_KEY)) { // Only process tags that are relevant to baggage +// baggage = parseBaggageHeaders(value); +// } +// return true; +// } +// +// @Override +// protected TagContext build() { +// return super.build(); +// } +// } +// } 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..147c4031125 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/baggage/BaggagePropagator.java @@ -0,0 +1,178 @@ +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 java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import javax.annotation.ParametersAreNonnullByDefault; + +/** Propagator for tracing concern. */ +@ParametersAreNonnullByDefault +public class BaggagePropagator implements Propagator { + static final String BAGGAGE_KEY = "baggage"; + + private static final Set UNSAFE_CHARACTERS_KEY = + new HashSet<>( + Arrays.asList( + '\"', ',', ';', '\\', '(', ')', '/', ':', '<', '=', '>', '?', '@', '[', ']', '{', + '}')); + private static final Set UNSAFE_CHARACTERS_VALUE = + new HashSet<>(Arrays.asList('\"', ',', ';', '\\')); + + public BaggagePropagator() {} + + private int encodeKey(String key, StringBuilder builder) { + return encode(key, builder, UNSAFE_CHARACTERS_KEY); + } + + private int encodeValue(String key, StringBuilder builder) { + return encode(key, builder, UNSAFE_CHARACTERS_VALUE); + } + + private int encode(String input, StringBuilder builder, Set UNSAFE_CHARACTERS) { + int size = 0; + for (int i = 0; i < input.length(); i++) { + char c = input.charAt(i); + if (UNSAFE_CHARACTERS.contains(c) || c > 126 || c <= 32) { // encode character + byte[] bytes = + Character.toString(c) + .getBytes(StandardCharsets.UTF_8); // not most efficient but what URLEncoder does + for (byte b : bytes) { + builder.append(String.format("%%%02X", b & 0xFF)); + size += 1; + } + } else { + builder.append(c); + size += 1; + } + } + return size; + } + + @Override + public void inject(Context context, C carrier, CarrierSetter setter) { + Config config = Config.get(); + + StringBuilder baggageText = new StringBuilder(); + int processedBaggage = 0; + int currentBytes = 0; + int maxItems = config.getTraceBaggageMaxItems(); + int maxBytes = config.getTraceBaggageMaxBytes(); + BaggageContext baggageContext = BaggageContext.fromContext(context); + for (final Map.Entry entry : baggageContext.getBaggage().entrySet()) { + if (processedBaggage >= maxItems) { + break; + } + int additionalCharacters = 1; // accounting for potential comma and colon + if (processedBaggage != 0) { + additionalCharacters = 2; // allocating space for comma + } + + int byteSize = 1; // default include size of '=' + if (additionalCharacters == 2) { + baggageText.append(','); + byteSize += 1; + } + + byteSize += encodeKey(entry.getKey(), baggageText); + baggageText.append('='); + byteSize += encodeValue(entry.getValue(), baggageText); + + if (currentBytes + byteSize > maxBytes) { + baggageText.setLength(currentBytes); + break; + } + currentBytes += byteSize; + processedBaggage++; + } + + setter.set(carrier, BAGGAGE_KEY, baggageText.toString()); + } + + @Override + public Context extract(Context context, C carrier, CarrierVisitor visitor) { + //noinspection ConstantValue + if (context == null || carrier == null || visitor == null) { + return context; + } + BaggageContextExtractor baggageContextExtractor = new BaggageContextExtractor(); + visitor.forEachKeyValue( + carrier, baggageContextExtractor); // If the extraction fails, return the original context + BaggageContext extractedContext = baggageContextExtractor.extractedContext; + if (extractedContext == null) { + return context; + } + return extractedContext.storeInto(context); // does this make sense? + } + + 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) { + } + 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) { // value is missing + 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()) { + 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) { + if (null == key || key.isEmpty()) { + return; + } + if (key.equalsIgnoreCase(BAGGAGE_KEY)) { // Only process tags that are relevant to baggage + extractedContext = + BaggageContext.create( + parseBaggageHeaders( + value)); // is this correct to assume that one instance will be created for each + // propagator? + } + } + } +} 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..808837bcfca --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/baggage/BaggagePropagatorTest.groovy @@ -0,0 +1,152 @@ +package datadog.trace.core.baggage + +import datadog.context.Context +import datadog.context.propagation.CarrierSetter +import datadog.context.propagation.Propagators +import datadog.trace.api.sampling.PrioritySampling +import datadog.trace.bootstrap.instrumentation.api.AgentPropagation +import datadog.trace.common.writer.LoggingWriter +import datadog.trace.core.ControllableSampler +import datadog.trace.core.test.DDCoreSpecification + +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP +import static datadog.trace.api.sampling.PrioritySampling.USER_DROP +import static datadog.trace.core.baggage.BaggagePropagator.BAGGAGE_KEY + +class BaggagePropagatorTest extends DDCoreSpecification { + BaggagePropagator propagator + + def setup() { + this.propagator = new BaggagePropagator() + } + + def 'test tracing propagator context injection'() { + setup: + def tracer = tracerBuilder().build() + def span = tracer.buildSpan('test', 'operation').start() + def setter = Mock(CarrierSetter) + def carrier = new Object() + + when: + this.propagator.inject(span, carrier, setter) + + then: + 1 * injector.inject(span.context(), carrier, _) + + cleanup: + span.finish() + tracer.close() + } + + def 'test tracing propagator context extractor'() { + setup: + def context = Context.root() + // TODO Use ContextVisitor mock as getter once extractor API is refactored + def getter = Mock(AgentPropagation.ContextVisitor) + def headers = [ + (BAGGAGE_KEY) : baggageHeader, + ] + // BaggagePropagator.BaggageContextExtractor baggageContextExtractor = new BaggagePropagator.BaggageContextExtractor() + + + when: + this.propagator.extract(context, headers, getter) + // getter.forEachKeyValue(headers, baggageContextExtractor) + + then: + 1*_ + 1*_ + 1*_ + 1*_ + 1*_ + + where: + baggageHeader | baggageMap + "key1=val1,key2=val2,foo=bar,x=y" | ["key1": "val1", "key2": "val2", "foo": "bar", "x": "y"] + "%22%2C%3B%5C%28%29%2F%3A%3C%3D%3E%3F%40%5B%5D%7B%7D=%22%2C%3B%5C" | ['",;\\()/:<=>?@[]{}': '",;\\'] + + // 1 * extractor.extract(carrier, _) + } + + def 'span priority set when injecting'() { + given: + injectSysConfig('writer.type', 'LoggingWriter') + def tracer = tracerBuilder().build() + def setter = Mock(CarrierSetter) + def carrier = new Object() + + when: + def root = tracer.buildSpan('test', 'parent').start() + def child = tracer.buildSpan('test', 'child').asChildOf(root).start() + Propagators.defaultPropagator().inject(child, carrier, setter) + + then: + root.getSamplingPriority() == SAMPLER_KEEP as int + child.getSamplingPriority() == root.getSamplingPriority() + 1 * setter.set(carrier, DatadogHttpCodec.SAMPLING_PRIORITY_KEY, String.valueOf(SAMPLER_KEEP)) + + cleanup: + child.finish() + root.finish() + tracer.close() + } + + def 'span priority only set after first injection'() { + given: + def sampler = new ControllableSampler() + def tracer = tracerBuilder().writer(new LoggingWriter()).sampler(sampler).build() + def setter = Mock(AgentPropagation.Setter) + def carrier = new Object() + + when: + def root = tracer.buildSpan('test', 'parent').start() + def child = tracer.buildSpan('test', 'child').asChildOf(root).start() + Propagators.defaultPropagator().inject(child, carrier, setter) + + then: + root.getSamplingPriority() == SAMPLER_KEEP as int + child.getSamplingPriority() == root.getSamplingPriority() + 1 * setter.set(carrier, DatadogHttpCodec.SAMPLING_PRIORITY_KEY, String.valueOf(SAMPLER_KEEP)) + + when: + sampler.nextSamplingPriority = PrioritySampling.SAMPLER_DROP as int + def child2 = tracer.buildSpan('test', 'child2').asChildOf(root).start() + Propagators.defaultPropagator().inject(child2, carrier, setter) + + then: + root.getSamplingPriority() == SAMPLER_KEEP as int + child.getSamplingPriority() == root.getSamplingPriority() + child2.getSamplingPriority() == root.getSamplingPriority() + 1 * setter.set(carrier, DatadogHttpCodec.SAMPLING_PRIORITY_KEY, String.valueOf(SAMPLER_KEEP)) + + cleanup: + child.finish() + child2.finish() + root.finish() + tracer.close() + } + + def 'injection does not override set priority'() { + given: + def sampler = new ControllableSampler() + def tracer = tracerBuilder().writer(new LoggingWriter()).sampler(sampler).build() + def setter = Mock(AgentPropagation.Setter) + def carrier = new Object() + + when: + def root = tracer.buildSpan('test', 'root').start() + def child = tracer.buildSpan('test', 'child').asChildOf(root).start() + child.setSamplingPriority(USER_DROP) + Propagators.defaultPropagator().inject(child, carrier, setter) + + then: + root.getSamplingPriority() == USER_DROP as int + child.getSamplingPriority() == root.getSamplingPriority() + 1 * setter.set(carrier, DatadogHttpCodec.SAMPLING_PRIORITY_KEY, String.valueOf(USER_DROP)) + + cleanup: + child.finish() + root.finish() + tracer.close() + } +} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/propagation/BaggageHttpExtractorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/propagation/BaggageHttpExtractorTest.groovy new file mode 100644 index 00000000000..7ecddc426ce --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/propagation/BaggageHttpExtractorTest.groovy @@ -0,0 +1,78 @@ +package datadog.trace.core.propagation + +import datadog.trace.api.Config +import datadog.trace.api.DynamicConfig +import datadog.trace.bootstrap.instrumentation.api.TagContext +import datadog.trace.bootstrap.instrumentation.api.ContextVisitors +//import datadog.trace.core.baggage.BaggageHttpCodec +import datadog.trace.test.util.DDSpecification +//import static datadog.trace.core.baggage.BaggageHttpCodec.BAGGAGE_KEY + +class BaggageHttpExtractorTest extends DDSpecification { + + private DynamicConfig dynamicConfig + private HttpCodec.Extractor _extractor + + private HttpCodec.Extractor getExtractor() { + _extractor ?: (_extractor = createExtractor(Config.get())) + } + + private HttpCodec.Extractor createExtractor(Config config) { + BaggageHttpCodec.newExtractor(config, { dynamicConfig.captureTraceConfig() }) + } + + void setup() { + dynamicConfig = DynamicConfig.create() + .apply() + } + + void cleanup() { + extractor.cleanup() + } + + def "extract valid baggage headers"() { + setup: + def extractor = createExtractor(Config.get()) + def headers = [ + (BAGGAGE_KEY) : baggageHeader, + ] + + when: + final TagContext context = extractor.extract(headers, ContextVisitors.stringValuesMap()) + + then: + context.baggage == baggageMap + + cleanup: + extractor.cleanup() + + where: + baggageHeader | baggageMap + "key1=val1,key2=val2,foo=bar,x=y" | ["key1": "val1", "key2": "val2", "foo": "bar", "x": "y"] + "%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 extractor = createExtractor(Config.get()) + def headers = [ + (BAGGAGE_KEY) : baggageHeader, + ] + + when: + final TagContext context = extractor.extract(headers, ContextVisitors.stringValuesMap()) + + then: + context == null + + cleanup: + extractor.cleanup() + + where: + baggageHeader | baggageMap + "no-equal-sign,foo=gets-dropped-because-previous-pair-is-malformed" | [] + "foo=gets-dropped-because-subsequent-pair-is-malformed,=" | [] + "=no-key" | [] + "no-value=" | [] + } +} diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/propagation/BaggageHttpInjectorTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/propagation/BaggageHttpInjectorTest.groovy new file mode 100644 index 00000000000..34b56a917df --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/propagation/BaggageHttpInjectorTest.groovy @@ -0,0 +1,155 @@ +package datadog.trace.core.propagation + +import datadog.trace.api.DDSpanId +import datadog.trace.api.DDTraceId +import datadog.trace.bootstrap.instrumentation.api.AgentTracer.NoopPathwayContext +import datadog.trace.common.writer.ListWriter +import datadog.trace.core.CoreTracer +import datadog.trace.core.DDSpanContext +import datadog.trace.core.test.DDCoreSpecification + + +import static datadog.trace.api.sampling.PrioritySampling.* +//import static datadog.trace.core.baggage.BaggageHttpCodec.* + + +class BaggageHttpInjectorTest extends DDCoreSpecification { + + private HttpCodec.Injector injector = newInjector(["some-baggage-key":"SOME_CUSTOM_HEADER"]) + private ListWriter writer + private CoreTracer tracer + private Map carrier + + void setup() { + writer = new ListWriter() + tracer = tracerBuilder().writer(writer).build() + carrier = Mock() + } + + def "test baggage injection and encoding"() { + setup: + final DDSpanContext mockedContext = + new DDSpanContext( + DDTraceId.from("1"), + DDSpanId.from("2"), + DDSpanId.ZERO, + null, + "fakeService", + "fakeOperation", + "fakeResource", + UNSET, + "fakeOrigin", + baggage, + false, + "fakeType", + 0, + tracer.traceCollectorFactory.create(DDTraceId.ONE), + null, + null, + NoopPathwayContext.INSTANCE, + false, + PropagationTags.factory().fromHeaderValue(PropagationTags.HeaderType.DATADOG, "_dd.p.dm=-4,_dd.p.anytag=value")) + when: + injector.inject(mockedContext, carrier, MapSetter.INSTANCE) + + then: + 1 * carrier.put(BAGGAGE_KEY, baggageHeaders) + 0 * _ + + cleanup: + tracer.close() + + where: + baggage | baggageHeaders + [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" + ['",;\\()/:<=>?@[]{}': '",;\\'] | "%22%2C%3B%5C%28%29%2F%3A%3C%3D%3E%3F%40%5B%5D%7B%7D=%22%2C%3B%5C" + ["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') + final DDSpanContext mockedContext = + new DDSpanContext( + DDTraceId.from("1"), + DDSpanId.from("2"), + DDSpanId.ZERO, + null, + "fakeService", + "fakeOperation", + "fakeResource", + UNSET, + "fakeOrigin", + baggage, + false, + "fakeType", + 0, + tracer.traceCollectorFactory.create(DDTraceId.ONE), + null, + null, + NoopPathwayContext.INSTANCE, + false, + PropagationTags.factory().fromHeaderValue(PropagationTags.HeaderType.DATADOG, "_dd.p.dm=-4,_dd.p.anytag=value")) + + when: + injector.inject(mockedContext, carrier, MapSetter.INSTANCE) + + then: + 1 * carrier.put(BAGGAGE_KEY, baggageHeaders) + 0 * _ + + cleanup: + tracer.close() + + where: + baggage | baggageHeaders + [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') + final DDSpanContext mockedContext = + new DDSpanContext( + DDTraceId.from("1"), + DDSpanId.from("2"), + DDSpanId.ZERO, + null, + "fakeService", + "fakeOperation", + "fakeResource", + UNSET, + "fakeOrigin", + baggage, + false, + "fakeType", + 0, + tracer.traceCollectorFactory.create(DDTraceId.ONE), + null, + null, + NoopPathwayContext.INSTANCE, + false, + PropagationTags.factory().fromHeaderValue(PropagationTags.HeaderType.DATADOG, "_dd.p.dm=-4,_dd.p.anytag=value")) + + when: + injector.inject(mockedContext, carrier, MapSetter.INSTANCE) + + then: + 1 * carrier.put(BAGGAGE_KEY, baggageHeaders) + 0 * _ + + cleanup: + tracer.close() + + where: + baggage | baggageHeaders + // [key1: "val1", key2: "val2"] | "key1=val1,key2=val2" + [key1: "val1", key2: "val2", key3: "val3"] | "key1=val1,key2=val2" + ["abcdefg": "hijklmnopq♥"] | "" + } +} 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 aee544c3f24..a94f07f682b 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; @@ -989,6 +991,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 = @@ -2243,6 +2251,14 @@ public boolean isTracePropagationExtractFirst() { return tracePropagationExtractFirst; } + 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 b574dc35038..48ac94bcddb 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 @@ -10,6 +10,7 @@ public interface AgentPropagation { Concern TRACING_CONCERN = Concern.named("tracing"); + Concern BAGGAGE_CONCERN = Concern.named("baggage"); void inject(AgentSpan span, C carrier, Setter setter); @@ -48,6 +49,7 @@ interface ContextVisitor extends CarrierVisitor { @ParametersAreNonnullByDefault @Override default void forEachKeyValue(C carrier, BiConsumer visitor) { + System.out.println("inside foreachkeyvalue"); forEachKey( carrier, (key, value) -> { 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..00e44365628 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/BaggageContext.java @@ -0,0 +1,33 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import datadog.context.Context; +import datadog.context.ContextKey; +import datadog.context.ImplicitContextKeyed; +import java.util.Map; + +public class BaggageContext implements ImplicitContextKeyed { + private static final ContextKey CONTEXT_KEY = ContextKey.named("baggage-key"); + + private Map baggage; + + public static BaggageContext create(Map baggage) { + return new BaggageContext(baggage); + } + + private BaggageContext(Map baggage) { + this.baggage = baggage; + } + + public static BaggageContext fromContext(Context context) { + return context.get(CONTEXT_KEY); + } + + public Map getBaggage() { + return baggage; + } + + @Override + public Context storeInto(Context context) { + return context.with(CONTEXT_KEY, this); + } +}