diff --git a/CHANGELOG.md b/CHANGELOG.md index 841e70b453..5d7d5ca907 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,15 @@ # Changelog -## Unreleased +## 8.11.0-alpha.1 ### Features +- Support `globalHubMode` for OpenTelemetry ([#4349](https://github.com/getsentry/sentry-java/pull/4349)) + - Sentry now adds OpenTelemetry spans without a parent to the last known unfinished root span (transaction) + - Previously those spans would end up in Sentry as separate transactions + - Spans created via Sentry API are preferred over those created through OpenTelemetry API or auto instrumentation +- New option `ignoreStandaloneClientSpans` that prevents Sentry from creating transactions for OpenTelemetry spans with kind `CLIENT` ([#4349](https://github.com/getsentry/sentry-java/pull/4349)) + - Defaults to `false` meaning standalone OpenTelemetry spans with kind `CLIENT` will be turned into Sentry transactions - Make `RequestDetailsResolver` public ([#4326](https://github.com/getsentry/sentry-java/pull/4326)) - `RequestDetailsResolver` is now public and has an additional constructor, making it easier to use a custom `TransportFactory` diff --git a/gradle.properties b/gradle.properties index 418a5780d8..5bc6931774 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ org.gradle.workers.max=2 android.useAndroidX=true # Release information -versionName=8.10.0 +versionName=8.11.0-alpha.1 # Override the SDK name on native crashes on Android sentryAndroidSdkName=sentry.native.android diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api index 8e6b59b4be..fbff8db2c1 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api @@ -1,6 +1,7 @@ public abstract interface class io/sentry/opentelemetry/IOtelSpanWrapper : io/sentry/ISpan { public abstract fun getData ()Ljava/util/Map; public abstract fun getMeasurements ()Ljava/util/Map; + public abstract fun getOpenTelemetrySpan ()Lio/opentelemetry/api/trace/Span; public abstract fun getOpenTelemetrySpanAttributes ()Lio/opentelemetry/api/common/Attributes; public abstract fun getScopes ()Lio/sentry/IScopes; public abstract fun getTags ()Ljava/util/Map; @@ -8,6 +9,7 @@ public abstract interface class io/sentry/opentelemetry/IOtelSpanWrapper : io/se public abstract fun getTransactionName ()Ljava/lang/String; public abstract fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; public abstract fun isProfileSampled ()Ljava/lang/Boolean; + public abstract fun isRoot ()Z public abstract fun setTransactionName (Ljava/lang/String;)V public abstract fun setTransactionName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V public abstract fun storeInContext (Lio/opentelemetry/context/Context;)Lio/opentelemetry/context/Context; @@ -16,6 +18,7 @@ public abstract interface class io/sentry/opentelemetry/IOtelSpanWrapper : io/se public final class io/sentry/opentelemetry/InternalSemanticAttributes { public static final field BAGGAGE Lio/opentelemetry/api/common/AttributeKey; public static final field BAGGAGE_MUTABLE Lio/opentelemetry/api/common/AttributeKey; + public static final field CREATED_VIA_SENTRY_API Lio/opentelemetry/api/common/AttributeKey; public static final field IS_REMOTE_PARENT Lio/opentelemetry/api/common/AttributeKey; public static final field PARENT_SAMPLED Lio/opentelemetry/api/common/AttributeKey; public static final field PROFILE_SAMPLED Lio/opentelemetry/api/common/AttributeKey; @@ -52,6 +55,7 @@ public final class io/sentry/opentelemetry/OtelStrongRefSpanWrapper : io/sentry/ public fun getDescription ()Ljava/lang/String; public fun getFinishDate ()Lio/sentry/SentryDate; public fun getMeasurements ()Ljava/util/Map; + public fun getOpenTelemetrySpan ()Lio/opentelemetry/api/trace/Span; public fun getOpenTelemetrySpanAttributes ()Lio/opentelemetry/api/common/Attributes; public fun getOperation ()Ljava/lang/String; public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; @@ -68,6 +72,7 @@ public final class io/sentry/opentelemetry/OtelStrongRefSpanWrapper : io/sentry/ public fun isFinished ()Z public fun isNoOp ()Z public fun isProfileSampled ()Ljava/lang/Boolean; + public fun isRoot ()Z public fun isSampled ()Ljava/lang/Boolean; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; public fun setContext (Ljava/lang/String;Ljava/lang/Object;)V @@ -151,6 +156,7 @@ public final class io/sentry/opentelemetry/SentryContextStorage : io/opentelemet public fun (Lio/opentelemetry/context/ContextStorage;)V public fun attach (Lio/opentelemetry/context/Context;)Lio/opentelemetry/context/Scope; public fun current ()Lio/opentelemetry/context/Context; + public fun root ()Lio/opentelemetry/context/Context; } public final class io/sentry/opentelemetry/SentryContextStorageProvider : io/opentelemetry/context/ContextStorageProvider { @@ -165,6 +171,20 @@ public final class io/sentry/opentelemetry/SentryContextWrapper : io/opentelemet public static fun wrap (Lio/opentelemetry/context/Context;)Lio/sentry/opentelemetry/SentryContextWrapper; } +public final class io/sentry/opentelemetry/SentryOtelGlobalHubModeSpan : io/opentelemetry/api/trace/Span { + public fun ()V + public fun addEvent (Ljava/lang/String;Lio/opentelemetry/api/common/Attributes;)Lio/opentelemetry/api/trace/Span; + public fun addEvent (Ljava/lang/String;Lio/opentelemetry/api/common/Attributes;JLjava/util/concurrent/TimeUnit;)Lio/opentelemetry/api/trace/Span; + public fun end ()V + public fun end (JLjava/util/concurrent/TimeUnit;)V + public fun getSpanContext ()Lio/opentelemetry/api/trace/SpanContext; + public fun isRecording ()Z + public fun recordException (Ljava/lang/Throwable;Lio/opentelemetry/api/common/Attributes;)Lio/opentelemetry/api/trace/Span; + public fun setAttribute (Lio/opentelemetry/api/common/AttributeKey;Ljava/lang/Object;)Lio/opentelemetry/api/trace/Span; + public fun setStatus (Lio/opentelemetry/api/trace/StatusCode;Ljava/lang/String;)Lio/opentelemetry/api/trace/Span; + public fun updateName (Ljava/lang/String;)Lio/opentelemetry/api/trace/Span; +} + public final class io/sentry/opentelemetry/SentryOtelKeys { public static final field SENTRY_BAGGAGE_KEY Lio/opentelemetry/context/ContextKey; public static final field SENTRY_SCOPES_KEY Lio/opentelemetry/context/ContextKey; @@ -181,6 +201,7 @@ public final class io/sentry/opentelemetry/SentryOtelThreadLocalStorage : io/ope public final class io/sentry/opentelemetry/SentryWeakSpanStorage { public fun clear ()V public static fun getInstance ()Lio/sentry/opentelemetry/SentryWeakSpanStorage; + public fun getLastKnownUnfinishedRootSpan ()Lio/sentry/opentelemetry/IOtelSpanWrapper; public fun getSentrySpan (Lio/opentelemetry/api/trace/SpanContext;)Lio/sentry/opentelemetry/IOtelSpanWrapper; public fun storeSentrySpan (Lio/opentelemetry/api/trace/SpanContext;Lio/sentry/opentelemetry/IOtelSpanWrapper;)V } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/IOtelSpanWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/IOtelSpanWrapper.java index 1eefc854a8..6e34f508ed 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/IOtelSpanWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/IOtelSpanWrapper.java @@ -1,6 +1,7 @@ package io.sentry.opentelemetry; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; import io.opentelemetry.context.Context; import io.sentry.IScopes; import io.sentry.ISpan; @@ -52,4 +53,9 @@ public interface IOtelSpanWrapper extends ISpan { @ApiStatus.Internal @Nullable Attributes getOpenTelemetrySpanAttributes(); + + boolean isRoot(); + + @Nullable + Span getOpenTelemetrySpan(); } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java index 4795401266..a60ca9bab0 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/InternalSemanticAttributes.java @@ -21,4 +21,6 @@ public final class InternalSemanticAttributes { public static final AttributeKey BAGGAGE = AttributeKey.stringKey("sentry.baggage"); public static final AttributeKey BAGGAGE_MUTABLE = AttributeKey.booleanKey("sentry.baggage_mutable"); + public static final AttributeKey CREATED_VIA_SENTRY_API = + AttributeKey.booleanKey("sentry.is_created_via_sentry_api"); } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java index a4008a0128..2d9460a325 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java @@ -310,4 +310,14 @@ public void setContext(@Nullable String key, @Nullable Object context) { public @Nullable Attributes getOpenTelemetrySpanAttributes() { return delegate.getOpenTelemetrySpanAttributes(); } + + @Override + public boolean isRoot() { + return delegate.isRoot(); + } + + @Override + public @Nullable Span getOpenTelemetrySpan() { + return delegate.getOpenTelemetrySpan(); + } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java index 4f3efa40c2..a47460593f 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java @@ -3,6 +3,7 @@ import io.opentelemetry.context.Context; import io.opentelemetry.context.ContextStorage; import io.opentelemetry.context.Scope; +import io.sentry.Sentry; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -38,4 +39,16 @@ public Scope attach(Context toAttach) { public Context current() { return contextStorage.current(); } + + @Override + public Context root() { + final @NotNull Context originalRoot = contextStorage.root(); + + if (Sentry.isGlobalHubMode()) { + return new SentryOtelGlobalHubModeSpan() + .storeInContext(SentryContextWrapper.wrap(originalRoot)); + } + + return originalRoot; + } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java index a0213bafe6..429d841547 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java @@ -21,8 +21,43 @@ private SentryContextWrapper(final @NotNull Context delegate) { } @Override - public V get(final @NotNull ContextKey contextKey) { - return delegate.get(contextKey); + public @Nullable V get(final @NotNull ContextKey contextKey) { + final @Nullable V result = delegate.get(contextKey); + if (shouldReturnRootSpanInstead(contextKey, result)) { + return returnUnfinishedRootSpanIfAvailable(result); + } + return result; + } + + private boolean shouldReturnRootSpanInstead( + final @NotNull ContextKey contextKey, final @Nullable V result) { + if (!Sentry.isGlobalHubMode()) { + return false; + } + if (!isOpentelemetrySpan(contextKey)) { + return false; + } + if (result == null) { + return true; + } + if (result instanceof SentryOtelGlobalHubModeSpan) { + return true; + } + return result instanceof Span && !((Span) result).getSpanContext().isValid(); + } + + @SuppressWarnings("unchecked") + private @Nullable V returnUnfinishedRootSpanIfAvailable(final @Nullable V result) { + final @Nullable IOtelSpanWrapper sentrySpan = + SentryWeakSpanStorage.getInstance().getLastKnownUnfinishedRootSpan(); + if (sentrySpan != null) { + try { + return (V) sentrySpan.getOpenTelemetrySpan(); + } catch (Throwable t) { + return result; + } + } + return result; } @Override diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryOtelGlobalHubModeSpan.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryOtelGlobalHubModeSpan.java new file mode 100644 index 0000000000..213cb5519c --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryOtelGlobalHubModeSpan.java @@ -0,0 +1,78 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.StatusCode; +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Experimental +public final class SentryOtelGlobalHubModeSpan implements Span { + + private @NotNull Span getOtelSpan() { + final @Nullable IOtelSpanWrapper lastKnownUnfinishedRootSpan = + SentryWeakSpanStorage.getInstance().getLastKnownUnfinishedRootSpan(); + if (lastKnownUnfinishedRootSpan != null) { + final @Nullable Span openTelemetrySpan = lastKnownUnfinishedRootSpan.getOpenTelemetrySpan(); + if (openTelemetrySpan != null) { + return openTelemetrySpan; + } + } + + return Span.getInvalid(); + } + + @Override + public Span setAttribute(AttributeKey key, T value) { + return getOtelSpan().setAttribute(key, value); + } + + @Override + public Span addEvent(String name, Attributes attributes) { + return getOtelSpan().addEvent(name, attributes); + } + + @Override + public Span addEvent(String name, Attributes attributes, long timestamp, TimeUnit unit) { + return getOtelSpan().addEvent(name, attributes, timestamp, unit); + } + + @Override + public Span setStatus(StatusCode statusCode, String description) { + return getOtelSpan().setStatus(statusCode, description); + } + + @Override + public Span recordException(Throwable exception, Attributes additionalAttributes) { + return getOtelSpan().recordException(exception, additionalAttributes); + } + + @Override + public Span updateName(String name) { + return getOtelSpan().updateName(name); + } + + @Override + public void end() { + getOtelSpan().end(); + } + + @Override + public void end(long timestamp, TimeUnit unit) { + getOtelSpan().end(timestamp, unit); + } + + @Override + public SpanContext getSpanContext() { + return getOtelSpan().getSpanContext(); + } + + @Override + public boolean isRecording() { + return getOtelSpan().isRecording(); + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java index 5096c011e2..b14ea0f77f 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryWeakSpanStorage.java @@ -1,9 +1,11 @@ package io.sentry.opentelemetry; +import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.context.internal.shaded.WeakConcurrentMap; import io.sentry.ISentryLifecycleToken; import io.sentry.util.AutoClosableReentrantLock; +import java.lang.ref.WeakReference; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -34,6 +36,8 @@ public final class SentryWeakSpanStorage { // weak keys, spawns a thread to clean up values that have been garbage collected private final @NotNull WeakConcurrentMap sentrySpans = new WeakConcurrentMap<>(true); + private volatile @NotNull WeakReference lastKnownRootSpan = + new WeakReference<>(null); private SentryWeakSpanStorage() {} @@ -43,11 +47,45 @@ private SentryWeakSpanStorage() {} public void storeSentrySpan( final @NotNull SpanContext otelSpan, final @NotNull IOtelSpanWrapper sentrySpan) { + System.out.println("storing span: " + sentrySpan.getOperation()); this.sentrySpans.put(otelSpan, sentrySpan); + if (shouldStoreSpanAsRootSpan(sentrySpan)) { + System.out.println("storing span as last known root: " + sentrySpan.getOperation()); + lastKnownRootSpan = new WeakReference<>(sentrySpan); + } + } + + private boolean shouldStoreSpanAsRootSpan(final @NotNull IOtelSpanWrapper sentrySpan) { + if (!sentrySpan.isRoot()) { + return false; + } + + final @Nullable IOtelSpanWrapper previousRootSpan = getLastKnownUnfinishedRootSpan(); + if (previousRootSpan == null) { + return true; + } + + final @Nullable Attributes attributes = previousRootSpan.getOpenTelemetrySpanAttributes(); + if (attributes == null) { + return true; + } + + final @Nullable Boolean isCreatedViaSentryApi = + attributes.get(InternalSemanticAttributes.CREATED_VIA_SENTRY_API); + return isCreatedViaSentryApi != null && isCreatedViaSentryApi == true; + } + + public @Nullable IOtelSpanWrapper getLastKnownUnfinishedRootSpan() { + final @Nullable IOtelSpanWrapper span = lastKnownRootSpan.get(); + if (span != null && !span.isFinished()) { + return span; + } + return null; } @TestOnly public void clear() { sentrySpans.clear(); + lastKnownRootSpan.clear(); } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api b/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api index f20ea0ab86..21489b4aa6 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api +++ b/sentry-opentelemetry/sentry-opentelemetry-core/api/sentry-opentelemetry-core.api @@ -61,6 +61,7 @@ public final class io/sentry/opentelemetry/OtelSpanWrapper : io/sentry/opentelem public fun getDescription ()Ljava/lang/String; public fun getFinishDate ()Lio/sentry/SentryDate; public fun getMeasurements ()Ljava/util/Map; + public fun getOpenTelemetrySpan ()Lio/opentelemetry/api/trace/Span; public fun getOpenTelemetrySpanAttributes ()Lio/opentelemetry/api/common/Attributes; public fun getOperation ()Ljava/lang/String; public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; @@ -77,6 +78,7 @@ public final class io/sentry/opentelemetry/OtelSpanWrapper : io/sentry/opentelem public fun isFinished ()Z public fun isNoOp ()Z public fun isProfileSampled ()Ljava/lang/Boolean; + public fun isRoot ()Z public fun isSampled ()Ljava/lang/Boolean; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; public fun setContext (Ljava/lang/String;Ljava/lang/Object;)V diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts index 05c194d860..df06b30352 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(Config.TestLibs.kotlinTestJunit) testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) testImplementation(Config.TestLibs.awaitility) testImplementation(Config.Libs.OpenTelemetry.otelSdk) diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java index bc78643fb9..aadf040e0b 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java @@ -515,6 +515,25 @@ public Map getMeasurements() { } } + @Override + public boolean isRoot() { + if (context.getParentSpanId() == null) { + return true; + } + + final @Nullable ReadWriteSpan readWriteSpan = span.get(); + if (readWriteSpan != null) { + return readWriteSpan.getParentSpanContext().isRemote(); + } + + return false; + } + + @Override + public @Nullable Span getOpenTelemetrySpan() { + return span.get(); + } + @SuppressWarnings("MustBeClosedChecker") @ApiStatus.Internal @Override diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java index 5493ba033c..4c362f10a0 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySampler.java @@ -66,7 +66,7 @@ public SamplingResult shouldSample( if (samplingDecision != null) { return new SentrySamplingResult(samplingDecision); } else { - return handleRootOtelSpan(traceId, parentContext, attributes); + return handleRootOtelSpan(traceId, parentContext, attributes, spanKind); } } } @@ -74,10 +74,14 @@ public SamplingResult shouldSample( private @NotNull SamplingResult handleRootOtelSpan( final @NotNull String traceId, final @NotNull Context parentContext, - final @NotNull Attributes attributes) { + final @NotNull Attributes attributes, + final @NotNull SpanKind spanKind) { if (!scopes.getOptions().isTracingEnabled()) { return SamplingResult.create(SamplingDecision.RECORD_ONLY); } + if (scopes.getOptions().isIgnoreStandaloneClientSpans() && SpanKind.CLIENT.equals(spanKind)) { + return SamplingResult.create(SamplingDecision.DROP); + } @Nullable Baggage baggage = null; @Nullable SentryTraceHeader sentryTraceHeader = parentContext.get(SentryOtelKeys.SENTRY_TRACE_KEY); diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt new file mode 100644 index 0000000000..2d61c833a0 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/SentryContextWrapperTest.kt @@ -0,0 +1,163 @@ +package io.sentry.opentelemetry + +import io.opentelemetry.api.trace.Span +import io.opentelemetry.api.trace.SpanContext +import io.opentelemetry.api.trace.TraceFlags +import io.opentelemetry.api.trace.TraceState +import io.opentelemetry.context.Context +import io.opentelemetry.sdk.trace.ReadWriteSpan +import io.opentelemetry.sdk.trace.data.SpanData +import io.opentelemetry.sdk.trace.data.StatusData +import io.sentry.Sentry +import io.sentry.SentryNanotimeDate +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + +class SentryContextWrapperTest { + + val spanStorage = SentryWeakSpanStorage.getInstance() + + @BeforeTest + fun setup() { + spanStorage.clear() + } + + @AfterTest + fun cleanup() { + spanStorage.clear() + Sentry.close() + } + + @Test + fun `returns global hub span if no transaction is available`() { + Sentry.init { + it.dsn = "https://key@sentry.io/proj" + it.isGlobalHubMode = true + } + + val c = SentryContextWrapper.wrap(Context.root()) + val returnedSpan = Span.fromContextOrNull(c) + assertTrue(returnedSpan is SentryOtelGlobalHubModeSpan) + } + + @Test + fun `returns available transaction`() { + Sentry.init { + it.dsn = "https://key@sentry.io/proj" + it.isGlobalHubMode = true + } + + val otelSpan = createOtelSpan() + val sentrySpan = createSentrySpan(otelSpan) + + spanStorage.storeSentrySpan(otelSpan.spanContext, sentrySpan) + + val c = SentryContextWrapper.wrap(Context.root()) + val returnedSpan = Span.fromContextOrNull(c) + assertSame(otelSpan, returnedSpan) + } + + @Test + fun `returns available transaction if span in context is invalid`() { + Sentry.init { + it.dsn = "https://key@sentry.io/proj" + it.isGlobalHubMode = true + } + + val otelSpan = createOtelSpan() + val sentrySpan = createSentrySpan(otelSpan) + + spanStorage.storeSentrySpan(otelSpan.spanContext, sentrySpan) + + val nonWrappedContext = Context.root() + val wrappedContext = SentryContextWrapper.wrap(Span.getInvalid().storeInContext(nonWrappedContext)) + val returnedSpan = Span.fromContextOrNull(wrappedContext) + assertSame(otelSpan, returnedSpan) + } + + @Test + fun `returns span from context if valid`() { + Sentry.init { + it.dsn = "https://key@sentry.io/proj" + it.isGlobalHubMode = true + } + + val otelSpan = createOtelSpan() + val otelSpanInContext = createOtelSpan() + val sentrySpan = createSentrySpan(otelSpan) + + spanStorage.storeSentrySpan(otelSpan.spanContext, sentrySpan) + + val nonWrappedContext = Context.root() + val wrappedContext = SentryContextWrapper.wrap(otelSpanInContext.storeInContext(nonWrappedContext)) + val returnedSpan = Span.fromContextOrNull(wrappedContext) + assertSame(otelSpanInContext, returnedSpan) + } + + @Test + fun `returns null if transaction is available but globalHubMode is false`() { + Sentry.init { + it.dsn = "https://key@sentry.io/proj" + it.isGlobalHubMode = false + } + + val otelSpan = createOtelSpan() + val sentrySpan = createSentrySpan(otelSpan) + + spanStorage.storeSentrySpan(otelSpan.spanContext, sentrySpan) + + val c = SentryContextWrapper.wrap(Context.root()) + val returnedSpan = Span.fromContextOrNull(c) + assertNull(returnedSpan) + } + + @Test + fun `returns null if available transaction is already finished`() { + Sentry.init { + it.dsn = "https://key@sentry.io/proj" + it.isGlobalHubMode = true + } + + val otelSpan = createOtelSpan() + val sentrySpan = createSentrySpan(otelSpan) + + spanStorage.storeSentrySpan(otelSpan.spanContext, sentrySpan) + sentrySpan.finish() + + val c = SentryContextWrapper.wrap(Context.root()) + val returnedSpan = Span.fromContextOrNull(c) + assertSame(otelSpan, returnedSpan) + } + + private fun createSentrySpan(otelSpan: ReadWriteSpan): OtelSpanWrapper { + val scopes = Sentry.getCurrentScopes() + return OtelSpanWrapper(otelSpan, scopes, SentryNanotimeDate(), null, null, null, null) + } + + private fun createOtelSpan(): ReadWriteSpan { + val otelSpanContext = SpanContext.create( + "f9118105af4a2d42b4124532cd1065ff", + "424cffc8f94feeee", + TraceFlags.getSampled(), + TraceState.getDefault() + ) + val otelSpan = mock() + whenever(otelSpan.spanContext).thenReturn(otelSpanContext) + whenever(otelSpan.name).thenReturn("some-name") + + val spanData = mock() + whenever(spanData.status).thenReturn(StatusData.ok()) + whenever(otelSpan.toSpanData()).thenReturn(spanData) + + whenever(otelSpan.storeInContext(any())).thenCallRealMethod() + + return otelSpan + } +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 78b5b0be2b..6c8174a5d2 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2535,6 +2535,7 @@ public final class io/sentry/Sentry { public static fun init (Ljava/lang/String;)V public static fun isCrashedLastRun ()Ljava/lang/Boolean; public static fun isEnabled ()Z + public static fun isGlobalHubMode ()Z public static fun isHealthy ()Z public static fun popScope ()V public static fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; @@ -3162,6 +3163,7 @@ public class io/sentry/SentryOptions { public fun isEnabled ()Z public fun isForceInit ()Z public fun isGlobalHubMode ()Ljava/lang/Boolean; + public fun isIgnoreStandaloneClientSpans ()Z public fun isPrintUncaughtStackTrace ()Z public fun isProfilingEnabled ()Z public fun isSendClientReports ()Z @@ -3222,6 +3224,7 @@ public class io/sentry/SentryOptions { public fun setGestureTargetLocators (Ljava/util/List;)V public fun setGlobalHubMode (Ljava/lang/Boolean;)V public fun setIdleTimeout (Ljava/lang/Long;)V + public fun setIgnoreStandaloneClientSpans (Z)V public fun setIgnoredCheckIns (Ljava/util/List;)V public fun setIgnoredErrors (Ljava/util/List;)V public fun setIgnoredSpanOrigins (Ljava/util/List;)V diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 70d4c7b380..421f296270 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1225,4 +1225,8 @@ public interface OptionsConfiguration { public static @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { return getCurrentScopes().captureCheckIn(checkIn); } + + public static boolean isGlobalHubMode() { + return globalHubMode; + } } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 54e37a405c..9b1205d749 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -568,6 +568,8 @@ public class SentryOptions { private @NotNull ISocketTagger socketTagger = NoOpSocketTagger.getInstance(); + private boolean ignoreStandaloneClientSpans = false; + /** * Adds an event processor * @@ -2847,6 +2849,16 @@ public void setSocketTagger(final @Nullable ISocketTagger socketTagger) { this.socketTagger = socketTagger != null ? socketTagger : NoOpSocketTagger.getInstance(); } + @ApiStatus.Experimental + public void setIgnoreStandaloneClientSpans(final boolean ignoreStandaloneClientSpans) { + this.ignoreStandaloneClientSpans = ignoreStandaloneClientSpans; + } + + @ApiStatus.Experimental + public boolean isIgnoreStandaloneClientSpans() { + return ignoreStandaloneClientSpans; + } + /** * 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.