Skip to content

Commit

Permalink
RUM-4116 Add the SpanLink support for Otel API implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusc83 committed Apr 18, 2024
1 parent 37e2e35 commit f28a8cf
Show file tree
Hide file tree
Showing 10 changed files with 271 additions and 23 deletions.
3 changes: 2 additions & 1 deletion detekt_custom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,6 @@ datadog:
- "kotlin.String.takeLast(kotlin.Int):kotlin.IllegalArgumentException"
- "kotlin.String.toLong():java.lang.NumberFormatException"
- "kotlin.String.format(kotlin.Array):java.util.IllegalFormatException"
- "kotlin.String.takeLast(kotlin.Int):java.lang.IllegalArgumentException"
- "kotlin.ByteArray.copyOf(kotlin.Int):java.lang.NegativeArraySizeException"
- "kotlin.ByteArray.copyOfRange(kotlin.Int, kotlin.Int):java.lang.IndexOutOfBoundsException,java.lang.IllegalArgumentException"
- "kotlin.ByteArray.get(kotlin.Int):java.lang.IndexOutOfBoundsException"
Expand Down Expand Up @@ -917,6 +916,7 @@ datadog:
- "kotlin.collections.MutableList.isNullOrEmpty()"
- "kotlin.collections.MutableList.iterator()"
- "kotlin.collections.MutableList.joinToString(kotlin.CharSequence, kotlin.CharSequence, kotlin.CharSequence, kotlin.Int, kotlin.CharSequence, kotlin.Function1?)"
- "kotlin.collections.MutableList.map(kotlin.Function1)"
- "kotlin.collections.MutableList.remove(java.io.File)"
- "kotlin.collections.MutableList.remove(java.lang.ref.WeakReference)"
- "kotlin.collections.MutableList.removeAll(kotlin.Function1)"
Expand Down Expand Up @@ -1120,6 +1120,7 @@ datadog:
- "kotlin.String.substringAfter(kotlin.Char, kotlin.String)"
- "kotlin.String.substringAfterLast(kotlin.Char, kotlin.String)"
- "kotlin.String.substringBefore(kotlin.Char, kotlin.String)"
- "kotlin.String.takeLeastSignificant64Bits()"
- "kotlin.String.toByteArray(java.nio.charset.Charset) "
- "kotlin.String.toByteArray(java.nio.charset.Charset)"
- "kotlin.String.toDoubleOrNull()"
Expand Down
1 change: 1 addition & 0 deletions features/dd-sdk-android-trace/api/dd-sdk-android-trace.api
Original file line number Diff line number Diff line change
Expand Up @@ -6079,6 +6079,7 @@ public class com/datadog/trace/core/DDSpan : com/datadog/trace/api/profiling/Tra
public fun getEndpointTracker ()Lcom/datadog/trace/api/EndpointTracker;
public fun getError ()I
public fun getHttpStatusCode ()S
public fun getLinks ()Ljava/util/List;
public synthetic fun getLocalRootSpan ()Lcom/datadog/trace/api/interceptor/MutableSpan;
public synthetic fun getLocalRootSpan ()Lcom/datadog/trace/bootstrap/instrumentation/api/AgentSpan;
public synthetic fun getLocalRootSpan ()Lcom/datadog/trace/core/CoreSpan;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,10 @@ public void setEndpointTracker(@NonNull EndpointTracker endpointTracker) {
}
}

public List<AgentSpanLink> getLinks() {
return links;
}

public Map<String, String> getBaggage() {
return Collections.unmodifiableMap(context.getBaggageItems());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal class OtelTraceWriter(
if (trace == null) return
sdkCore.getFeature(Feature.TRACING_FEATURE_NAME)
?.withWriteContext { datadogContext, eventBatchWriter ->
// TODO: RUM-4092 Add the capability in the serializer to handle multiple spans in one payload
trace.forEach { span ->
@Suppress("ThreadSafety") // called in the worker context
writeSpan(datadogContext, eventBatchWriter, span)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ package com.datadog.android.trace.internal.domain.event

import com.datadog.android.api.context.DatadogContext
import com.datadog.android.api.context.NetworkInfo
import com.datadog.android.core.internal.utils.toHexString
import com.datadog.android.log.LogAttributes
import com.datadog.android.trace.model.SpanEvent
import com.datadog.trace.api.DDSpanId
import com.datadog.trace.bootstrap.instrumentation.api.AgentSpanLink
import com.datadog.trace.core.DDSpan
import com.datadog.trace.core.DDSpanContext
import com.google.gson.JsonArray
import com.google.gson.JsonObject

@Suppress("TooManyFunctions")
internal class OtelDdSpanToSpanEventMapper(
internal val networkInfoEnabled: Boolean
) : ContextAwareMapper<DDSpan, SpanEvent> {
Expand All @@ -28,9 +32,9 @@ internal class OtelDdSpanToSpanEventMapper(
// we remove the first part as we specifically set an IdGeneratorStrategy with
// 64bits length. Our current endpoint does not accept trace ids longer than 64bits
traceId = resolveTraceId(model),
spanId = model.spanId.toHexString(),
parentId = model.parentId.toHexString(),
resource = model.resourceName.toString(), //
spanId = resolveSpanId(model),
parentId = resolveParentId(model),
resource = model.resourceName.toString(),
name = model.operationName.toString(), // GET, POST, etc
service = model.serviceName,
duration = model.durationNano,
Expand All @@ -46,9 +50,19 @@ internal class OtelDdSpanToSpanEventMapper(
// region internal

private fun resolveTraceId(model: DDSpan): String {
// the argument will never be negative
@Suppress("UnsafeThirdPartyFunctionCall")
return model.traceId.toHexString().takeLast(RUM_ENDPOINT_REQUIRED_TRACE_ID_LENGTH)
// because the backend endpoint does not support 32 hex characters trace ids we are going to take only the
// least significant 64 bits of the trace id. Later on when we will introduce the 128 bits support
// the most significant bits will be reported in a separated dedicated meta tag.
return model.traceId.toHexString().takeLeastSignificant64Bits()
}

private fun resolveSpanId(model: DDSpan): String {
// the span id is always 64 bits long so we can pad it with zeros
return DDSpanId.toHexStringPadded(model.spanId)
}

private fun resolveParentId(model: DDSpan): String {
return DDSpanId.toHexStringPadded(model.parentId)
}

private fun resolveMetrics(event: DDSpan): SpanEvent.Metrics {
Expand Down Expand Up @@ -90,7 +104,10 @@ internal class OtelDdSpanToSpanEventMapper(
view = event.tags[LogAttributes.RUM_VIEW_ID]?.let { SpanEvent.View(it as? String) }
)
val tags = event.tags.mapValues { it.value.toString() }
val meta = (event.baggage + tags).toMutableMap()
val meta = mutableMapOf<String, String>()
meta.putAll(event.baggage)
meta.putAll(tags)
resolveSpanLinks(event)?.let { meta[SPAN_LINKS_KEY] = it }
return SpanEvent.Meta(
version = datadogContext.version,
dd = dd,
Expand All @@ -104,6 +121,35 @@ internal class OtelDdSpanToSpanEventMapper(
)
}

private fun resolveSpanLinks(model: DDSpan): String? {
if (model.links.isEmpty()) return null
return model.links.map { resolveSpanLink(it) }.fold(JsonArray()) { acc, link ->
acc.add(link)
acc
}.toString()
}

private fun resolveSpanLink(link: AgentSpanLink): JsonObject {
// The SpanLinks support the full 128 bits trace so we can use the full hex string
val linkedTraceId = link.traceId().toHexString()
val linkedSpanId = DDSpanId.toHexStringPadded(link.spanId())
val attributes = toJson(link.attributes().asMap())
val flags = link.traceFlags()
val traceState = link.traceState()
val spanLink = JsonObject().apply {
addProperty(TRACE_ID_KEY, linkedTraceId)
addProperty(SPAN_ID_KEY, linkedSpanId)
add(ATTRIBUTES_KEY, attributes)
if (flags.toInt() != 0) {
addProperty(FLAGS_KEY, flags)
}
if (traceState.isNotEmpty()) {
addProperty(TRACE_STATE_KEY, traceState)
}
}
return spanLink
}

private fun resolveSimCarrier(networkInfo: NetworkInfo): SpanEvent.SimCarrier? {
return if (networkInfo.carrierId != null || networkInfo.carrierName != null) {
SpanEvent.SimCarrier(
Expand All @@ -122,9 +168,29 @@ internal class OtelDdSpanToSpanEventMapper(
.toMutableMap()
}

private fun String.takeLeastSignificant64Bits(): String {
// n is always positive so we can suppress the warning
@Suppress("UnsafeThirdPartyFunctionCall")
return this.takeLast(LEAST_SIGNIFICANT_64_BITS_AS_HEX_LENGTH)
}

private fun toJson(map: Map<String, String>): JsonObject {
val jsonObject = JsonObject()
map.forEach { (key, value) ->
jsonObject.addProperty(key, value)
}
return jsonObject
}

// endregion

companion object {
internal const val RUM_ENDPOINT_REQUIRED_TRACE_ID_LENGTH = 16
private const val LEAST_SIGNIFICANT_64_BITS_AS_HEX_LENGTH = 16
private const val ATTRIBUTES_KEY = "attributes"
private const val SPAN_ID_KEY = "span_id"
private const val TRACE_ID_KEY = "trace_id"
private const val TRACE_STATE_KEY = "tracestate"
private const val FLAGS_KEY = "flags"
internal const val SPAN_LINKS_KEY = "_dd.span_links"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ package com.datadog.android.trace.assertj

import com.datadog.android.api.context.NetworkInfo
import com.datadog.android.api.context.UserInfo
import com.datadog.android.trace.internal.domain.event.OtelDdSpanToSpanEventMapper
import com.datadog.android.trace.model.SpanEvent
import com.datadog.trace.api.DDSpanId
import com.datadog.trace.bootstrap.instrumentation.api.AgentSpanLink
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import org.assertj.core.api.AbstractObjectAssert
import org.assertj.core.api.Assertions.assertThat

Expand Down Expand Up @@ -187,7 +192,6 @@ internal class SpanEventAssert(actual: SpanEvent) :

fun hasMeta(attributes: Map<String, String>): SpanEventAssert {
assertThat(actual.meta.additionalProperties)
.hasSameSizeAs(attributes)
.containsAllEntriesOf(attributes)
return this
}
Expand Down Expand Up @@ -299,8 +303,128 @@ internal class SpanEventAssert(actual: SpanEvent) :
return this
}

fun hasSpanLinks(links: List<AgentSpanLink>): SpanEventAssert {
if (links.isEmpty()) {
assertThat(actual.meta.additionalProperties)
.overridingErrorMessage(
"Expected SpanEvent to not have span links but " +
"instead was: ${actual.meta.additionalProperties}"
)
.doesNotContainKey(OtelDdSpanToSpanEventMapper.SPAN_LINKS_KEY)
return this
}
val serializedLinks = actual.meta.additionalProperties[OtelDdSpanToSpanEventMapper.SPAN_LINKS_KEY]
val deserializedLinks = JsonParser.parseString(serializedLinks).asJsonArray
assertThat(deserializedLinks.size())
.overridingErrorMessage(
"Expected SpanEvent to have ${links.size} span links but " +
"instead was: ${deserializedLinks.size()}"
)
.isEqualTo(links.size)
links.forEachIndexed { index, link ->
val actualLink = deserializedLinks[index].asJsonObject
val traceId = link.traceId().toHexString()
val spanId = DDSpanId.toHexStringPadded(link.spanId())
val traceFlags = link.traceFlags()
val traceState = link.traceState()
val attributes = link.attributes().asMap()
SerializedSpanLinkAssert(actualLink)
.hasTraceId(traceId)
.hasSpanId(spanId)
.hasFlags(traceFlags)
.hasTraceState(traceState)
.hasAttributes(attributes)
}
return this
}

// endregion

private class SerializedSpanLinkAssert(actualLink: JsonObject) :
AbstractObjectAssert<SerializedSpanLinkAssert, JsonObject>(actualLink, SerializedSpanLinkAssert::class.java) {
fun hasTraceId(traceId: String): SerializedSpanLinkAssert {
val actualTraceId = actual.get("trace_id").asString
assertThat(actualTraceId)
.overridingErrorMessage(
"Expected SpanEvent to have span link trace id: " +
"$traceId but " +
"instead was: $actualTraceId"
)
.isEqualTo(traceId)
return this
}

fun hasSpanId(spanId: String): SerializedSpanLinkAssert {
val actualSpanId = actual.get("span_id").asString
assertThat(actualSpanId)
.overridingErrorMessage(
"Expected SpanEvent to have span link span id: " +
"$spanId but " +
"instead was: $$actualSpanId"
)
.isEqualTo(spanId)
return this
}

fun hasFlags(flags: Byte): SerializedSpanLinkAssert {
if (flags.toInt() != 0) {
val actualTraceFlags = actual.get("flags").asByte
assertThat(actualTraceFlags)
.overridingErrorMessage(
"Expected SpanEvent to have span link flags: " +
"$flags but " +
"instead was: $actualTraceFlags"
)
.isEqualTo(flags)
} else {
assertThat(actual.has("flags"))
.overridingErrorMessage(
"Expected SpanEvent to not have span link flags but " +
"instead was: $actual"
)
.isFalse()
}
return this
}

fun hasTraceState(traceState: String): SerializedSpanLinkAssert {
val actualTraceState = actual.get("tracestate").asString
if (actualTraceState.isNotEmpty()) {
assertThat(actualTraceState)
.overridingErrorMessage(
"Expected SpanEvent to have span link trace state: " +
"$traceState but " +
"instead was: $actualTraceState"
)
.isEqualTo(traceState)
} else {
assertThat(actual.has("tracestate"))
.overridingErrorMessage(
"Expected SpanEvent to not have span link trace state but " +
"instead was: $actual"
).isFalse()
}
return this
}

fun hasAttributes(attributes: Map<String, String>): SerializedSpanLinkAssert {
val actualAttributes = actual.get("attributes").toString()
val jsonObject = JsonObject()
attributes.forEach { (key, value) ->
jsonObject.addProperty(key, value)
}
val expectedAttributesJsonString = jsonObject.toString()
assertThat(actualAttributes)
.overridingErrorMessage(
"Expected SpanEvent to have span link attributes: " +
"$expectedAttributesJsonString but " +
"instead was: $actualAttributes"
)
.isEqualTo(expectedAttributesJsonString)
return this
}
}

companion object {
internal fun assertThat(actual: SpanEvent): SpanEventAssert {
return SpanEventAssert(actual)
Expand Down
Loading

0 comments on commit f28a8cf

Please sign in to comment.