Skip to content
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

[WIP] initial implementation and unit tests for inject/extract #8330

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,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;
Expand Down Expand Up @@ -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<TracePropagationStyle> DEFAULT_TRACE_PROPAGATION_STYLE =
new LinkedHashSet<>(asList(DATADOG, TRACECONTEXT));
new LinkedHashSet<>(asList(DATADOG, TRACECONTEXT, BAGGAGE));
static final Set<PropagationStyle> 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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package datadog.trace.core.propagation;

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.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
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 BaggageHttpCodec() {
// This class should not be created. This also makes code coverage checks happy.
}

public static HttpCodec.Injector newInjector(Map<String, String> invertedBaggageMapping) {
return new Injector(invertedBaggageMapping);
}

private static class Injector implements HttpCodec.Injector {

private final Map<String, String> invertedBaggageMapping;

public Injector(Map<String, String> invertedBaggageMapping) {
assert invertedBaggageMapping != null;
this.invertedBaggageMapping = invertedBaggageMapping;
}

@Override
public <C> void inject(
final DDSpanContext context, final C carrier, final AgentPropagation.Setter<C> setter) {
Config config = Config.get();

StringBuilder baggageText = new StringBuilder();
int processedBaggage = 0;
int currentBytes = 0;
int maxItems = config.getTraceBaggageMaxItems();
int maxBytes = config.getTraceBaggageMaxBytes();
int currentCharacters = 0;
int maxSafeCharacters = maxBytes / MAX_CHARACTER_SIZE;
for (final Map.Entry<String, String> entry : context.baggageItems()) {
if (processedBaggage >= maxItems) {
break;
}
StringBuilder currentText = new StringBuilder();
if (processedBaggage != 0) {
currentText.append(',');
}

currentText.append(HttpCodec.encodeBaggage(entry.getKey()));
currentText.append('=');
currentText.append(HttpCodec.encodeBaggage(entry.getValue()));

// worst case check
if (currentCharacters + currentText.length() <= maxSafeCharacters) {
currentCharacters += currentText.length();
} else {
if (currentBytes
== 0) { // special case to calculate byte size after surpassing worst-case number of
// characters
currentBytes = baggageText.toString().getBytes(StandardCharsets.UTF_8).length;
}
int byteSize =
currentText
.toString()
.getBytes(StandardCharsets.UTF_8)
.length; // find largest possible byte size for UTF encoded characters and only do
// size checking after we hit this worst case scenario
if (byteSize + currentBytes > maxBytes) {
break;
}
currentBytes += byteSize;
}
baggageText.append(currentText);
processedBaggage++;
}

setter.set(carrier, BAGGAGE_KEY, baggageText.toString());
}
}

public static HttpCodec.Extractor newExtractor(
Config config, Supplier<TraceConfig> 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<String, String> parseBaggageHeaders(String input) {
Map<String, String> 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ private static Map<TracePropagationStyle, Injector> createInjectors(
case TRACECONTEXT:
result.put(style, W3CHttpCodec.newInjector(reverseBaggageMapping));
break;
case BAGGAGE:
result.put(style, BaggageHttpCodec.newInjector(reverseBaggageMapping));
break;
default:
log.debug("No implementation found to inject propagation style: {}", style);
break;
Expand Down Expand Up @@ -159,6 +162,9 @@ public static Extractor createExtractor(
case TRACECONTEXT:
extractors.add(W3CHttpCodec.newExtractor(config, traceConfigSupplier));
break;
case BAGGAGE:
extractors.add(BaggageHttpCodec.newExtractor(config, traceConfigSupplier));
break;
default:
log.debug("No implementation found to extract propagation style: {}", style);
break;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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.test.util.DDSpecification
import static datadog.trace.core.propagation.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=" | []
}
}
Loading
Loading