Skip to content

Commit 78a622d

Browse files
authored
Add session.id attribute to spans to denote starting session (#2310)
## Goal Add `session.id` attribute to all spans that corresponds to the session in which they were started in
2 parents a3a684b + fec65b7 commit 78a622d

File tree

12 files changed

+97
-15
lines changed

12 files changed

+97
-15
lines changed

embrace-android-core/src/main/kotlin/io/embrace/android/embracesdk/internal/injection/OpenTelemetryModuleImpl.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ internal class OpenTelemetryModuleImpl(
5252
sdkName = BuildConfig.LIBRARY_PACKAGE_NAME,
5353
sdkVersion = BuildConfig.VERSION_NAME,
5454
systemInfo = initModule.systemInfo,
55+
sessionIdProvider = { currentSessionSpan.getSessionId() },
5556
processIdentifierProvider = initModule.processIdentifierProvider
5657
)
5758
}

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,15 @@ internal class CurrentSessionSpanImpl(
103103
currentSessionSpan?.addSystemLink(spanToStopContext, LinkType.EndedIn)
104104
}
105105

106-
currentSessionSpan?.spanContext?.let { sessionSpanContext ->
107-
spanToStop?.addSystemLink(sessionSpanContext, LinkType.EndSession)
106+
val sessionId = currentSessionSpan?.getSystemAttribute(SessionIncubatingAttributes.SESSION_ID.key)
107+
if (sessionId != null) {
108+
currentSessionSpan.spanContext?.let { sessionSpanContext ->
109+
spanToStop?.addSystemLink(
110+
linkedSpanContext = sessionSpanContext,
111+
type = LinkType.EndSession,
112+
attributes = mapOf(SessionIncubatingAttributes.SESSION_ID.key to sessionId)
113+
)
114+
}
108115
}
109116
}
110117
}

embrace-android-core/src/test/java/io/embrace/android/embracesdk/internal/spans/CurrentSessionSpanImplTests.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import io.embrace.android.embracesdk.spans.ErrorCode
3434
import io.embrace.opentelemetry.kotlin.ExperimentalApi
3535
import io.embrace.opentelemetry.kotlin.aliases.OtelJavaClock
3636
import io.embrace.opentelemetry.kotlin.tracing.Tracer
37+
import io.opentelemetry.semconv.incubating.SessionIncubatingAttributes
3738
import org.junit.Assert.assertEquals
3839
import org.junit.Assert.assertFalse
3940
import org.junit.Assert.assertNotEquals
@@ -569,14 +570,19 @@ internal class CurrentSessionSpanImplTests {
569570
@Test
570571
fun `span stop callback creates the correct span links`() {
571572
val sessionSpan = checkNotNull(spanRepository.getActiveSpans().single())
573+
val sessionId = checkNotNull(sessionSpan.getSystemAttribute(SessionIncubatingAttributes.SESSION_ID.key))
572574
val span = spanService.startSpan("test")?.apply {
573575
stop()
574576
}
575577

576578
val spanSnapshot = checkNotNull(span?.snapshot())
577579
val sessionSpanSnapshot = checkNotNull(sessionSpan.snapshot())
578580

579-
checkNotNull(spanSnapshot.links).single().validateSystemLink(sessionSpanSnapshot, LinkType.EndSession)
581+
checkNotNull(spanSnapshot.links).single().validateSystemLink(
582+
linkedSpan = sessionSpanSnapshot,
583+
type = LinkType.EndSession,
584+
expectedAttributes = mapOf(SessionIncubatingAttributes.SESSION_ID.key to sessionId)
585+
)
580586
checkNotNull(sessionSpanSnapshot.links).single().validateSystemLink(spanSnapshot, LinkType.EndedIn)
581587
}
582588

embrace-android-otel/src/main/kotlin/io/embrace/android/embracesdk/internal/otel/config/OtelSdkConfig.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class OtelSdkConfig(
3030
val sdkName: String,
3131
val sdkVersion: String,
3232
systemInfo: SystemInfo,
33+
private val sessionIdProvider: () -> String? = { null },
3334
private val processIdentifierProvider: () -> String = IdGenerator.Companion::generateLaunchInstanceId,
3435
) {
3536
val otelJavaResourceBuilder: OtelJavaResourceBuilder = OtelJavaResource.getDefault().toBuilder()
@@ -94,6 +95,7 @@ class OtelSdkConfig(
9495
val otelJavaSpanProcessor: OtelJavaSpanProcessor by lazy {
9596
EmbraceOtelJavaSpanProcessor(
9697
otelJavaSpanExporter,
98+
sessionIdProvider,
9799
processIdentifier
98100
)
99101
}

embrace-android-otel/src/main/kotlin/io/embrace/android/embracesdk/internal/otel/spans/EmbraceOtelJavaSpanProcessor.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ import io.embrace.opentelemetry.kotlin.aliases.OtelJavaReadWriteSpan
88
import io.embrace.opentelemetry.kotlin.aliases.OtelJavaReadableSpan
99
import io.embrace.opentelemetry.kotlin.aliases.OtelJavaSpanExporter
1010
import io.embrace.opentelemetry.kotlin.aliases.OtelJavaSpanProcessor
11+
import io.opentelemetry.semconv.incubating.SessionIncubatingAttributes
1112
import java.util.concurrent.atomic.AtomicLong
1213

13-
/**
14-
* [SpanProcessor] that adds custom attributes to a [Span] when it starts, and exports it to the given [SpanExporter] when it finishes
15-
*/
1614
class EmbraceOtelJavaSpanProcessor(
1715
private val spanExporter: OtelJavaSpanExporter,
16+
private val sessionIdProvider: () -> String?,
1817
private val processIdentifier: String,
1918
) : OtelJavaSpanProcessor {
2019

@@ -23,6 +22,9 @@ class EmbraceOtelJavaSpanProcessor(
2322
override fun onStart(parentContext: OtelJavaContext, span: OtelJavaReadWriteSpan) {
2423
span.setEmbraceAttribute(embSequenceId, counter.getAndIncrement().toString())
2524
span.setEmbraceAttribute(embProcessIdentifier, processIdentifier)
25+
sessionIdProvider()?.let { sessionId ->
26+
span.setAttribute(SessionIncubatingAttributes.SESSION_ID, sessionId)
27+
}
2628
}
2729

2830
override fun onEnd(span: OtelJavaReadableSpan) {

embrace-android-otel/src/test/kotlin/io/embrace/android/embracesdk/internal/otel/spans/EmbraceSpanServiceTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ internal class EmbraceSpanServiceTest {
4545
sdkName = "test-sdk",
4646
sdkVersion = "1.0",
4747
systemInfo = SystemInfo(),
48+
sessionIdProvider = { "fake-session-id" },
4849
processIdentifierProvider = { "fake-pid" }
4950
)
5051
val otelSdkWrapper = OtelSdkWrapper(

embrace-android-otel/src/test/kotlin/io/embrace/android/embracesdk/internal/otel/spans/SpanServiceImplTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ internal class SpanServiceImplTest {
533533

534534
val completedSpans = spanSink.completedSpans()
535535
assertEquals(1, completedSpans.size)
536-
assertEquals(48, completedSpans[0].attributes.filterNot { it.key.startsWith("emb.") }.size)
536+
assertEquals(49, completedSpans[0].attributes.filterNot { it.key.startsWith("emb.") }.size)
537537
}
538538

539539
@Test
@@ -610,6 +610,7 @@ internal class SpanServiceImplTest {
610610
sdkName = "test-sdk",
611611
sdkVersion = "1.0",
612612
systemInfo = SystemInfo(),
613+
sessionIdProvider = { "fake-session-id" },
613614
processIdentifierProvider = { "fake-pid" }
614615
)
615616
val otelSdkWrapper = OtelSdkWrapper(

embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testcases/TracingApiTest.kt

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.embrace.android.embracesdk.arch.assertIsTypePerformance
55
import io.embrace.android.embracesdk.assertions.assertEmbraceSpanData
66
import io.embrace.android.embracesdk.assertions.findCustomLinks
77
import io.embrace.android.embracesdk.assertions.findSpanByName
8+
import io.embrace.android.embracesdk.assertions.getSessionId
89
import io.embrace.android.embracesdk.assertions.hasLinkToEmbraceSpan
910
import io.embrace.android.embracesdk.assertions.isLinkedToSpanContext
1011
import io.embrace.android.embracesdk.assertions.validateLinkToSpan
@@ -20,6 +21,7 @@ import io.embrace.android.embracesdk.internal.clock.millisToNanos
2021
import io.embrace.android.embracesdk.internal.otel.payload.toEmbracePayload
2122
import io.embrace.android.embracesdk.internal.otel.schema.EmbType
2223
import io.embrace.android.embracesdk.internal.otel.schema.LinkType
24+
import io.embrace.android.embracesdk.internal.otel.sdk.findAttributeValue
2325
import io.embrace.android.embracesdk.internal.otel.sdk.id.OtelIds
2426
import io.embrace.android.embracesdk.internal.otel.sdk.toEmbraceSpanData
2527
import io.embrace.android.embracesdk.internal.otel.spans.EmbraceSpanData
@@ -37,6 +39,7 @@ import io.embrace.opentelemetry.kotlin.aliases.OtelJavaContext
3739
import io.embrace.opentelemetry.kotlin.aliases.OtelJavaSpanContext
3840
import io.embrace.opentelemetry.kotlin.aliases.OtelJavaTraceFlags
3941
import io.embrace.opentelemetry.kotlin.aliases.OtelJavaTraceState
42+
import io.opentelemetry.semconv.incubating.SessionIncubatingAttributes
4043
import org.junit.Assert.assertEquals
4144
import org.junit.Assert.assertNotEquals
4245
import org.junit.Assert.assertNotNull
@@ -216,15 +219,17 @@ internal class TracingApiTest {
216219
timestampNanos = (testStartTimeMs + 250).millisToNanos(),
217220
attributes = emptyList()
218221
),
219-
)
222+
),
223+
expectedSessionId = session.getSessionId()
220224
)
221225
val expectedParentId = traceRootSpan.spanId
222226
assertEmbraceSpanData(
223227
span = spansMap["record-span-span"],
224228
expectedStartTimeMs = testStartTimeMs,
225229
expectedEndTimeMs = testStartTimeMs + 100,
226230
expectedParentId = checkNotNull(expectedParentId),
227-
expectedTraceId = traceRootSpan.traceId
231+
expectedTraceId = traceRootSpan.traceId,
232+
expectedSessionId = session.getSessionId()
228233
)
229234

230235
assertEmbraceSpanData(
@@ -241,30 +246,34 @@ internal class TracingApiTest {
241246
timestampNanos = (testStartTimeMs + 300).millisToNanos(),
242247
attributes = listOf(Attribute("retry", "1"))
243248
)
244-
)
249+
),
250+
expectedSessionId = session.getSessionId()
245251
)
246252

247253
assertEmbraceSpanData(
248254
span = spansMap["bonus-span"],
249255
expectedStartTimeMs = testStartTimeMs + 300,
250256
expectedEndTimeMs = testStartTimeMs + 301,
251257
expectedParentId = expectedParentId,
252-
expectedTraceId = traceRootSpan.traceId
258+
expectedTraceId = traceRootSpan.traceId,
259+
expectedSessionId = session.getSessionId()
253260
)
254261

255262
assertEmbraceSpanData(
256263
span = spansMap["bonus-span-2"],
257264
expectedStartTimeMs = testStartTimeMs + 310,
258265
expectedEndTimeMs = testStartTimeMs + 600,
259266
expectedParentId = expectedParentId,
260-
expectedTraceId = traceRootSpan.traceId
267+
expectedTraceId = traceRootSpan.traceId,
268+
expectedSessionId = session.getSessionId()
261269
)
262270

263271
assertEmbraceSpanData(
264272
span = sessionSpan,
265273
expectedStartTimeMs = sessionStartTimeMs,
266274
expectedEndTimeMs = sessionEndTimeMs,
267275
expectedParentId = OtelIds.invalidSpanId,
276+
expectedSessionId = session.getSessionId(),
268277
private = false
269278
)
270279

@@ -286,6 +295,12 @@ internal class TracingApiTest {
286295
)
287296
)
288297
)
298+
},
299+
otelExportAssertion = {
300+
val sessionSpan = awaitSpansWithType(2, EmbType.Ux.Session).last().toEmbraceSpanData().toEmbracePayload()
301+
val sessionId = checkNotNull(sessionSpan.attributes?.findAttributeValue(SessionIncubatingAttributes.SESSION_ID.key))
302+
val span = awaitSpans(1) { it.name == "test-trace-root" }.single().toEmbraceSpanData().toEmbracePayload()
303+
assertEquals(sessionId, span.attributes?.findAttributeValue(SessionIncubatingAttributes.SESSION_ID.key))
289304
}
290305
)
291306
}
@@ -391,6 +406,46 @@ internal class TracingApiTest {
391406
)
392407
}
393408

409+
@Test
410+
fun `correct session id populated in spans`() {
411+
var sessionId1 = ""
412+
var sessionId2 = ""
413+
testRule.runTest(
414+
testCaseAction = {
415+
val span1 = checkNotNull(embrace.createSpan("span1"))
416+
val span2 = checkNotNull(embrace.createSpan("span2"))
417+
recordSession {
418+
sessionId1 = checkNotNull(embrace.currentSessionId)
419+
span1.start()
420+
span2.start()
421+
span1.stop()
422+
}
423+
424+
recordSession {
425+
sessionId2 = checkNotNull(embrace.currentSessionId)
426+
span2.stop()
427+
}
428+
},
429+
assertAction = {
430+
assertNotEquals(sessionId1, sessionId2)
431+
432+
val sessions = getSessionEnvelopes(2)
433+
val span1 = sessions.first().findSpanByName("span1")
434+
val span2 = sessions.last().findSpanByName("span2")
435+
436+
assertEquals(sessionId1, span1.attributes?.findAttributeValue(SessionIncubatingAttributes.SESSION_ID.key))
437+
assertEquals(sessionId1, span2.attributes?.findAttributeValue(SessionIncubatingAttributes.SESSION_ID.key))
438+
439+
},
440+
otelExportAssertion = {
441+
val span1 = awaitSpans(1) { it.name == "span1" }.single().toEmbraceSpanData().toEmbracePayload()
442+
val span2 = awaitSpans(1) { it.name == "span2" }.single().toEmbraceSpanData().toEmbracePayload()
443+
assertEquals(sessionId1, span1.attributes?.findAttributeValue(SessionIncubatingAttributes.SESSION_ID.key))
444+
assertEquals(sessionId1, span2.attributes?.findAttributeValue(SessionIncubatingAttributes.SESSION_ID.key))
445+
}
446+
)
447+
}
448+
394449
private fun EmbracePayloadAssertionInterface.getSdkInitSpanFromBackgroundActivity(): List<Span> {
395450
val lastSentBackgroundActivity = getSingleSessionEnvelope(ApplicationState.BACKGROUND)
396451
val spans = checkNotNull(lastSentBackgroundActivity.data.spans)

embrace-android-sdk/src/integrationTest/kotlin/io/embrace/android/embracesdk/testframework/export/ExportedSpanValidator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ internal class ExportedSpanValidator {
4646
}
4747

4848
private fun OtelJavaSpanData.representAttributes(): Map<String, String> {
49-
val ignoreList = listOf("emb.process_identifier", "emb.private.sequence_id")
49+
val ignoreList = listOf("emb.process_identifier", "emb.private.sequence_id", "session.id")
5050
val attrs: Map<String, String> = attributes.asMap().map {
5151
it.key.key to it.value.toString()
5252
}.toMap()

embrace-android-sdk/src/integrationTest/resources/system-low-power-export.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"startEpochNanos": "169220160030000000",
77
"endEpochNanos": "169220163030000000",
88
"hasEnded": "true",
9-
"totalAttributeCount": "3",
9+
"totalAttributeCount": "4",
1010
"attributes": {
1111
"emb.type": "sys.low_power"
1212
},

0 commit comments

Comments
 (0)