-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implementation for OTel Span API (#906)
## 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
Showing
7 changed files
with
280 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
80 changes: 80 additions & 0 deletions
80
embrace-android-sdk/src/main/java/io/embrace/android/embracesdk/opentelemetry/EmbSpan.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
embrace-android-sdk/src/test/java/io/embrace/android/embracesdk/opentelemetry/EmbSpanTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |