Skip to content

Commit dfa8aad

Browse files
adinauerclaude
andauthored
feat(core): Add timestamped child spans to ISpan (#5929)
* feat(core): Add timestamped child spans to ISpan Expose explicit start timestamps for nested spans and forward them through core and OpenTelemetry span implementations. Use a default interface method to preserve compatibility with external ISpan implementations. Fixes #5896 Co-Authored-By: Claude <noreply@anthropic.com> * changelog --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c5053ad commit dfa8aad

16 files changed

Lines changed: 152 additions & 13 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features
66

7+
- Allow child spans to use explicit start timestamps through `ISpan` ([#5929](https://github.com/getsentry/sentry-java/pull/5929))
78
- Make `ISpan.startChild` overloads with `SpanOptions` public ([#5927](https://github.com/getsentry/sentry-java/pull/5927))
89

910
### Improvements

‎sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public final class io/sentry/opentelemetry/OtelStrongRefSpanWrapper : io/sentry/
8585
public fun startChild (Lio/sentry/SpanContext;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;
8686
public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan;
8787
public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan;
88+
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;)Lio/sentry/ISpan;
8889
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan;
8990
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;
9091
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;

‎sentry-opentelemetry/sentry-opentelemetry-bootstrap/build.gradle.kts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies {
2828
testImplementation(projects.sentryTestSupport)
2929
testImplementation(kotlin(Config.kotlinStdLib))
3030
testImplementation(libs.awaitility.kotlin)
31+
testImplementation(libs.google.truth)
3132
testImplementation(libs.kotlin.test.junit)
3233
testImplementation(libs.mockito.kotlin)
3334

‎sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ public void setTransactionName(@NotNull String name, @NotNull TransactionNameSou
125125
return delegate.startChild(spanContext, spanOptions);
126126
}
127127

128+
@Override
129+
public @NotNull ISpan startChild(
130+
@NotNull String operation, @Nullable String description, @Nullable SentryDate timestamp) {
131+
return delegate.startChild(operation, description, timestamp);
132+
}
133+
128134
@Override
129135
public @NotNull ISpan startChild(
130136
@NotNull String operation,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.sentry.opentelemetry
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import io.opentelemetry.api.trace.Span
5+
import io.sentry.ISpan
6+
import io.sentry.SentryLongDate
7+
import kotlin.test.Test
8+
import org.mockito.kotlin.mock
9+
import org.mockito.kotlin.verify
10+
import org.mockito.kotlin.whenever
11+
12+
class OtelStrongRefSpanWrapperTest {
13+
@Test
14+
fun `startChild with timestamp forwards to delegate`() {
15+
val delegate = mock<IOtelSpanWrapper>()
16+
val wrapper = OtelStrongRefSpanWrapper(mock<Span>(), delegate)
17+
val timestamp = SentryLongDate(1234)
18+
val expectedChild = mock<ISpan>()
19+
whenever(delegate.startChild("child-op", "description", timestamp)).thenReturn(expectedChild)
20+
21+
val child = wrapper.startChild("child-op", "description", timestamp)
22+
23+
verify(delegate).startChild("child-op", "description", timestamp)
24+
assertThat(child).isSameInstanceAs(expectedChild)
25+
}
26+
}

‎sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public final class io/sentry/opentelemetry/OtelSpanWrapper : io/sentry/opentelem
9999
public fun startChild (Lio/sentry/SpanContext;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;
100100
public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan;
101101
public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan;
102+
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;)Lio/sentry/ISpan;
102103
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan;
103104
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;
104105
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;

‎sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies {
3838
testImplementation(projects.sentryTestSupport)
3939
testImplementation(kotlin(Config.kotlinStdLib))
4040
testImplementation(libs.awaitility.kotlin)
41+
testImplementation(libs.google.truth)
4142
testImplementation(libs.kotlin.test.junit)
4243
testImplementation(libs.mockito.kotlin)
4344

‎sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ public OtelSpanWrapper(
141141
return childSpan;
142142
}
143143

144+
@Override
145+
public @NotNull ISpan startChild(
146+
@NotNull String operation, @Nullable String description, @Nullable SentryDate timestamp) {
147+
return startChild(operation, description, timestamp, Instrumenter.SENTRY);
148+
}
149+
144150
@Override
145151
public @NotNull ISpan startChild(
146152
@NotNull String operation,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.sentry.opentelemetry
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import io.opentelemetry.api.trace.SpanContext
5+
import io.opentelemetry.api.trace.TraceFlags
6+
import io.opentelemetry.api.trace.TraceState
7+
import io.opentelemetry.sdk.trace.ReadWriteSpan
8+
import io.sentry.IScopes
9+
import io.sentry.ISpan
10+
import io.sentry.ISpanFactory
11+
import io.sentry.Instrumenter
12+
import io.sentry.SentryLongDate
13+
import io.sentry.SentryOptions
14+
import io.sentry.SpanOptions
15+
import kotlin.test.Test
16+
import org.mockito.kotlin.any
17+
import org.mockito.kotlin.argumentCaptor
18+
import org.mockito.kotlin.eq
19+
import org.mockito.kotlin.mock
20+
import org.mockito.kotlin.verify
21+
import org.mockito.kotlin.whenever
22+
23+
class OtelSpanWrapperTest {
24+
@Test
25+
fun `startChild with timestamp forwards timestamp and Sentry instrumenter`() {
26+
val otelSpan = mock<ReadWriteSpan>()
27+
whenever(otelSpan.spanContext)
28+
.thenReturn(
29+
SpanContext.create(
30+
"2722d9f6ec019ade60c776169d9a8904",
31+
"cedf5b7571cb4972",
32+
TraceFlags.getSampled(),
33+
TraceState.getDefault(),
34+
)
35+
)
36+
whenever(otelSpan.name).thenReturn("parent")
37+
38+
val spanFactory = mock<ISpanFactory>()
39+
val options = SentryOptions().apply { this.spanFactory = spanFactory }
40+
val scopes = mock<IScopes>()
41+
whenever(scopes.options).thenReturn(options)
42+
43+
val parent = OtelSpanWrapper(otelSpan, scopes, SentryLongDate(0), null, null, null, null)
44+
val expectedChild = mock<ISpan>()
45+
whenever(spanFactory.createSpan(eq(scopes), any(), any(), eq(parent))).thenReturn(expectedChild)
46+
val timestamp = SentryLongDate(1234)
47+
48+
val child = parent.startChild("child-op", "description", timestamp)
49+
50+
val spanOptions = argumentCaptor<SpanOptions>()
51+
val spanContext = argumentCaptor<io.sentry.SpanContext>()
52+
verify(spanFactory)
53+
.createSpan(eq(scopes), spanOptions.capture(), spanContext.capture(), eq(parent))
54+
assertThat(child).isSameInstanceAs(expectedChild)
55+
assertThat(spanOptions.firstValue.startTimestamp).isSameInstanceAs(timestamp)
56+
assertThat(spanContext.firstValue.operation).isEqualTo("child-op")
57+
assertThat(spanContext.firstValue.description).isEqualTo("description")
58+
assertThat(spanContext.firstValue.instrumenter).isEqualTo(Instrumenter.SENTRY)
59+
}
60+
}

‎sentry/api/sentry.api‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,7 @@ public abstract interface class io/sentry/ISpan {
11971197
public abstract fun startChild (Lio/sentry/SpanContext;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;
11981198
public abstract fun startChild (Ljava/lang/String;)Lio/sentry/ISpan;
11991199
public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan;
1200+
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;)Lio/sentry/ISpan;
12001201
public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan;
12011202
public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;
12021203
public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;
@@ -1223,7 +1224,6 @@ public abstract interface class io/sentry/ITransaction : io/sentry/ISpan {
12231224
public abstract fun scheduleFinish ()V
12241225
public abstract fun setName (Ljava/lang/String;)V
12251226
public abstract fun setName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V
1226-
public abstract fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;)Lio/sentry/ISpan;
12271227
}
12281228

12291229
public abstract interface class io/sentry/ITransactionProfiler {
@@ -1939,6 +1939,7 @@ public final class io/sentry/NoOpSpan : io/sentry/ISpan {
19391939
public fun startChild (Lio/sentry/SpanContext;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;
19401940
public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan;
19411941
public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan;
1942+
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;)Lio/sentry/ISpan;
19421943
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan;
19431944
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;
19441945
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;
@@ -4378,6 +4379,7 @@ public final class io/sentry/Span : io/sentry/ISpan {
43784379
public fun startChild (Lio/sentry/SpanContext;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;
43794380
public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan;
43804381
public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan;
4382+
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;)Lio/sentry/ISpan;
43814383
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan;
43824384
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;
43834385
public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan;

0 commit comments

Comments
 (0)