Skip to content

Commit

Permalink
RUM-1924: Use tracestate header to supply vendor-specific information
Browse files Browse the repository at this point in the history
  • Loading branch information
0xnm committed Nov 13, 2023
1 parent 7e80c37 commit c4a7806
Show file tree
Hide file tree
Showing 7 changed files with 381 additions and 112 deletions.
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;
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"
internal const val W3C_TRACESTATE_DROP_SAMPLING_DECISION = "dd=p:%s;s:0"
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

0 comments on commit c4a7806

Please sign in to comment.