Skip to content

Commit

Permalink
Implementation for OTel Span API (#906)
Browse files Browse the repository at this point in the history
## Goal

Create `EmbSpan`, a wrapper implementation of the OTel Span API object that will call into `EmbraceSpan` in order to do span operations. 

The current architecture is not ideal, as we have an Embrace OTel API implementation that wraps an Embrace Span API implementation that in turn wraps the official Java SDK Span API implementation. Ideally, all the Embrace-specific logic exists in Embrace OTel API implementation, and the Embrace Span API implementation just wraps around that. But that is a much bigger change, and will likely require refactoring the builder objects as well, so I just want to get this to work first before doing that heavy lifting.

Note: I did not implement the ability to add SpanLinks and a status description , as those are features that are not surfaced in the Embrace implementation. We can look to implement this in the future.

## Testing

Unit test on `EmbSpan` to test the entire API surface.
  • Loading branch information
bidetofevil authored May 30, 2024
2 parents e944cba + 4a3f045 commit 1316add
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 16 deletions.
1 change: 1 addition & 0 deletions embrace-android-sdk/api/embrace-android-sdk.api
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ public abstract interface class io/embrace/android/embracesdk/spans/EmbraceSpan
public abstract fun addEvent (Ljava/lang/String;Ljava/lang/Long;)Z
public abstract fun addEvent (Ljava/lang/String;Ljava/lang/Long;Ljava/util/Map;)Z
public abstract fun getParent ()Lio/embrace/android/embracesdk/spans/EmbraceSpan;
public abstract fun getSpanContext ()Lio/opentelemetry/api/trace/SpanContext;
public abstract fun getSpanId ()Ljava/lang/String;
public abstract fun getTraceId ()Ljava/lang/String;
public abstract fun isRecording ()Z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import io.embrace.android.embracesdk.spans.EmbraceSpanEvent.Companion.inputsVali
import io.embrace.android.embracesdk.spans.ErrorCode
import io.embrace.android.embracesdk.spans.PersistableEmbraceSpan
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.trace.SpanContext
import io.opentelemetry.sdk.common.Clock
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
Expand Down Expand Up @@ -48,11 +49,14 @@ internal class EmbraceSpanImpl(

override val parent: EmbraceSpan? = spanBuilder.parent

override val spanContext: SpanContext?
get() = startedSpan.get()?.spanContext

override val traceId: String?
get() = startedSpan.get()?.spanContext?.traceId
get() = spanContext?.traceId

override val spanId: String?
get() = startedSpan.get()?.spanContext?.spanId
get() = spanContext?.spanId

override val isRecording: Boolean
get() = startedSpan.get()?.isRecording == true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package io.embrace.android.embracesdk.opentelemetry

import io.embrace.android.embracesdk.internal.spans.toStringMap
import io.embrace.android.embracesdk.spans.EmbraceSpan
import io.embrace.android.embracesdk.spans.ErrorCode
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.trace.Span
import io.opentelemetry.api.trace.SpanContext
import io.opentelemetry.api.trace.StatusCode
import io.opentelemetry.sdk.common.Clock
import java.util.concurrent.TimeUnit

internal class EmbSpan(
private val embraceSpan: EmbraceSpan,
private val clock: Clock
) : Span {

private var spanStatus: StatusCode = StatusCode.UNSET
private var spanStatusDescription: String? = null

override fun <T : Any> setAttribute(key: AttributeKey<T>, value: T): Span {
embraceSpan.addAttribute(key = key.key, value = value.toString())
return this
}

override fun addEvent(name: String, attributes: Attributes): Span = addEvent(
name = name,
attributes = attributes,
timestamp = clock.now(),
unit = TimeUnit.NANOSECONDS
)

override fun addEvent(name: String, attributes: Attributes, timestamp: Long, unit: TimeUnit): Span {
embraceSpan.addEvent(
name = name,
timestampMs = unit.toMillis(timestamp),
attributes = attributes.toStringMap()
)
return this
}

override fun setStatus(statusCode: StatusCode, description: String): Span {
spanStatus = statusCode
spanStatusDescription = description
return this
}

override fun recordException(exception: Throwable, additionalAttributes: Attributes): Span {
embraceSpan.recordException(exception, additionalAttributes.toStringMap())
return this
}

override fun updateName(name: String): Span {
embraceSpan.updateName(name)
return this
}

override fun end() = end(timestamp = clock.now(), unit = TimeUnit.NANOSECONDS)

/**
* One difference between the implementation of this method and the equivalent implementation in the Java SDK is that [StatusCode] for
* the underlying span is set to [StatusCode.OK] automatically if this is called before [setStatus] is called.
*/
override fun end(timestamp: Long, unit: TimeUnit) {
val endTimeMs = unit.toMillis(timestamp)
when (spanStatus) {
StatusCode.ERROR -> {
embraceSpan.stop(errorCode = ErrorCode.FAILURE, endTimeMs = endTimeMs)
}
else -> {
embraceSpan.stop(endTimeMs = endTimeMs)
}
}
}

override fun getSpanContext(): SpanContext = embraceSpan.spanContext ?: SpanContext.getInvalid()

override fun isRecording(): Boolean = embraceSpan.isRecording
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ package io.embrace.android.embracesdk.spans

import io.embrace.android.embracesdk.annotation.BetaApi
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.trace.SpanContext

/**
* Represents a Span that can be started and stopped with the appropriate [ErrorCode] if applicable. This wraps the OpenTelemetry Span
* by adding an additional layer for local validation
*/
@BetaApi
public interface EmbraceSpan {
/**
* The [SpanContext] for this [EmbraceSpan] instance. This is null if the span has not been started.
*/
public val spanContext: SpanContext?

/**
* ID of the Trace that this Span belongs to. The format adheres to the OpenTelemetry standard for Trace IDs
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ internal class FakeClock(
this.currentTime = currentTime
}

@JvmOverloads
fun tick(millis: Long = 1) {
fun tick(millis: Long = 1): Long {
currentTime += millis
return currentTime
}

fun tickSecond() = tick(1000)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ import io.embrace.android.embracesdk.spans.EmbraceSpan
import io.embrace.android.embracesdk.spans.EmbraceSpanEvent
import io.embrace.android.embracesdk.spans.ErrorCode
import io.embrace.android.embracesdk.spans.PersistableEmbraceSpan
import io.opentelemetry.api.trace.SpanContext
import io.opentelemetry.sdk.trace.IdGenerator
import java.util.concurrent.ConcurrentLinkedQueue

internal class FakePersistableEmbraceSpan(
override val parent: EmbraceSpan?,
val name: String? = null,
var name: String? = null,
val type: TelemetryType = EmbType.Performance.Default,
val internal: Boolean = false,
val private: Boolean = internal,
Expand All @@ -33,9 +34,11 @@ internal class FakePersistableEmbraceSpan(
var stopped = false
var errorCode: ErrorCode? = null

private var spanStartTimeMs: Long? = null
private var spanEndTimeMs: Long? = null
private var status = Span.Status.UNSET
var spanStartTimeMs: Long? = null
var spanEndTimeMs: Long? = null
var status = Span.Status.UNSET

override val spanContext: SpanContext? = null

override var traceId: String? = parent?.traceId

Expand Down Expand Up @@ -72,7 +75,7 @@ internal class FakePersistableEmbraceSpan(
return true
}

override fun addEvent(name: String): Boolean = addEvent(name)
override fun addEvent(name: String): Boolean = addEvent(name, null, null)

override fun addEvent(name: String, timestampMs: Long?, attributes: Map<String, String>?): Boolean {
events.add(
Expand All @@ -99,7 +102,8 @@ internal class FakePersistableEmbraceSpan(
}

override fun updateName(newName: String): Boolean {
TODO("Not yet implemented")
name = newName
return true
}

override fun snapshot(): Span? {
Expand Down Expand Up @@ -128,25 +132,28 @@ internal class FakePersistableEmbraceSpan(
override fun removeCustomAttribute(key: String): Boolean = attributes.remove(key) != null

companion object {
fun notStarted(parent: EmbraceSpan? = null): FakePersistableEmbraceSpan =
fun notStarted(parent: EmbraceSpan? = null, clock: FakeClock = FakeClock()): FakePersistableEmbraceSpan =
FakePersistableEmbraceSpan(
parent = parent,
name = "not-started"
name = "not-started",
fakeClock = clock,
)

fun started(parent: EmbraceSpan? = null): FakePersistableEmbraceSpan {
fun started(parent: EmbraceSpan? = null, clock: FakeClock = FakeClock()): FakePersistableEmbraceSpan {
val span = FakePersistableEmbraceSpan(
parent = parent,
name = "started"
name = "started",
fakeClock = clock,
)
span.start()
return span
}

fun stopped(parent: EmbraceSpan? = null): FakePersistableEmbraceSpan {
fun stopped(parent: EmbraceSpan? = null, clock: FakeClock = FakeClock()): FakePersistableEmbraceSpan {
val span = FakePersistableEmbraceSpan(
parent = parent,
name = "stopped"
name = "stopped",
fakeClock = clock,
)
span.start()
span.stop()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package io.embrace.android.embracesdk.opentelemetry

import io.embrace.android.embracesdk.arch.schema.ErrorCodeAttribute
import io.embrace.android.embracesdk.fakes.FakeClock
import io.embrace.android.embracesdk.fakes.FakePersistableEmbraceSpan
import io.embrace.android.embracesdk.fakes.injection.FakeInitModule
import io.embrace.android.embracesdk.internal.payload.Span
import io.embrace.android.embracesdk.internal.spans.EmbraceSpanImpl.Companion.EXCEPTION_EVENT_NAME
import io.embrace.android.embracesdk.internal.spans.hasFixedAttribute
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.trace.StatusCode
import io.opentelemetry.sdk.common.Clock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import java.util.concurrent.TimeUnit

internal class EmbSpanTest {
private lateinit var fakeClock: FakeClock
private lateinit var openTelemetryClock: Clock
private lateinit var fakeEmbraceSpan: FakePersistableEmbraceSpan
private lateinit var embSpan: EmbSpan

@Before
fun setup() {
fakeClock = FakeClock()
openTelemetryClock = FakeInitModule(fakeClock).openTelemetryClock
fakeEmbraceSpan = FakePersistableEmbraceSpan.started(clock = fakeClock)
embSpan = EmbSpan(
embraceSpan = fakeEmbraceSpan,
clock = openTelemetryClock
)
}

@Test
fun `validate started and stopped span`() {
assertNotNull(embSpan.spanContext)
assertTrue(embSpan.isRecording)
with(fakeEmbraceSpan) {
assertEquals(fakeClock.now(), spanStartTimeMs)
assertNull(spanEndTimeMs)
assertEquals(status, Span.Status.UNSET)
}
val stopTime = fakeClock.tick()
embSpan.end()
assertFalse(embSpan.isRecording)
with(fakeEmbraceSpan) {
assertEquals(stopTime, spanEndTimeMs)
assertEquals(status, Span.Status.OK)
}
}

@Test
fun `specific end time used`() {
with(embSpan) {
val stopTimeSeconds = TimeUnit.MILLISECONDS.toSeconds(fakeClock.tickSecond())
end(stopTimeSeconds, TimeUnit.SECONDS)
assertFalse(isRecording)
assertEquals(TimeUnit.SECONDS.toNanos(stopTimeSeconds), fakeEmbraceSpan.snapshot()?.endTimeUnixNano)
}
}

@Test
fun `set error status before end`() {
with(embSpan) {
setStatus(StatusCode.ERROR, "error")
end()
}
with(fakeEmbraceSpan) {
assertEquals(status, Span.Status.ERROR)
assertTrue(attributes.hasFixedAttribute(ErrorCodeAttribute.Failure))
}
}

@Test
fun `check adding events`() {
val attributesBuilder =
Attributes
.builder()
.put("boolean", true)
.put("integer", 1)
.put("long", 2L)
.put("double", 3.0)
.put("string", "value")
.put("booleanArray", true, false)
.put("integerArray", 1, 2)
.put("longArray", 2L, 3L)
.put("doubleArray", 3.0, 4.0)
.put("stringArray", "value", "vee")

val event1Time = openTelemetryClock.now()
embSpan.addEvent("event1")
fakeClock.tick(1)
val event2Time = openTelemetryClock.now()
embSpan.addEvent("event2", attributesBuilder.build())
with(checkNotNull(fakeEmbraceSpan.events)) {
assertEquals(2, size)
with(first()) {
assertEquals("event1", name)
assertEquals(event1Time, timestampNanos)
assertEquals(0, attributes.size)
}

with(last()) {
assertEquals("event2", name)
assertEquals(event2Time, timestampNanos)
assertEquals(10, attributes.size)
}
}
}

@Test
fun `span name update`() {
with(embSpan) {
updateName("new-name")
assertEquals("new-name", fakeEmbraceSpan.name)
}
}

@Test
fun `recording exceptions as span events`() {
val firstExceptionTime = openTelemetryClock.now()
embSpan.recordException(IllegalStateException())
val secondExceptionTime = openTelemetryClock.now()
embSpan.recordException(RuntimeException(), Attributes.builder().put("myKey", "myValue").build())

with(checkNotNull(fakeEmbraceSpan.events)) {
assertEquals(2, size)
with(first()) {
assertEquals(EXCEPTION_EVENT_NAME, name)
assertEquals(firstExceptionTime, timestampNanos)
assertEquals(0, attributes.size)
}

with(last()) {
assertEquals(EXCEPTION_EVENT_NAME, name)
assertEquals(secondExceptionTime, timestampNanos)
assertEquals(1, attributes.size)
}
}
}

@Test
fun `check adding and removing custom attributes`() {
val attributesCount = fakeEmbraceSpan.attributes.size
with(embSpan) {
setAttribute("boolean", true)
setAttribute("integer", 1)
setAttribute("long", 2L)
setAttribute("double", 3.0)
setAttribute("string", "value")
setAttribute(AttributeKey.booleanArrayKey("booleanArray"), listOf(true, false))
setAttribute(AttributeKey.longArrayKey("integerArray"), listOf(1, 2))
setAttribute(AttributeKey.longArrayKey("longArray"), listOf(2L, 3L))
setAttribute(AttributeKey.doubleArrayKey("doubleArray"), listOf(3.0, 4.0))
setAttribute(AttributeKey.stringArrayKey("stringArray"), listOf("value", "vee"))
}

assertEquals(attributesCount + 10, fakeEmbraceSpan.attributes.size)
}
}

0 comments on commit 1316add

Please sign in to comment.