RUM-1829 Provide 128 bits support for the trace ids in the Tracing sdk#2089
Conversation
| @@ -50,6 +51,9 @@ | |||
| public class DDTracer implements io.opentracing.Tracer, Closeable, Tracer { | |||
| // UINT64 max value | |||
There was a problem hiding this comment.
seems like comment should be updated?
| int samplingPriority = PrioritySampling.UNSET; | ||
| String origin = null; | ||
| String mostSignificantTraceId = "0"; | ||
| String lessSignificantTraceId = "0"; |
There was a problem hiding this comment.
| String lessSignificantTraceId = "0"; | |
| String leastSignificantTraceId = "0"; |
| final BigInteger parsedValue = new StringCachingBigInteger(value, radix); | ||
| if (parsedValue.compareTo(DDTracer.TRACE_ID_MIN) < 0 | ||
| || parsedValue.compareTo(DDTracer.TRACE_ID_MAX) > 0) { | ||
| || parsedValue.compareTo(DDTracer.SPAN_ID_MAX) > 0) { |
There was a problem hiding this comment.
check the method name...this validates a 64bits number and we introduced another one below for 128 bits. We also introduced a SPAN_ID_MAX and TRACE_ID_MAX (one for 64 and the other for 128). Maybe I should re - name these TRACE_ID_64 and TRACE_ID_128 ?
There was a problem hiding this comment.
naming should be changed indeed, because using trace word for the lower boundary and span word for the upper boundary is kind of confusing.
| private const val HEX_RADIX = 16 | ||
| private val LONG_MASK = BigInteger("ffffffffffffffff", HEX_RADIX) | ||
|
|
||
| fun lessSignificantUnsignedLongAsHexa(traceId: BigInteger): String { |
There was a problem hiding this comment.
| fun lessSignificantUnsignedLongAsHexa(traceId: BigInteger): String { | |
| fun leastSignificantUnsignedLongAsHex(traceId: BigInteger): String { |
| @Suppress("MagicNumber") | ||
| return traceId.and(LONG_MASK).toString(HEX_RADIX) | ||
| } | ||
|
|
||
| fun lessSignificantUnsignedLongAsDecimal(traceId: BigInteger): String { | ||
| @Suppress("MagicNumber") | ||
| return traceId.and(LONG_MASK).toString() | ||
| } | ||
|
|
||
| fun mostSignificantUnsignedLongAsHexa(traceId: BigInteger): String { | ||
| @Suppress("MagicNumber") | ||
| return traceId.shiftRight(LONG_BITS_SIZE).toString(HEX_RADIX) |
There was a problem hiding this comment.
is Suppress(MagicNumber) still needed here? all the arguments are named
| } | ||
|
|
||
| fun hasTraceId(traceId: String): SpanEventAssert { | ||
| fun hasLessSignificantTraceId(traceId: String): SpanEventAssert { |
There was a problem hiding this comment.
| fun hasLessSignificantTraceId(traceId: String): SpanEventAssert { | |
| fun hasLeastSignificantTraceId(traceId: String): SpanEventAssert { |
| assertThat(actual.traceId) | ||
| .overridingErrorMessage( | ||
| "Expected SpanEvent to have traceId: $traceId" + | ||
| "Expected SpanEvent to have less significant traceId: $traceId" + |
There was a problem hiding this comment.
| "Expected SpanEvent to have less significant traceId: $traceId" + | |
| "Expected SpanEvent to have least significant traceId: $traceId" + |
| } | ||
|
|
||
| fun hasMostSignificantTraceId(traceId: String): SpanEventAssert { | ||
| val lessSignificantTraceId = actual.meta.additionalProperties[TRACE_ID_META_KEY] |
There was a problem hiding this comment.
| val lessSignificantTraceId = actual.meta.additionalProperties[TRACE_ID_META_KEY] | |
| val mostSignificantTraceId = actual.meta.additionalProperties[TRACE_ID_META_KEY] |
| TracingHeaderType.B3, | ||
| TracingHeaderType.B3MULTI |
There was a problem hiding this comment.
do we have this set up on the backend side? is it useful?
There was a problem hiding this comment.
nope this was just for tests will be removed in the actual PR
| firstPartyHostsWithHeaderType = tracedHosts.associateWith { | ||
| tracingHeaderTypes | ||
| } |
There was a problem hiding this comment.
alternatively we can set them up in the core configuration
8a2f82e to
28c1051
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #2089 +/- ##
===========================================
+ Coverage 69.68% 70.18% +0.49%
===========================================
Files 718 721 +3
Lines 26662 26759 +97
Branches 4468 4485 +17
===========================================
+ Hits 18579 18779 +200
+ Misses 6857 6733 -124
- Partials 1226 1247 +21
|
28c1051 to
c385c73
Compare
| // BigInteger overflow cannot happen in this context | ||
|
|
||
| @Suppress("SwallowedException") | ||
| fun leastSignificant64BitsAsHex(traceId: BigInteger): String { |
There was a problem hiding this comment.
minor: we could add 'safe' to these method names to indicate they won't throw when called
There was a problem hiding this comment.
good call but I am afraid I am going to run of out space for that method name. Can you give me an example of how this method should be named in that case ?
| @@ -2,13 +2,11 @@ Dependencies List | |||
|
|
|||
| com.squareup.okhttp3:okhttp:4.11.0 : 768 Kb | |||
There was a problem hiding this comment.
I've seen this particular change in a few prs unintentionally so just making sure -> in this pr we want this change?
There was a problem hiding this comment.
As I replied to Nikita above not sure why, need to check what's going on.
|
|
||
| @Test | ||
| fun `M return empty string W traceIdAsHexString() { ddSpanContext id is null}`() { | ||
| // When |
There was a problem hiding this comment.
Should be Given I think - same for some others in this class
| * Returns the span's traceId in hex format. | ||
| * The [SpanContext.toTraceId] method returns a string in decimal format, | ||
| * which doesn't match what we send in our events | ||
| * Returns the span's less significant trace id in hex format (the last 64 bits from the 128 bits trace id) |
There was a problem hiding this comment.
| * Returns the span's less significant trace id in hex format (the last 64 bits from the 128 bits trace id) | |
| * Returns the span's least significant trace id in hex format (the last 64 bits from the 128 bits trace id) |
|
|
||
| // Then | ||
| assertThat(traceId.substring(0, 16)).isEqualTo("0000000000000000") | ||
| assertThat(traceId.length).isEqualTo(32) |
There was a problem hiding this comment.
minor proposal for a better message:
| assertThat(traceId.length).isEqualTo(32) | |
| assertThat(traceId).hasSize(32) |
| private static final String OT_BAGGAGE_PREFIX = "ot-baggage-"; | ||
| private static final String TRACE_ID_KEY = "x-datadog-trace-id"; | ||
| private static final String SPAN_ID_KEY = "x-datadog-parent-id"; | ||
| private static final String SAMPLING_PRIORITY_KEY = "x-datadog-sampling-priority"; | ||
| private static final String ORIGIN_KEY = "x-datadog-origin"; | ||
|
|
||
| private DatadogHttpCodec() { | ||
| // This class should not be created. This also makes code coverage checks happy. | ||
| } | ||
|
|
||
| public static class Injector implements HttpCodec.Injector { | ||
|
|
||
| @Override | ||
| public void inject(final DDSpanContext context, final TextMapInject carrier) { | ||
| carrier.put(TRACE_ID_KEY, context.getTraceId().toString()); | ||
| carrier.put(SPAN_ID_KEY, context.getSpanId().toString()); | ||
| final String origin = context.getOrigin(); | ||
| if (origin != null) { | ||
| carrier.put(ORIGIN_KEY, origin); | ||
| } | ||
|
|
||
| for (final Map.Entry<String, String> entry : context.baggageItems()) { | ||
| carrier.put(OT_BAGGAGE_PREFIX + entry.getKey(), HttpCodec.encode(entry.getValue())); | ||
| } | ||
|
|
||
| // always use max sampling priority for Android traces | ||
| carrier.put(SAMPLING_PRIORITY_KEY, "1"); | ||
| public static final String OT_BAGGAGE_PREFIX = "ot-baggage-"; | ||
| public static final String LEAST_SIGNIFICANT_TRACE_ID_KEY = "x-datadog-trace-id"; | ||
| public static final String MOST_SIGNIFICANT_TRACE_ID_KEY = "_dd.p.tid"; | ||
| public static final String DATADOG_TAGS_KEY = "x-datadog-tags"; | ||
| public static final String SPAN_ID_KEY = "x-datadog-parent-id"; | ||
| public static final String SAMPLING_PRIORITY_KEY = "x-datadog-sampling-priority"; | ||
| public static final String ORIGIN_KEY = "x-datadog-origin"; |
There was a problem hiding this comment.
is private -> public change necessary?
Upd: ok, probably for the test?
| final String leastSignificantTraceId = bigIntegerUtils.leastSignificant64BitsAsDecimal(traceId); | ||
| final String mostSignificantTraceId = bigIntegerUtils.mostSignificant64BitsAsHex(traceId); |
There was a problem hiding this comment.
why to we take lower part as decimal, but upper part as hex. some compatibility thing?
| tags.put(taggedHeaders.get(key), HttpCodec.decode(value)); | ||
| } | ||
| } | ||
| if(leastSignificant64BitsTraceIdAsDecimal==null|| mostSignificant64BitsTraceIdAsHex==null){ |
There was a problem hiding this comment.
| if(leastSignificant64BitsTraceIdAsDecimal==null|| mostSignificant64BitsTraceIdAsHex==null){ | |
| if (leastSignificant64BitsTraceIdAsDecimal == null || mostSignificant64BitsTraceIdAsHex == null) { |
| final String[] tagArray = tags.split(","); | ||
| for (String tag : tagArray) { | ||
| final String[] tagKeyValue = tag.split("="); | ||
| if (tagKeyValue.length >= 2 && tagKeyValue[0].equals(MOST_SIGNIFICANT_TRACE_ID_KEY)) { |
There was a problem hiding this comment.
| if (tagKeyValue.length >= 2 && tagKeyValue[0].equals(MOST_SIGNIFICANT_TRACE_ID_KEY)) { | |
| if (tagKeyValue.length >= 2 && MOST_SIGNIFICANT_TRACE_ID_KEY.equals(tagKeyValue[0]) { |
There was a problem hiding this comment.
should be reverted? why this change comes?
There was a problem hiding this comment.
Yes I need to see why is this happening as I did not change anything there
| } | ||
|
|
||
| @Test | ||
| fun `M return null W extract { out of bounds less significant trace id }`(forge: Forge) { |
There was a problem hiding this comment.
| fun `M return null W extract { out of bounds less significant trace id }`(forge: Forge) { | |
| fun `M return null W extract { out of bounds least significant trace id }`(forge: Forge) { |
| val fakeOutOfBoundsLessSignficantTraceId = | ||
| BigInteger(forge.aStringMatching("[0-9][a-f]{$outOfBoundsSize}"), 16).toString() | ||
| val headers = resolveExtractedHeadersFromSpanContext(fakeDDSpanContext, forge).apply { | ||
| set(DatadogHttpCodec.LEAST_SIGNIFICANT_TRACE_ID_KEY, fakeOutOfBoundsLessSignficantTraceId) |
There was a problem hiding this comment.
| val fakeOutOfBoundsLessSignficantTraceId = | |
| BigInteger(forge.aStringMatching("[0-9][a-f]{$outOfBoundsSize}"), 16).toString() | |
| val headers = resolveExtractedHeadersFromSpanContext(fakeDDSpanContext, forge).apply { | |
| set(DatadogHttpCodec.LEAST_SIGNIFICANT_TRACE_ID_KEY, fakeOutOfBoundsLessSignficantTraceId) | |
| val fakeOutOfBoundsLeastSignficantTraceId = | |
| BigInteger(forge.aStringMatching("[0-9][a-f]{$outOfBoundsSize}"), 16).toString() | |
| val headers = resolveExtractedHeadersFromSpanContext(fakeDDSpanContext, forge).apply { | |
| set(DatadogHttpCodec.LEAST_SIGNIFICANT_TRACE_ID_KEY, fakeOutOfBoundsLeastSignficantTraceId) |
| @MethodSource("spanContextValues") | ||
| fun spanContextValues(): List<DDSpanContext> { | ||
| val max128BitsHexa = forge.aStringMatching("[f]{32}") | ||
| val minLessSignificant128BitsHexa = forge.aStringMatching("[0]{31}[1]{1}") |
There was a problem hiding this comment.
| val minLessSignificant128BitsHexa = forge.aStringMatching("[0]{31}[1]{1}") | |
| val minLeastSignificant128BitsHexa = forge.aStringMatching("[0]{31}[1]{1}") |
| val tracer = testedProvider.tracerBuilder(fakeInstrumentationName).build() | ||
| val blockingWriterWrapper = tracer.useBlockingWriter() | ||
| var parentSpanTraceId: String | ||
| var lessSignificantParentSpanTraceId: String |
There was a problem hiding this comment.
| var lessSignificantParentSpanTraceId: String | |
| var leastSignificantParentSpanTraceId: String |
c385c73 to
2a5b4e4
Compare
6cc1e75 to
60ee7be
Compare
|
|
||
| private lateinit var fakeTaggedHeaders: Map<String, String> | ||
|
|
||
| private lateinit var fakeIdAsHexaString: String |
There was a problem hiding this comment.
| private lateinit var fakeIdAsHexaString: String | |
| private lateinit var fakeIdAsHexString: String |
| } | ||
|
|
||
| @Test | ||
| fun `M return the expected hexa padded string W traceIdAsHexString()`() { |
There was a problem hiding this comment.
| fun `M return the expected hexa padded string W traceIdAsHexString()`() { | |
| fun `M return the expected hex padded string W traceIdAsHexString()`() { |
| val traceId = span.spanContext.traceId | ||
|
|
||
| // Then | ||
| assertThat(traceId.substring(0, 16)).isEqualTo("0000000000000000") |
There was a problem hiding this comment.
/improvement
we should assert on the content of the traceId as well, not just the size.
There was a problem hiding this comment.
what do you mean the content ? how would you assess that ? Do you mean regex assessment ? That will be too much IMO
There was a problem hiding this comment.
that's the only way we can make sure final version is correct with a plain string assert.
| // UINT64 max value | ||
| public static final BigInteger TRACE_ID_MAX = | ||
| // UINT128 max value | ||
| public static final BigInteger TRACE_ID_128_BITS_MAX = |
There was a problem hiding this comment.
/question
why do we need to subtract 1? Theoretically an id can be all bits set to 1. ie BigInteger's max value.
There was a problem hiding this comment.
2^128-1 (we remove the 0). That was already in the java code I am just following the pattern.
| return value; | ||
| } | ||
|
|
||
| private BigInteger generateNewTraceId() { |
There was a problem hiding this comment.
/blocking
This shouldn't be any random id but must be in chronologically sorted pattern.
TLDR
<32-bit unix seconds> <32 bits of zero> <64 random bits>
There was a problem hiding this comment.
that's the old java code and we need to be consistent with the way the id is generated for spans. I am not touching that part here.
There was a problem hiding this comment.
why does it need to be chronologically sorted ? Is there a blocking reason behind it ?
There was a problem hiding this comment.
Ok I see why, we will switch to the new CoreTracer way of generating ids.
| assertThat(lastValue.header(TracingInterceptor.DATADOG_LEAST_SIGNIFICANT_64_BITS_TRACE_ID_HEADER)).isNull() | ||
| assertThat(lastValue.header(TracingInterceptor.DATADOG_SPAN_ID_HEADER)).isNull() | ||
| assertThat(lastValue.header(TracingInterceptor.DATADOG_ORIGIN_HEADER)).isNull() | ||
| assertThat(lastValue.header(TracingInterceptor.DATADOG_TAGS)).isNull() |
There was a problem hiding this comment.
/improvement
could you also assert on the tag value? this should be higher order part.
There was a problem hiding this comment.
not sure if it is possible or is not going to be too much. Need to check.
| val event0 = JsonParser.parseString(eventsWritten[0].eventData) as JsonObject | ||
| assertThat(event0.getString("env")).isEqualTo(stubSdkCore.getDatadogContext().env) | ||
| assertThat(event0.getString("spans[0].trace_id")).isEqualTo(traceId.toHexString()) | ||
| assertThat(event0.getString("spans[0].trace_id")).isEqualTo(leastSignificantTraceId) |
There was a problem hiding this comment.
/improvement
could you also assert on meta._dd.p.tid?
There was a problem hiding this comment.
you mean meta._dd.p.id I think ? this property always confuses me as it is _dd.p.id into the distributed traces headers.
4a9da7e to
975a0e7
Compare
975a0e7 to
f15c224
Compare
| assertThat(traceId).isNotNull | ||
| assertThat(traceId).isNotEqualTo("foo") | ||
| assertThat(traceId).isNotEqualTo(DDTraceId.ZERO) | ||
| assertThat(traceId).isEqualTo(traceId) |
There was a problem hiding this comment.
/blocking
this is always going to be true.
There was a problem hiding this comment.
Assuming Sequential here is what we default do. Could you make sure we assert on the right things here
There was a problem hiding this comment.
yes I ported this test from the APM code, that's weird indeed let me check what they have in original groovy test. I might have missed something
There was a problem hiding this comment.
I checked, all good that's the test, I think the reason why is to test the reflexivity of equals method: "an object must equal itself"
I basically performed all these tests + many other but will double check it now. |
f15c224 to
ce9e1f3
Compare
256c37e to
8b3c6ab
Compare
| } | ||
|
|
||
| @Test | ||
| fun `M extract unsigned less and most significant bits W extract LSB and MSB`() { |
There was a problem hiding this comment.
| fun `M extract unsigned less and most significant bits W extract LSB and MSB`() { | |
| fun `M extract unsigned least and most significant bits W extract LSB and MSB`() { |
| companion object { | ||
| private const val MAX_UNSIGNED_LONG_HEXA_STRING = "ffffffffffffffff" | ||
| private const val MIN_UNSIGNED_LONG_HEXA_STRING = "0000000000000000" | ||
| private val MAX_EXEC_TIME_IN_NANOS = TimeUnit.MILLISECONDS.toNanos(8) |
There was a problem hiding this comment.
it is not really relevant, because these are the unit tests, which are executed on JVM, on a server CPU, etc. The execution number you get here is be quite different from the one on the real device.
| val json = sentSpansObjects.first { | ||
| it.get(TRACE_ID_KEY).asString == Long.toHexString((span.traceId.toLong())) && | ||
| it.get(SPAN_ID_KEY).asString == Long.toHexString((span.spanId.toLong())) | ||
| val leasSignificantTraceId = it.get(TRACE_ID_KEY).asString |
| val leasSignificantTraceId = it.get(TRACE_ID_KEY).asString | ||
| val mostSignificantTraceId = it.getAsJsonObject("meta") | ||
| .getAsJsonPrimitive(MOST_SIGNIFICANT_64_BITS_TRACE_ID_KEY).asString | ||
| leasSignificantTraceId == span.leastSignificant64BitsTraceId() && |
| fun Span.traceIdAsHexString(): String { | ||
| return context().toTraceId().toLong().toHexString() | ||
| fun Span.leastSignificant64BitsTraceId(): String { | ||
| return (this as? DDSpan)?.traceId?.toString(16)?.padStart(32, '0')?.takeLast(16) ?: "" |
There was a problem hiding this comment.
will we be aware about the cases when we get empty string? nothing will crash?
There was a problem hiding this comment.
based on the code we have there is no way we could get empty strings here as the generator is tested above and other tests will fail first. Here the debate is either we add these logs just for the sake of it or we just don't and consider this safe.
There was a problem hiding this comment.
also we will be aware as I added this extra checks:
hasValidMostSignificant64BitsTraceId()
hasValidLeastSignificant64BitsTraceId()
They validate the regEx
| val tracer = testedProvider.tracerBuilder(fakeInstrumentationName).build() | ||
| val blockingWriterWrapper = tracer.useBlockingWriter() | ||
| var parentSpanTraceId: String | ||
| var lessSignificantParentSpanTraceId: String |
8b3c6ab to
446a7b4
Compare
What does this PR do?
In this PR we are adding the 128 bits support for our OpenTelemetry and OpenTracing manual APIs + the distributed traces handled through our DatadogInterceptor. The PR contains 3 parts:
Motivation
What inspired you to submit this pull request?
Additional Notes
Anything else we should know when reviewing?
Review checklist (to be filled by reviewers)