Skip to content

Commit 9825312

Browse files
authored
Add support for SpanLinks (#2053)
## Goal Add basic support for SpanLinks. A couple of things still have to be finalized: - Payload shape - Limits Thought I'd get this in review first and made the approach changes after ## Testing Unit and integration tests added
2 parents 52fa657 + 1ef05a6 commit 9825312

File tree

15 files changed

+205
-31
lines changed

15 files changed

+205
-31
lines changed

embrace-android-api/api/embrace-android-api.api

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,10 @@ public abstract interface class io/embrace/android/embracesdk/spans/EmbraceSpan
263263
public abstract fun addEvent (Ljava/lang/String;)Z
264264
public abstract fun addEvent (Ljava/lang/String;Ljava/lang/Long;)Z
265265
public abstract fun addEvent (Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;)Z
266+
public abstract fun addLink (Lio/embrace/android/embracesdk/spans/EmbraceSpan;)Z
267+
public abstract fun addLink (Lio/embrace/android/embracesdk/spans/EmbraceSpan;Ljava/util/Map;)Z
268+
public abstract fun addLink (Lio/opentelemetry/api/trace/SpanContext;)Z
269+
public abstract fun addLink (Lio/opentelemetry/api/trace/SpanContext;Ljava/util/Map;)Z
266270
public abstract fun getAutoTerminationMode ()Lio/embrace/android/embracesdk/spans/AutoTerminationMode;
267271
public abstract fun getParent ()Lio/embrace/android/embracesdk/spans/EmbraceSpan;
268272
public abstract fun getSpanContext ()Lio/opentelemetry/api/trace/SpanContext;
@@ -283,6 +287,9 @@ public abstract interface class io/embrace/android/embracesdk/spans/EmbraceSpan
283287
public final class io/embrace/android/embracesdk/spans/EmbraceSpan$DefaultImpls {
284288
public static fun addEvent (Lio/embrace/android/embracesdk/spans/EmbraceSpan;Ljava/lang/String;)Z
285289
public static fun addEvent (Lio/embrace/android/embracesdk/spans/EmbraceSpan;Ljava/lang/String;Ljava/lang/Long;)Z
290+
public static fun addLink (Lio/embrace/android/embracesdk/spans/EmbraceSpan;Lio/embrace/android/embracesdk/spans/EmbraceSpan;)Z
291+
public static fun addLink (Lio/embrace/android/embracesdk/spans/EmbraceSpan;Lio/embrace/android/embracesdk/spans/EmbraceSpan;Ljava/util/Map;)Z
292+
public static fun addLink (Lio/embrace/android/embracesdk/spans/EmbraceSpan;Lio/opentelemetry/api/trace/SpanContext;)Z
286293
public static fun recordException (Lio/embrace/android/embracesdk/spans/EmbraceSpan;Ljava/lang/Throwable;)Z
287294
public static fun start (Lio/embrace/android/embracesdk/spans/EmbraceSpan;)Z
288295
public static fun stop (Lio/embrace/android/embracesdk/spans/EmbraceSpan;)Z

embrace-android-api/src/main/kotlin/io/embrace/android/embracesdk/spans/EmbraceSpan.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,33 @@ public interface EmbraceSpan {
131131
* Update the name of the span. Returns false if the update was not successful, like when it has already been stopped
132132
*/
133133
public fun updateName(newName: String): Boolean
134+
135+
/**
136+
* Add a link to the given [EmbraceSpan]
137+
*/
138+
public fun addLink(linkedSpan: EmbraceSpan): Boolean = addLink(linkedSpan = linkedSpan, attributes = null)
139+
140+
/**
141+
* Add a link to the span with the given [SpanContext]
142+
*/
143+
public fun addLink(
144+
linkedSpanContext: SpanContext
145+
): Boolean = addLink(linkedSpanContext = linkedSpanContext, attributes = null)
146+
147+
/**
148+
* Add a link to the given [EmbraceSpan] with the given attributes
149+
*/
150+
public fun addLink(linkedSpan: EmbraceSpan, attributes: Map<String, String>?): Boolean {
151+
val spanContext = linkedSpan.spanContext
152+
return if (spanContext != null) {
153+
addLink(linkedSpanContext = spanContext, attributes = attributes)
154+
} else {
155+
false
156+
}
157+
}
158+
159+
/**
160+
* Add a link to the span with the given [SpanContext] with the given attributes
161+
*/
162+
public fun addLink(linkedSpanContext: SpanContext, attributes: Map<String, String>?): Boolean
134163
}

embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/gating/SpanSanitizer.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ internal class SpanSanitizer(
3434
sessionSpan.endTimeNanos,
3535
sessionSpan.status,
3636
sanitizedEvents,
37-
sessionSpan.attributes
37+
sessionSpan.attributes,
38+
sessionSpan.links,
3839
)
3940
sanitizedSpans.add(sanitizedSessionSpan)
4041
return sanitizedSpans

embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/opentelemetry/EmbSpan.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ class EmbSpan(
3838
return this
3939
}
4040

41+
override fun addLink(spanContext: SpanContext, attributes: Attributes): Span {
42+
embraceSpan.addLink(spanContext, attributes.toStringMap())
43+
return this
44+
}
45+
4146
override fun setStatus(statusCode: StatusCode, description: String): Span {
4247
if (isRecording) {
4348
embraceSpan.setStatus(statusCode, description)

embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/payload/SpanMapper.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.embrace.android.embracesdk.internal.arch.schema.EmbType
55
import io.embrace.android.embracesdk.internal.arch.schema.ErrorCodeAttribute
66
import io.embrace.android.embracesdk.internal.clock.millisToNanos
77
import io.embrace.android.embracesdk.internal.clock.nanosToMillis
8+
import io.embrace.android.embracesdk.internal.spans.EmbraceLinkData
89
import io.embrace.android.embracesdk.internal.spans.EmbraceSpanData
910
import io.embrace.android.embracesdk.internal.spans.hasFixedAttribute
1011
import io.embrace.android.embracesdk.internal.spans.setFixedAttribute
@@ -22,7 +23,8 @@ fun EmbraceSpanData.toNewPayload(): Span = Span(
2223
endTimeNanos = endTimeNanos,
2324
status = status.toStatus(),
2425
events = events.map(EmbraceSpanEvent::toNewPayload),
25-
attributes = attributes.toNewPayload()
26+
attributes = attributes.toNewPayload(),
27+
links = links,
2628
)
2729

2830
fun EmbraceSpanEvent.toNewPayload(): SpanEvent = SpanEvent(
@@ -43,6 +45,8 @@ fun Map<String, String>.toNewPayload(): List<Attribute> =
4345
fun List<Attribute>.toOldPayload(): Map<String, String> =
4446
associate { Pair(it.key ?: "", it.data ?: "") }.filterKeys { it.isNotBlank() }
4547

48+
fun EmbraceLinkData.toPayload() = Link(spanContext.spanId, attributes?.toNewPayload())
49+
4650
fun Span.toOldPayload(): EmbraceSpanData {
4751
return EmbraceSpanData(
4852
traceId = traceId ?: "",
@@ -58,7 +62,8 @@ fun Span.toOldPayload(): EmbraceSpanData {
5862
else -> StatusCode.UNSET
5963
},
6064
events = events?.mapNotNull { it.toOldPayload() } ?: emptyList(),
61-
attributes = attributes?.toOldPayload() ?: emptyMap()
65+
attributes = attributes?.toOldPayload() ?: emptyMap(),
66+
links = links ?: emptyList()
6267
)
6368
}
6469

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io.embrace.android.embracesdk.internal.spans
2+
3+
import io.opentelemetry.api.trace.SpanContext
4+
5+
data class EmbraceLinkData(
6+
val spanContext: SpanContext,
7+
val attributes: Map<String, String>?
8+
)

embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/spans/EmbraceSpanData.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.embrace.android.embracesdk.internal.spans
22

33
import io.embrace.android.embracesdk.internal.clock.nanosToMillis
4+
import io.embrace.android.embracesdk.internal.payload.Link
45
import io.embrace.android.embracesdk.spans.EmbraceSpanEvent
56
import io.opentelemetry.api.trace.StatusCode
67
import io.opentelemetry.sdk.trace.data.EventData
@@ -26,6 +27,8 @@ data class EmbraceSpanData(
2627
val events: List<EmbraceSpanEvent> = emptyList(),
2728

2829
val attributes: Map<String, String> = emptyMap(),
30+
31+
val links: List<Link> = emptyList(),
2932
) {
3033

3134
companion object {

embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/spans/EmbraceSpanImpl.kt

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import io.embrace.android.embracesdk.internal.config.instrumented.schema.OtelLim
1616
import io.embrace.android.embracesdk.internal.payload.Attribute
1717
import io.embrace.android.embracesdk.internal.payload.Span
1818
import io.embrace.android.embracesdk.internal.payload.toNewPayload
19+
import io.embrace.android.embracesdk.internal.payload.toPayload
1920
import io.embrace.android.embracesdk.internal.utils.truncatedStacktraceText
2021
import io.embrace.android.embracesdk.spans.AutoTerminationMode
2122
import io.embrace.android.embracesdk.spans.EmbraceSpan
@@ -64,6 +65,7 @@ internal class EmbraceSpanImpl(
6465
private val customAttributes = ConcurrentHashMap<String, String>().apply {
6566
putAll(spanBuilder.getCustomAttributes())
6667
}
68+
private val systemLinks = ConcurrentLinkedQueue<EmbraceLinkData>()
6769

6870
// size for ConcurrentLinkedQueues is not a constant operation, so it could be subject to race conditions
6971
// do the bookkeeping separately so we don't have to worry about this
@@ -127,29 +129,9 @@ internal class EmbraceSpanImpl(
127129
}
128130

129131
startedSpan.get()?.let { spanToStop ->
130-
systemAttributes.forEach { systemAttribute ->
131-
spanToStop.setAttribute(systemAttribute.key, systemAttribute.value)
132-
}
133-
customAttributes.redactIfSensitive().forEach { attribute ->
134-
spanToStop.setAttribute(attribute.key, attribute.value)
135-
}
136-
137-
val redactedCustomEvents = customEvents.map { it.copy(attributes = it.attributes.redactIfSensitive()) }
138-
139-
(systemEvents + redactedCustomEvents).forEach { event ->
140-
val eventAttributes = if (event.attributes.isNotEmpty()) {
141-
Attributes.builder().fromMap(event.attributes, spanBuilder.internal).build()
142-
} else {
143-
Attributes.empty()
144-
}
145-
146-
spanToStop.addEvent(
147-
event.name,
148-
eventAttributes,
149-
event.timestampNanos,
150-
TimeUnit.NANOSECONDS
151-
)
152-
}
132+
populateAttributes(spanToStop)
133+
populateEvents(spanToStop)
134+
populateLinks(spanToStop)
153135

154136
if (errorCode != null) {
155137
setStatus(StatusCode.ERROR)
@@ -171,6 +153,17 @@ internal class EmbraceSpanImpl(
171153
return successful
172154
}
173155

156+
private fun populateLinks(spanToStop: io.opentelemetry.api.trace.Span) {
157+
systemLinks.forEach {
158+
val linkAttributes = if (it.attributes != null) {
159+
Attributes.builder().fromMap(attributes = it.attributes, false).build()
160+
} else {
161+
Attributes.empty()
162+
}
163+
spanToStop.addLink(it.spanContext, linkAttributes)
164+
}
165+
}
166+
174167
override fun addEvent(name: String, timestampMs: Long?, attributes: Map<String, String>?): Boolean =
175168
recordEvent(customEvents, customEventCount, limits.getMaxCustomEventCount()) {
176169
EmbraceSpanEvent.create(
@@ -268,6 +261,20 @@ internal class EmbraceSpanImpl(
268261
return false
269262
}
270263

264+
override fun addLink(linkedSpanContext: SpanContext, attributes: Map<String, String>?): Boolean {
265+
if (systemLinks.size < limits.getMaxTotalLinkCount()) {
266+
synchronized(systemLinks) {
267+
if (systemLinks.size < limits.getMaxTotalLinkCount()) {
268+
systemLinks.add(EmbraceLinkData(linkedSpanContext, attributes))
269+
spanRepository.notifySpanUpdate()
270+
return true
271+
}
272+
}
273+
}
274+
275+
return false
276+
}
277+
271278
override fun asNewContext(): Context? = startedSpan.get()?.run { spanBuilder.parentContext.with(this) }
272279

273280
override fun snapshot(): Span? {
@@ -282,7 +289,8 @@ internal class EmbraceSpanImpl(
282289
endTimeNanos = spanEndTimeMs?.millisToNanos(),
283290
status = status,
284291
events = systemEvents.map(EmbraceSpanEvent::toNewPayload) + redactedCustomEvents.map(EmbraceSpanEvent::toNewPayload),
285-
attributes = getAttributesPayload()
292+
attributes = getAttributesPayload(),
293+
links = systemLinks.toList().map { it.toPayload() }
286294
)
287295
} else {
288296
null
@@ -348,4 +356,32 @@ internal class EmbraceSpanImpl(
348356
}
349357
}
350358
}
359+
360+
private fun populateAttributes(spanToStop: io.opentelemetry.api.trace.Span) {
361+
systemAttributes.forEach { systemAttribute ->
362+
spanToStop.setAttribute(systemAttribute.key, systemAttribute.value)
363+
}
364+
customAttributes.redactIfSensitive().forEach { attribute ->
365+
spanToStop.setAttribute(attribute.key, attribute.value)
366+
}
367+
}
368+
369+
private fun populateEvents(spanToStop: io.opentelemetry.api.trace.Span) {
370+
val redactedCustomEvents = customEvents.map { it.copy(attributes = it.attributes.redactIfSensitive()) }
371+
372+
(systemEvents + redactedCustomEvents).forEach { event ->
373+
val eventAttributes = if (event.attributes.isNotEmpty()) {
374+
Attributes.builder().fromMap(event.attributes, spanBuilder.internal).build()
375+
} else {
376+
Attributes.empty()
377+
}
378+
379+
spanToStop.addEvent(
380+
event.name,
381+
eventAttributes,
382+
event.timestampNanos,
383+
TimeUnit.NANOSECONDS
384+
)
385+
}
386+
}
351387
}

embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/spans/SpanDataExt.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.embrace.android.embracesdk.internal.spans
22

3+
import io.embrace.android.embracesdk.internal.payload.Link
4+
import io.embrace.android.embracesdk.internal.payload.toNewPayload
35
import io.embrace.android.embracesdk.internal.spans.EmbraceSpanData.Companion.fromEventData
46
import io.opentelemetry.sdk.trace.data.SpanData
57

@@ -13,4 +15,5 @@ fun SpanData.toEmbraceSpanData(): EmbraceSpanData = EmbraceSpanData(
1315
status = status.statusCode,
1416
events = fromEventData(eventDataList = events),
1517
attributes = attributes.toStringMap(),
18+
links = links.map { Link(it.spanContext.spanId, it.attributes.toNewPayload()) }
1619
)

embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/opentelemetry/EmbSpanTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,18 @@ internal class EmbSpanTest {
176176

177177
assertEquals(attributesCount + 10, fakeEmbraceSpan.attributes.size)
178178
}
179+
180+
@Test
181+
fun `add span link`() {
182+
with(embSpan) {
183+
val linkedSpanContext = checkNotNull(FakePersistableEmbraceSpan.started().spanContext)
184+
addLink(linkedSpanContext, Attributes.builder().put("boolean", true).build())
185+
with(fakeEmbraceSpan.links.single()) {
186+
assertEquals(linkedSpanContext.spanId, spanId)
187+
val attribute = checkNotNull(attributes?.single())
188+
assertEquals("boolean", attribute.key)
189+
assertEquals("true", attribute.data)
190+
}
191+
}
192+
}
179193
}

0 commit comments

Comments
 (0)