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

RUM-1924: Use tracestate header to supply vendor-specific information #1694

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import io.opentracing.SpanContext;
import io.opentracing.propagation.TextMapExtract;
import io.opentracing.propagation.TextMapInject;
import kotlin.text.StringsKt;

/**
* A codec designed for HTTP transport via headers using W3C traceparent header
Expand All @@ -31,11 +32,21 @@
class W3CHttpCodec {

private static final String TRACEPARENT_KEY = "traceparent";
private static final String TRACEPARENT_VALUE = "00-0000000000000000%s-%s-0%s";
private static final String TRACESTATE_KEY = "tracestate";

private static final String TRACEPARENT_VALUE = "00-%s-%s-0%s";

private static final int TRACECONTEXT_PARENT_ID_LENGTH = 16;
mariusc83 marked this conversation as resolved.
Show resolved Hide resolved
private static final int TRACECONTEXT_TRACE_ID_LENGTH = 32;
private static final String SAMPLING_PRIORITY_ACCEPT = String.valueOf(1);
private static final String SAMPLING_PRIORITY_DROP = String.valueOf(0);
private static final int HEX_RADIX = 16;

private static final String ORIGIN_TRACESTATE_TAG_VALUE = "o";
private static final String SAMPLING_PRIORITY_TRACESTATE_TAG_VALUE = "s";
private static final String PARENT_SPAN_ID_TRACESTATE_TAG_VALUE = "p";
private static final String DATADOG_VENDOR_TRACESTATE_PREFIX = "dd=";

private W3CHttpCodec() {
// This class should not be created. This also makes code coverage checks happy.
}
Expand All @@ -47,11 +58,15 @@ public void inject(final DDSpanContext context, final TextMapInject carrier) {
try {
String traceId = context.getTraceId().toString(HEX_RADIX).toLowerCase(Locale.US);
String spanId = context.getSpanId().toString(HEX_RADIX).toLowerCase(Locale.US);
String samplingPriority = convertSamplingPriority(context.getSamplingPriority());
String origin = context.getOrigin();

carrier.put(TRACEPARENT_KEY, String.format(TRACEPARENT_VALUE,
traceId,
spanId,
convertSamplingPriority(context.getSamplingPriority())));
StringsKt.padStart(traceId, TRACECONTEXT_TRACE_ID_LENGTH, '0'),
StringsKt.padStart(spanId, TRACECONTEXT_PARENT_ID_LENGTH, '0'),
samplingPriority));
// TODO RUM-2121 3rd party vendor information will be erased
carrier.put(TRACESTATE_KEY, createTraceStateHeader(samplingPriority, origin, spanId));

} catch (final NumberFormatException e) {
}
Expand All @@ -60,6 +75,30 @@ public void inject(final DDSpanContext context, final TextMapInject carrier) {
private String convertSamplingPriority(final int samplingPriority) {
return samplingPriority > 0 ? SAMPLING_PRIORITY_ACCEPT : SAMPLING_PRIORITY_DROP;
}

private String createTraceStateHeader(
String samplingPriority,
String origin,
String parentSpanId
) {
StringBuilder sb = new StringBuilder(DATADOG_VENDOR_TRACESTATE_PREFIX)
.append(SAMPLING_PRIORITY_TRACESTATE_TAG_VALUE)
.append(":")
.append(samplingPriority)
.append(";")
.append(PARENT_SPAN_ID_TRACESTATE_TAG_VALUE)
.append(":")
.append(parentSpanId);

if (origin != null) {
sb.append(";")
.append(ORIGIN_TRACESTATE_TAG_VALUE)
.append(":")
.append(origin.toLowerCase(Locale.US));
}

return sb.toString();
}
}

public static class Extractor implements HttpCodec.Extractor {
Expand All @@ -80,6 +119,7 @@ public SpanContext extract(final TextMapExtract carrier) {
BigInteger traceId = BigInteger.ZERO;
BigInteger spanId = BigInteger.ZERO;
int samplingPriority = PrioritySampling.UNSET;
String origin = null;

for (final Map.Entry<String, String> entry : carrier) {
final String key = entry.getKey().toLowerCase(Locale.US);
Expand All @@ -90,13 +130,15 @@ public SpanContext extract(final TextMapExtract carrier) {
}

if (TRACEPARENT_KEY.equalsIgnoreCase(key)) {
// version - traceId - parentId - traceFlags
String[] valueParts = value.split("-");

if (valueParts.length != 4){
continue;
}

if (valueParts[0] == "ff"){
if ("ff".equalsIgnoreCase(valueParts[0])){
// ff version is forbidden
continue;
}

Expand All @@ -116,6 +158,9 @@ public SpanContext extract(final TextMapExtract carrier) {

samplingPriority = convertSamplingPriority(valueParts[3]);

} else if (TRACESTATE_KEY.equalsIgnoreCase(key)) {
Map<String, String> datadogTraceStateTags = extractDatadogTagsFromTraceState(value);
origin = datadogTraceStateTags.get(ORIGIN_TRACESTATE_TAG_VALUE);
}

if (taggedHeaders.containsKey(key)) {
Expand All @@ -132,14 +177,14 @@ public SpanContext extract(final TextMapExtract carrier) {
traceId,
spanId,
samplingPriority,
null,
origin,
Collections.<String, String>emptyMap(),
tags);
context.lockSamplingPriority();

return context;
} else if (!tags.isEmpty()) {
return new TagContext(null, tags);
return new TagContext(origin, tags);
}
} catch (final RuntimeException e) {
}
Expand All @@ -152,5 +197,23 @@ private int convertSamplingPriority(final String samplingPriority) {
? PrioritySampling.SAMPLER_KEEP
: PrioritySampling.SAMPLER_DROP;
}

private Map<String, String> extractDatadogTagsFromTraceState(String traceState) {
String[] vendors = traceState.split(",");
Map<String, String> tags = new HashMap<>();
for (String vendor : vendors) {
if (vendor.startsWith(DATADOG_VENDOR_TRACESTATE_PREFIX)) {
String[] vendorTags = vendor.substring(DATADOG_VENDOR_TRACESTATE_PREFIX.length())
.split(";");
for (String vendorTag : vendorTags) {
String[] keyAndValue = vendorTag.split(":");
if (keyAndValue.length == 2) {
tags.put(keyAndValue[0], keyAndValue[1]);
}
}
}
}
return tags;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -480,14 +480,25 @@ internal constructor(

TracingHeaderType.TRACECONTEXT -> {
requestBuilder.removeHeader(W3C_TRACEPARENT_KEY)
requestBuilder.removeHeader(W3C_TRACESTATE_KEY)
val traceId = span.context().toTraceId()
val spanId = span.context().toSpanId()
requestBuilder.addHeader(
W3C_TRACEPARENT_KEY,
@Suppress("UnsafeThirdPartyFunctionCall") // Format string is static
W3C_DROP_SAMPLING_DECISION.format(
span.context().toTraceId(),
span.context().toSpanId()
W3C_TRACEPARENT_DROP_SAMPLING_DECISION.format(
traceId.padStart(length = W3C_TRACE_ID_LENGTH, padChar = '0'),
spanId.padStart(length = W3C_PARENT_ID_LENGTH, padChar = '0')
)
)
// TODO RUM-2121 3rd party vendor information will be erased
@Suppress("UnsafeThirdPartyFunctionCall") // Format string is static
var traceStateHeader = W3C_TRACESTATE_DROP_SAMPLING_DECISION
.format(spanId.padStart(length = W3C_PARENT_ID_LENGTH, padChar = '0'))
if (traceOrigin != null) {
traceStateHeader += ";o:$traceOrigin"
}
requestBuilder.addHeader(W3C_TRACESTATE_KEY, traceStateHeader)
}
}
}
Expand Down Expand Up @@ -538,7 +549,8 @@ internal constructor(
tracedRequestBuilder.addHeader(key, value)
}

W3C_TRACEPARENT_KEY -> if (tracingHeaderTypes.contains(TracingHeaderType.TRACECONTEXT)) {
W3C_TRACEPARENT_KEY,
W3C_TRACESTATE_KEY -> if (tracingHeaderTypes.contains(TracingHeaderType.TRACECONTEXT)) {
tracedRequestBuilder.addHeader(key, value)
}

Expand Down Expand Up @@ -649,7 +661,13 @@ internal constructor(

// taken from W3CHttpCodec
internal const val W3C_TRACEPARENT_KEY = "traceparent"
internal const val W3C_DROP_SAMPLING_DECISION = "00-%s-%s-00"
internal const val W3C_TRACESTATE_KEY = "tracestate"

// https://www.w3.org/TR/trace-context/#traceparent-header
internal const val W3C_TRACEPARENT_DROP_SAMPLING_DECISION = "00-%s-%s-00"
mariusc83 marked this conversation as resolved.
Show resolved Hide resolved
internal const val W3C_TRACESTATE_DROP_SAMPLING_DECISION = "dd=p:%s;s:0"
mariusc83 marked this conversation as resolved.
Show resolved Hide resolved
internal const val W3C_SAMPLING_DECISION_INDEX = 3
internal const val W3C_TRACE_ID_LENGTH = 32
internal const val W3C_PARENT_ID_LENGTH = 16
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.datadog.android.api.feature.Feature
import com.datadog.android.core.internal.net.DefaultFirstPartyHostHeaderTypeResolver
import com.datadog.android.core.internal.utils.loggableStackTrace
import com.datadog.android.core.sampling.Sampler
import com.datadog.android.okhttp.utils.assertj.HeadersAssert.Companion.assertThat
import com.datadog.android.okhttp.utils.config.DatadogSingletonTestConfiguration
import com.datadog.android.okhttp.utils.verifyLog
import com.datadog.android.trace.TracingHeaderType
Expand Down Expand Up @@ -148,8 +149,7 @@ internal open class TracingInterceptorNonDdTracerNotSendingSpanTest {
@StringForgery(type = StringForgeryType.HEXADECIMAL)
lateinit var fakeTraceId: String

@StringForgery
lateinit var fakeOrigin: String
private var fakeOrigin: String? = null

lateinit var fakeLocalHosts: Map<String, Set<TracingHeaderType>>

Expand All @@ -165,6 +165,7 @@ internal open class TracingInterceptorNonDdTracerNotSendingSpanTest {
whenever(mockSpanContext.toTraceId()) doReturn fakeTraceId
whenever(mockTraceSampler.sample()) doReturn true

fakeOrigin = forge.aNullable { anAlphabeticalString() }
fakeMediaType = if (forge.aBool()) {
val mediaType = forge.anElementFrom("application", "image", "text", "model") +
"/" + forge.anAlphabeticalString()
Expand Down Expand Up @@ -338,12 +339,16 @@ internal open class TracingInterceptorNonDdTracerNotSendingSpanTest {
assertThat(response).isSameAs(fakeResponse)
argumentCaptor<Request> {
verify(mockChain).proceed(capture())
assertThat(lastValue.header(TracingInterceptor.W3C_TRACEPARENT_KEY))
.isEqualTo(
"00-%s-%s-00".format(
mockSpan.context().toTraceId(),
mockSpan.context().toSpanId()
)
assertThat(lastValue.headers)
.hasTraceParentHeader(
mockSpan.context().toTraceId(),
mockSpan.context().toSpanId(),
isSampled = false
)
.hasTraceStateHeaderWithOnlyDatadogVendorValues(
mockSpan.context().toSpanId(),
isSampled = false,
fakeOrigin
)
}
}
Expand Down Expand Up @@ -486,12 +491,16 @@ internal open class TracingInterceptorNonDdTracerNotSendingSpanTest {
assertThat(response).isSameAs(fakeResponse)
argumentCaptor<Request> {
verify(mockChain).proceed(capture())
assertThat(lastValue.header(TracingInterceptor.W3C_TRACEPARENT_KEY))
.isEqualTo(
"00-%s-%s-00".format(
mockSpan.context().toTraceId(),
mockSpan.context().toSpanId()
)
assertThat(lastValue.headers)
.hasTraceParentHeader(
mockSpan.context().toTraceId(),
mockSpan.context().toSpanId(),
isSampled = false
)
.hasTraceStateHeaderWithOnlyDatadogVendorValues(
mockSpan.context().toSpanId(),
isSampled = false,
fakeOrigin
)
}
}
Expand Down Expand Up @@ -831,12 +840,16 @@ internal open class TracingInterceptorNonDdTracerNotSendingSpanTest {
assertThat(response).isSameAs(fakeResponse)
argumentCaptor<Request> {
verify(mockChain).proceed(capture())
assertThat(lastValue.header(TracingInterceptor.W3C_TRACEPARENT_KEY))
.isEqualTo(
"00-%s-%s-00".format(
mockSpan.context().toTraceId(),
mockSpan.context().toSpanId()
)
assertThat(lastValue.headers)
.hasTraceParentHeader(
mockSpan.context().toTraceId(),
mockSpan.context().toSpanId(),
isSampled = false
)
.hasTraceStateHeaderWithOnlyDatadogVendorValues(
mockSpan.context().toSpanId(),
isSampled = false,
fakeOrigin
)
}
}
Expand Down
Loading
Loading