Skip to content

Commit 033bc88

Browse files
authored
Capture OpenTelemetry span events (#3564)
* capture otel events * Set trace for captured error; set timestamp; refactor * changelog * fix external option name * remove duplicate dependency entry * ignore buildSrc/.kotlin
1 parent 7074d0b commit 033bc88

File tree

9 files changed

+100
-1
lines changed

9 files changed

+100
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ distributions/
2020
sentry-spring-boot-starter-jakarta/src/main/resources/META-INF/spring.factories
2121
sentry-samples/sentry-samples-spring-boot-jakarta/spy.log
2222
spy.log
23+
buildSrc/.kotlin/

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@
99
### Features
1010

1111
- The SDK now automatically propagates the trace-context to the native layer. This allows to connect errors on different layers of the application. ([#4137](https://github.com/getsentry/sentry-java/pull/4137))
12+
- Capture OpenTelemetry span events ([#3564](https://github.com/getsentry/sentry-java/pull/3564))
13+
- OpenTelemetry spans may have exceptions attached to them (`openTelemetrySpan.recordException`). We can now send those to Sentry as errors.
14+
- Set `capture-open-telemetry-events=true` in `sentry.properties` to enable it
15+
- Set `sentry.capture-open-telemetry-events=true` in Springs `application.properties` to enable it
16+
- Set `sentry.captureOpenTelemetryEvents: true` in Springs `application.yml` to enable it
1217

1318
### Behavioural Changes
1419

sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,25 @@
88
import io.opentelemetry.sdk.trace.ReadWriteSpan;
99
import io.opentelemetry.sdk.trace.ReadableSpan;
1010
import io.opentelemetry.sdk.trace.SpanProcessor;
11+
import io.opentelemetry.sdk.trace.data.EventData;
12+
import io.opentelemetry.sdk.trace.data.ExceptionEventData;
1113
import io.sentry.Baggage;
14+
import io.sentry.DateUtils;
1215
import io.sentry.IScopes;
1316
import io.sentry.PropagationContext;
1417
import io.sentry.ScopesAdapter;
1518
import io.sentry.Sentry;
1619
import io.sentry.SentryDate;
20+
import io.sentry.SentryEvent;
1721
import io.sentry.SentryLevel;
1822
import io.sentry.SentryLongDate;
1923
import io.sentry.SentryTraceHeader;
2024
import io.sentry.SpanId;
2125
import io.sentry.TracesSamplingDecision;
26+
import io.sentry.exception.ExceptionMechanismException;
27+
import io.sentry.protocol.Mechanism;
2228
import io.sentry.protocol.SentryId;
29+
import java.util.List;
2330
import org.jetbrains.annotations.NotNull;
2431
import org.jetbrains.annotations.Nullable;
2532

@@ -143,9 +150,46 @@ public void onEnd(final @NotNull ReadableSpan spanBeingEnded) {
143150
final @NotNull SentryDate finishDate =
144151
new SentryLongDate(spanBeingEnded.toSpanData().getEndEpochNanos());
145152
sentrySpan.updateEndDate(finishDate);
153+
154+
maybeCaptureSpanEventsAsExceptions(spanBeingEnded, sentrySpan);
146155
}
147156
}
148157

158+
private void maybeCaptureSpanEventsAsExceptions(
159+
final @NotNull ReadableSpan spanBeingEnded, final @NotNull IOtelSpanWrapper sentrySpan) {
160+
final @NotNull IScopes spanScopes = sentrySpan.getScopes();
161+
if (spanScopes.getOptions().isCaptureOpenTelemetryEvents()) {
162+
final @NotNull List<EventData> events = spanBeingEnded.toSpanData().getEvents();
163+
for (EventData event : events) {
164+
if (event instanceof ExceptionEventData) {
165+
final @NotNull ExceptionEventData exceptionEvent = (ExceptionEventData) event;
166+
captureException(spanScopes, exceptionEvent, sentrySpan);
167+
}
168+
}
169+
}
170+
}
171+
172+
private void captureException(
173+
final @NotNull IScopes scopes,
174+
final @NotNull ExceptionEventData exceptionEvent,
175+
final @NotNull IOtelSpanWrapper sentrySpan) {
176+
final @NotNull Throwable exception = exceptionEvent.getException();
177+
final Mechanism mechanism = new Mechanism();
178+
mechanism.setType("OpenTelemetrySpanEvent");
179+
mechanism.setHandled(true);
180+
// This is potentially the wrong Thread as it's the current thread meaning the thread where
181+
// the span is being ended on. This may not match the thread where the exception occurred.
182+
final Throwable mechanismException =
183+
new ExceptionMechanismException(mechanism, exception, Thread.currentThread());
184+
185+
final SentryEvent event = new SentryEvent(mechanismException);
186+
event.setTimestamp(DateUtils.nanosToDate(exceptionEvent.getEpochNanos()));
187+
event.setLevel(SentryLevel.ERROR);
188+
event.getContexts().setTrace(sentrySpan.getSpanContext());
189+
190+
scopes.captureEvent(event);
191+
}
192+
149193
@Override
150194
public boolean isEndRequired() {
151195
return true;

sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ class SentryAutoConfigurationTest {
182182
"sentry.spotlight-connection-url=http://local.sentry.io:1234",
183183
"sentry.force-init=true",
184184
"sentry.global-hub-mode=true",
185+
"sentry.capture-open-telemetry-events=true",
185186
"sentry.cron.default-checkin-margin=10",
186187
"sentry.cron.default-max-runtime=30",
187188
"sentry.cron.default-timezone=America/New_York",
@@ -222,6 +223,7 @@ class SentryAutoConfigurationTest {
222223
assertThat(options.isEnableBackpressureHandling).isEqualTo(false)
223224
assertThat(options.isForceInit).isEqualTo(true)
224225
assertThat(options.isGlobalHubMode).isEqualTo(true)
226+
assertThat(options.isCaptureOpenTelemetryEvents).isEqualTo(true)
225227
assertThat(options.isEnableSpotlight).isEqualTo(true)
226228
assertThat(options.spotlightConnectionUrl).isEqualTo("http://local.sentry.io:1234")
227229
assertThat(options.cron).isNotNull

sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ class SentryAutoConfigurationTest {
181181
"sentry.spotlight-connection-url=http://local.sentry.io:1234",
182182
"sentry.force-init=true",
183183
"sentry.global-hub-mode=true",
184+
"sentry.capture-open-telemetry-events=true",
184185
"sentry.cron.default-checkin-margin=10",
185186
"sentry.cron.default-max-runtime=30",
186187
"sentry.cron.default-timezone=America/New_York",
@@ -221,6 +222,7 @@ class SentryAutoConfigurationTest {
221222
assertThat(options.isEnableBackpressureHandling).isEqualTo(false)
222223
assertThat(options.isForceInit).isEqualTo(true)
223224
assertThat(options.isGlobalHubMode).isEqualTo(true)
225+
assertThat(options.isCaptureOpenTelemetryEvents).isEqualTo(true)
224226
assertThat(options.isEnableSpotlight).isEqualTo(true)
225227
assertThat(options.spotlightConnectionUrl).isEqualTo("http://local.sentry.io:1234")
226228
assertThat(options.cron).isNotNull

sentry/api/sentry.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ public final class io/sentry/ExternalOptions {
479479
public fun getTags ()Ljava/util/Map;
480480
public fun getTracePropagationTargets ()Ljava/util/List;
481481
public fun getTracesSampleRate ()Ljava/lang/Double;
482+
public fun isCaptureOpenTelemetryEvents ()Ljava/lang/Boolean;
482483
public fun isEnableBackpressureHandling ()Ljava/lang/Boolean;
483484
public fun isEnablePrettySerializationOutput ()Ljava/lang/Boolean;
484485
public fun isEnableSpotlight ()Ljava/lang/Boolean;
@@ -487,6 +488,7 @@ public final class io/sentry/ExternalOptions {
487488
public fun isGlobalHubMode ()Ljava/lang/Boolean;
488489
public fun isSendDefaultPii ()Ljava/lang/Boolean;
489490
public fun isSendModules ()Ljava/lang/Boolean;
491+
public fun setCaptureOpenTelemetryEvents (Ljava/lang/Boolean;)V
490492
public fun setCron (Lio/sentry/SentryOptions$Cron;)V
491493
public fun setDebug (Ljava/lang/Boolean;)V
492494
public fun setDist (Ljava/lang/String;)V
@@ -2930,6 +2932,7 @@ public class io/sentry/SentryOptions {
29302932
public fun isAttachServerName ()Z
29312933
public fun isAttachStacktrace ()Z
29322934
public fun isAttachThreads ()Z
2935+
public fun isCaptureOpenTelemetryEvents ()Z
29332936
public fun isDebug ()Z
29342937
public fun isEnableAppStartProfiling ()Z
29352938
public fun isEnableAutoSessionTracking ()Z
@@ -2967,6 +2970,7 @@ public class io/sentry/SentryOptions {
29672970
public fun setBeforeSendReplay (Lio/sentry/SentryOptions$BeforeSendReplayCallback;)V
29682971
public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V
29692972
public fun setCacheDirPath (Ljava/lang/String;)V
2973+
public fun setCaptureOpenTelemetryEvents (Z)V
29702974
public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V
29712975
public fun setConnectionTimeoutMillis (I)V
29722976
public fun setCron (Lio/sentry/SentryOptions$Cron;)V

sentry/src/main/java/io/sentry/ExternalOptions.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public final class ExternalOptions {
5353
private @Nullable Boolean enableBackpressureHandling;
5454
private @Nullable Boolean globalHubMode;
5555
private @Nullable Boolean forceInit;
56+
private @Nullable Boolean captureOpenTelemetryEvents;
5657

5758
private @Nullable SentryOptions.Cron cron;
5859

@@ -146,6 +147,9 @@ public final class ExternalOptions {
146147

147148
options.setGlobalHubMode(propertiesProvider.getBooleanProperty("global-hub-mode"));
148149

150+
options.setCaptureOpenTelemetryEvents(
151+
propertiesProvider.getBooleanProperty("capture-open-telemetry-events"));
152+
149153
for (final String ignoredExceptionType :
150154
propertiesProvider.getList("ignored-exceptions-for-type")) {
151155
try {
@@ -504,4 +508,14 @@ public void setEnableSpotlight(final @Nullable Boolean enableSpotlight) {
504508
public void setSpotlightConnectionUrl(final @Nullable String spotlightConnectionUrl) {
505509
this.spotlightConnectionUrl = spotlightConnectionUrl;
506510
}
511+
512+
@ApiStatus.Experimental
513+
public void setCaptureOpenTelemetryEvents(final @Nullable Boolean captureOpenTelemetryEvents) {
514+
this.captureOpenTelemetryEvents = captureOpenTelemetryEvents;
515+
}
516+
517+
@ApiStatus.Experimental
518+
public @Nullable Boolean isCaptureOpenTelemetryEvents() {
519+
return captureOpenTelemetryEvents;
520+
}
507521
}

sentry/src/main/java/io/sentry/SentryOptions.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,7 @@ public class SentryOptions {
530530

531531
private @NotNull SentryReplayOptions sessionReplay;
532532

533+
@ApiStatus.Experimental private boolean captureOpenTelemetryEvents = false;
533534
/**
534535
* Adds an event processor
535536
*
@@ -2634,6 +2635,16 @@ public void setSessionReplay(final @NotNull SentryReplayOptions sessionReplayOpt
26342635
this.sessionReplay = sessionReplayOptions;
26352636
}
26362637

2638+
@ApiStatus.Experimental
2639+
public void setCaptureOpenTelemetryEvents(final boolean captureOpenTelemetryEvents) {
2640+
this.captureOpenTelemetryEvents = captureOpenTelemetryEvents;
2641+
}
2642+
2643+
@ApiStatus.Experimental
2644+
public boolean isCaptureOpenTelemetryEvents() {
2645+
return captureOpenTelemetryEvents;
2646+
}
2647+
26372648
/**
26382649
* Load the lazy fields. Useful to load in the background, so that results are already cached. DO
26392650
* NOT CALL THIS METHOD ON THE MAIN THREAD.
@@ -2927,7 +2938,9 @@ public void merge(final @NotNull ExternalOptions options) {
29272938
if (options.isSendDefaultPii() != null) {
29282939
setSendDefaultPii(options.isSendDefaultPii());
29292940
}
2930-
2941+
if (options.isCaptureOpenTelemetryEvents() != null) {
2942+
setCaptureOpenTelemetryEvents(options.isCaptureOpenTelemetryEvents());
2943+
}
29312944
if (options.isEnableSpotlight() != null) {
29322945
setEnableSpotlight(options.isEnableSpotlight());
29332946
}

sentry/src/test/java/io/sentry/ExternalOptionsTest.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,20 @@ class ExternalOptionsTest {
361361
}
362362
}
363363

364+
@Test
365+
fun `creates options with captureOpenTelemetryEvents set to false`() {
366+
withPropertiesFile("capture-open-telemetry-events=false") { options ->
367+
assertTrue(options.isCaptureOpenTelemetryEvents == false)
368+
}
369+
}
370+
371+
@Test
372+
fun `creates options with captureOpenTelemetryEvents set to true`() {
373+
withPropertiesFile("capture-open-telemetry-events=true") { options ->
374+
assertTrue(options.isCaptureOpenTelemetryEvents == true)
375+
}
376+
}
377+
364378
private fun withPropertiesFile(textLines: List<String> = emptyList(), logger: ILogger = mock(), fn: (ExternalOptions) -> Unit) {
365379
// create a sentry.properties file in temporary folder
366380
val temporaryFolder = TemporaryFolder()

0 commit comments

Comments
 (0)