Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture OpenTelemetry span events #3564

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
# Changelog

## Unreleased

### Features

- 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))
- Capture OpenTelemetry span events ([#3564](https://github.com/getsentry/sentry-java/pull/3564))
- OpenTelemetry spans may have exceptions attached to them (`openTelemetrySpan.recordException`). We can now send those to Sentry as errors.
- Set `capture-open-telemetry-events=true` in `sentry.properties` to enable it
- Set `sentry.capture-open-telemetry-events=true` in Springs `application.properties` to enable it
- Set `sentry.captureOpenTelemetryEvents: true` in Springs `application.yml` to enable it

### Behavioural Changes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,25 @@
import io.opentelemetry.sdk.trace.ReadWriteSpan;
import io.opentelemetry.sdk.trace.ReadableSpan;
import io.opentelemetry.sdk.trace.SpanProcessor;
import io.opentelemetry.sdk.trace.data.EventData;
import io.opentelemetry.sdk.trace.data.ExceptionEventData;
import io.sentry.Baggage;
import io.sentry.DateUtils;
import io.sentry.IScopes;
import io.sentry.PropagationContext;
import io.sentry.ScopesAdapter;
import io.sentry.Sentry;
import io.sentry.SentryDate;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.SentryLongDate;
import io.sentry.SentryTraceHeader;
import io.sentry.SpanId;
import io.sentry.TracesSamplingDecision;
import io.sentry.exception.ExceptionMechanismException;
import io.sentry.protocol.Mechanism;
import io.sentry.protocol.SentryId;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Expand Down Expand Up @@ -143,9 +150,46 @@ public void onEnd(final @NotNull ReadableSpan spanBeingEnded) {
final @NotNull SentryDate finishDate =
new SentryLongDate(spanBeingEnded.toSpanData().getEndEpochNanos());
sentrySpan.updateEndDate(finishDate);

maybeCaptureSpanEventsAsExceptions(spanBeingEnded, sentrySpan);
}
}

private void maybeCaptureSpanEventsAsExceptions(
final @NotNull ReadableSpan spanBeingEnded, final @NotNull IOtelSpanWrapper sentrySpan) {
final @NotNull IScopes spanScopes = sentrySpan.getScopes();
if (spanScopes.getOptions().isCaptureOpenTelemetryEvents()) {
final @NotNull List<EventData> events = spanBeingEnded.toSpanData().getEvents();
for (EventData event : events) {
if (event instanceof ExceptionEventData) {
final @NotNull ExceptionEventData exceptionEvent = (ExceptionEventData) event;
captureException(spanScopes, exceptionEvent, sentrySpan);
}
}
}
}

private void captureException(
final @NotNull IScopes scopes,
final @NotNull ExceptionEventData exceptionEvent,
final @NotNull IOtelSpanWrapper sentrySpan) {
final @NotNull Throwable exception = exceptionEvent.getException();
final Mechanism mechanism = new Mechanism();
mechanism.setType("OpenTelemetrySpanEvent");
mechanism.setHandled(true);
// This is potentially the wrong Thread as it's the current thread meaning the thread where
// the span is being ended on. This may not match the thread where the exception occurred.
final Throwable mechanismException =
new ExceptionMechanismException(mechanism, exception, Thread.currentThread());

final SentryEvent event = new SentryEvent(mechanismException);
event.setTimestamp(DateUtils.nanosToDate(exceptionEvent.getEpochNanos()));
event.setLevel(SentryLevel.ERROR);
event.getContexts().setTrace(sentrySpan.getSpanContext());

scopes.captureEvent(event);
}

@Override
public boolean isEndRequired() {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ class SentryAutoConfigurationTest {
"sentry.spotlight-connection-url=http://local.sentry.io:1234",
"sentry.force-init=true",
"sentry.global-hub-mode=true",
"sentry.capture-open-telemetry-events=true",
"sentry.cron.default-checkin-margin=10",
"sentry.cron.default-max-runtime=30",
"sentry.cron.default-timezone=America/New_York",
Expand Down Expand Up @@ -222,6 +223,7 @@ class SentryAutoConfigurationTest {
assertThat(options.isEnableBackpressureHandling).isEqualTo(false)
assertThat(options.isForceInit).isEqualTo(true)
assertThat(options.isGlobalHubMode).isEqualTo(true)
assertThat(options.isCaptureOpenTelemetryEvents).isEqualTo(true)
assertThat(options.isEnableSpotlight).isEqualTo(true)
assertThat(options.spotlightConnectionUrl).isEqualTo("http://local.sentry.io:1234")
assertThat(options.cron).isNotNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ class SentryAutoConfigurationTest {
"sentry.spotlight-connection-url=http://local.sentry.io:1234",
"sentry.force-init=true",
"sentry.global-hub-mode=true",
"sentry.capture-open-telemetry-events=true",
"sentry.cron.default-checkin-margin=10",
"sentry.cron.default-max-runtime=30",
"sentry.cron.default-timezone=America/New_York",
Expand Down Expand Up @@ -221,6 +222,7 @@ class SentryAutoConfigurationTest {
assertThat(options.isEnableBackpressureHandling).isEqualTo(false)
assertThat(options.isForceInit).isEqualTo(true)
assertThat(options.isGlobalHubMode).isEqualTo(true)
assertThat(options.isCaptureOpenTelemetryEvents).isEqualTo(true)
assertThat(options.isEnableSpotlight).isEqualTo(true)
assertThat(options.spotlightConnectionUrl).isEqualTo("http://local.sentry.io:1234")
assertThat(options.cron).isNotNull
Expand Down
4 changes: 4 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ public final class io/sentry/ExternalOptions {
public fun getTags ()Ljava/util/Map;
public fun getTracePropagationTargets ()Ljava/util/List;
public fun getTracesSampleRate ()Ljava/lang/Double;
public fun isCaptureOpenTelemetryEvents ()Ljava/lang/Boolean;
public fun isEnableBackpressureHandling ()Ljava/lang/Boolean;
public fun isEnablePrettySerializationOutput ()Ljava/lang/Boolean;
public fun isEnableSpotlight ()Ljava/lang/Boolean;
Expand All @@ -487,6 +488,7 @@ public final class io/sentry/ExternalOptions {
public fun isGlobalHubMode ()Ljava/lang/Boolean;
public fun isSendDefaultPii ()Ljava/lang/Boolean;
public fun isSendModules ()Ljava/lang/Boolean;
public fun setCaptureOpenTelemetryEvents (Ljava/lang/Boolean;)V
public fun setCron (Lio/sentry/SentryOptions$Cron;)V
public fun setDebug (Ljava/lang/Boolean;)V
public fun setDist (Ljava/lang/String;)V
Expand Down Expand Up @@ -2930,6 +2932,7 @@ public class io/sentry/SentryOptions {
public fun isAttachServerName ()Z
public fun isAttachStacktrace ()Z
public fun isAttachThreads ()Z
public fun isCaptureOpenTelemetryEvents ()Z
public fun isDebug ()Z
public fun isEnableAppStartProfiling ()Z
public fun isEnableAutoSessionTracking ()Z
Expand Down Expand Up @@ -2967,6 +2970,7 @@ public class io/sentry/SentryOptions {
public fun setBeforeSendReplay (Lio/sentry/SentryOptions$BeforeSendReplayCallback;)V
public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V
public fun setCacheDirPath (Ljava/lang/String;)V
public fun setCaptureOpenTelemetryEvents (Z)V
public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V
public fun setConnectionTimeoutMillis (I)V
public fun setCron (Lio/sentry/SentryOptions$Cron;)V
Expand Down
14 changes: 14 additions & 0 deletions sentry/src/main/java/io/sentry/ExternalOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public final class ExternalOptions {
private @Nullable Boolean enableBackpressureHandling;
private @Nullable Boolean globalHubMode;
private @Nullable Boolean forceInit;
private @Nullable Boolean captureOpenTelemetryEvents;

private @Nullable SentryOptions.Cron cron;

Expand Down Expand Up @@ -146,6 +147,9 @@ public final class ExternalOptions {

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

options.setCaptureOpenTelemetryEvents(
propertiesProvider.getBooleanProperty("capture-open-telemetry-events"));

for (final String ignoredExceptionType :
propertiesProvider.getList("ignored-exceptions-for-type")) {
try {
Expand Down Expand Up @@ -504,4 +508,14 @@ public void setEnableSpotlight(final @Nullable Boolean enableSpotlight) {
public void setSpotlightConnectionUrl(final @Nullable String spotlightConnectionUrl) {
this.spotlightConnectionUrl = spotlightConnectionUrl;
}

@ApiStatus.Experimental
public void setCaptureOpenTelemetryEvents(final @Nullable Boolean captureOpenTelemetryEvents) {
this.captureOpenTelemetryEvents = captureOpenTelemetryEvents;
}

@ApiStatus.Experimental
public @Nullable Boolean isCaptureOpenTelemetryEvents() {
return captureOpenTelemetryEvents;
}
}
15 changes: 14 additions & 1 deletion sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ public class SentryOptions {

private @NotNull SentryReplayOptions sessionReplay;

@ApiStatus.Experimental private boolean captureOpenTelemetryEvents = false;
/**
* Adds an event processor
*
Expand Down Expand Up @@ -2634,6 +2635,16 @@ public void setSessionReplay(final @NotNull SentryReplayOptions sessionReplayOpt
this.sessionReplay = sessionReplayOptions;
}

@ApiStatus.Experimental
public void setCaptureOpenTelemetryEvents(final boolean captureOpenTelemetryEvents) {
this.captureOpenTelemetryEvents = captureOpenTelemetryEvents;
}

@ApiStatus.Experimental
public boolean isCaptureOpenTelemetryEvents() {
return captureOpenTelemetryEvents;
}

/**
* Load the lazy fields. Useful to load in the background, so that results are already cached. DO
* NOT CALL THIS METHOD ON THE MAIN THREAD.
Expand Down Expand Up @@ -2927,7 +2938,9 @@ public void merge(final @NotNull ExternalOptions options) {
if (options.isSendDefaultPii() != null) {
setSendDefaultPii(options.isSendDefaultPii());
}

if (options.isCaptureOpenTelemetryEvents() != null) {
setCaptureOpenTelemetryEvents(options.isCaptureOpenTelemetryEvents());
}
if (options.isEnableSpotlight() != null) {
setEnableSpotlight(options.isEnableSpotlight());
}
Expand Down
14 changes: 14 additions & 0 deletions sentry/src/test/java/io/sentry/ExternalOptionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,20 @@ class ExternalOptionsTest {
}
}

@Test
fun `creates options with captureOpenTelemetryEvents set to false`() {
withPropertiesFile("capture-open-telemetry-events=false") { options ->
assertTrue(options.isCaptureOpenTelemetryEvents == false)
}
}

@Test
fun `creates options with captureOpenTelemetryEvents set to true`() {
withPropertiesFile("capture-open-telemetry-events=true") { options ->
assertTrue(options.isCaptureOpenTelemetryEvents == true)
}
}

private fun withPropertiesFile(textLines: List<String> = emptyList(), logger: ILogger = mock(), fn: (ExternalOptions) -> Unit) {
// create a sentry.properties file in temporary folder
val temporaryFolder = TemporaryFolder()
Expand Down
Loading