Skip to content

Commit ccb906e

Browse files
authored
fix(rt): allow root spans to inherit their parent from the context when not already set (#757)
1 parent 9b9297c commit ccb906e

File tree

4 files changed

+111
-2
lines changed

4 files changed

+111
-2
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"id": "0b232d1e-06b5-4557-a5f5-90245238f2ab",
3+
"type": "bugfix",
4+
"description": "Allow root trace spans to inherit their parent from current context",
5+
"issues": [
6+
"awslabs/aws-sdk-kotlin#759"
7+
]
8+
}

runtime/tracing/tracing-core/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ extra["displayName"] = "Smithy :: Kotlin :: Tracing :: Core"
88
extra["moduleName"] = "aws.smithy.kotlin.runtime.tracing"
99

1010
val kotlinLoggingVersion: String by project
11+
val coroutinesVersion: String by project
1112

1213
kotlin {
1314
sourceSets {
@@ -29,6 +30,12 @@ kotlin {
2930
}
3031
}
3132

33+
commonTest {
34+
dependencies {
35+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion")
36+
}
37+
}
38+
3239
all {
3340
languageSettings.optIn("aws.smithy.kotlin.runtime.util.InternalApi")
3441
}

runtime/tracing/tracing-core/common/src/aws/smithy/kotlin/runtime/tracing/CoroutineContextUtils.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,31 @@ public suspend inline fun <R> CoroutineContext.withChildTraceSpan(
4545
}
4646
}
4747

48+
@InternalApi
49+
public class WrappedRootSpan(
50+
private val rootSpan: TraceSpan,
51+
override val parent: TraceSpan,
52+
) : TraceSpan by rootSpan
53+
4854
@InternalApi
4955
public suspend inline fun <R> CoroutineContext.withRootTraceSpan(
5056
rootSpan: TraceSpan,
5157
crossinline block: suspend CoroutineScope.() -> R,
5258
): R =
5359
try {
5460
val existingSpan = get(TraceSpanContextElement)?.traceSpan
55-
check(existingSpan == null || existingSpan == rootSpan.parent) {
61+
check(existingSpan == null || rootSpan.parent == null || existingSpan == rootSpan.parent) {
5662
"This method may only be called when no current span exists or the new span is a child of the active span"
5763
}
5864

59-
withContext(TraceSpanContextElement(rootSpan)) {
65+
val newRoot = if (existingSpan != null && rootSpan.parent == null) {
66+
// we are in some nested context, the existing span becomes this root's parent
67+
WrappedRootSpan(rootSpan, existingSpan)
68+
} else {
69+
rootSpan
70+
}
71+
72+
withContext(TraceSpanContextElement(newRoot)) {
6073
block()
6174
}
6275
} finally {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package aws.smithy.kotlin.runtime.tracing
7+
8+
import io.kotest.matchers.string.shouldContain
9+
import kotlinx.coroutines.ExperimentalCoroutinesApi
10+
import kotlinx.coroutines.test.runTest
11+
import kotlin.test.Test
12+
import kotlin.test.assertEquals
13+
import kotlin.test.assertFailsWith
14+
import kotlin.test.assertNull
15+
16+
@OptIn(ExperimentalCoroutinesApi::class)
17+
class TraceSpanCoroutineUtilsTest {
18+
@Test
19+
fun testRootSpanNoExisting() = runTest {
20+
val tracer = DefaultTracer(NoOpTraceProbe, "test")
21+
val rootSpan = tracer.createRootSpan("root")
22+
coroutineContext.withRootTraceSpan(rootSpan) {
23+
val actualSpan = coroutineContext.traceSpan
24+
assertEquals(rootSpan, actualSpan)
25+
}
26+
}
27+
28+
@Test
29+
fun testRootSpanExistingIsParent() = runTest {
30+
val tracer = DefaultTracer(NoOpTraceProbe, "test")
31+
val rootSpan = tracer.createRootSpan("root")
32+
33+
coroutineContext.withRootTraceSpan(rootSpan) {
34+
val currSpan = coroutineContext.traceSpan
35+
assertNull(currSpan.parent)
36+
val nestedTracer = currSpan.asNestedTracer("nested")
37+
val nestedRoot = nestedTracer.createRootSpan("root2")
38+
// this should be allowed since the existing span is the parent
39+
coroutineContext.withRootTraceSpan(nestedRoot) {
40+
val actualSpan = coroutineContext.traceSpan
41+
assertEquals(currSpan, actualSpan.parent)
42+
}
43+
}
44+
}
45+
46+
@Test
47+
fun testRootSpanExistingIsNotParent() = runTest {
48+
val tracer = DefaultTracer(NoOpTraceProbe, "test")
49+
val rootSpan1 = tracer.createRootSpan("root1")
50+
val rootSpan2 = tracer.createRootSpan("root2")
51+
val illegalRoot = object : TraceSpan {
52+
override val parent: TraceSpan = rootSpan2
53+
override val id: String = "illegal span"
54+
override fun postEvent(event: TraceEvent) {}
55+
override fun child(id: String): TraceSpan { error("not needed for test") }
56+
override fun close() {}
57+
}
58+
59+
coroutineContext.withRootTraceSpan(rootSpan1) {
60+
assertFailsWith<IllegalStateException> {
61+
coroutineContext.withRootTraceSpan(illegalRoot) { Unit }
62+
}.message.shouldContain("when no current span exists or the new span is a child of the active span")
63+
}
64+
}
65+
66+
@Test
67+
fun testRootSpanExistingNoParent() = runTest {
68+
// existing span with a new root created with no lineage, should become a child of the outer span automatically
69+
val tracer = DefaultTracer(NoOpTraceProbe, "test")
70+
val rootSpan1 = tracer.createRootSpan("root1")
71+
val rootSpan2 = tracer.createRootSpan("root2")
72+
73+
coroutineContext.withRootTraceSpan(rootSpan1) {
74+
val outerSpan = coroutineContext.traceSpan
75+
coroutineContext.withRootTraceSpan(rootSpan2) {
76+
val innerSpan = coroutineContext.traceSpan
77+
assertEquals(outerSpan, innerSpan.parent)
78+
}
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)