Skip to content

Support globalHubMode for OpenTelemetry #4349

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

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 🚫 The changelog entry seems to be part of an already released section ## 8.11.0.
    Consider moving the entry to the ## Unreleased section, please.

- 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`

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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;
public abstract fun getTraceId ()Lio/sentry/protocol/SentryId;
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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -151,6 +156,7 @@ public final class io/sentry/opentelemetry/SentryContextStorage : io/opentelemet
public fun <init> (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 {
Expand All @@ -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 <init> ()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;
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -52,4 +53,9 @@ public interface IOtelSpanWrapper extends ISpan {
@ApiStatus.Internal
@Nullable
Attributes getOpenTelemetrySpanAttributes();

boolean isRoot();

@Nullable
Span getOpenTelemetrySpan();
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ public final class InternalSemanticAttributes {
public static final AttributeKey<String> BAGGAGE = AttributeKey.stringKey("sentry.baggage");
public static final AttributeKey<Boolean> BAGGAGE_MUTABLE =
AttributeKey.booleanKey("sentry.baggage_mutable");
public static final AttributeKey<Boolean> CREATED_VIA_SENTRY_API =
AttributeKey.booleanKey("sentry.is_created_via_sentry_api");
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,43 @@ private SentryContextWrapper(final @NotNull Context delegate) {
}

@Override
public <V> V get(final @NotNull ContextKey<V> contextKey) {
return delegate.get(contextKey);
public <V> @Nullable V get(final @NotNull ContextKey<V> contextKey) {
final @Nullable V result = delegate.get(contextKey);
if (shouldReturnRootSpanInstead(contextKey, result)) {
return returnUnfinishedRootSpanIfAvailable(result);
}
return result;
}

private <V> boolean shouldReturnRootSpanInstead(
final @NotNull ContextKey<V> contextKey, final @Nullable V result) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(l) not sure about the param naming. Would something other than result be more fitting?

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 <V> @Nullable V returnUnfinishedRootSpanIfAvailable(final @Nullable V result) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(l) not sure about the param naming. Would something other than result be more fitting?

final @Nullable IOtelSpanWrapper sentrySpan =
SentryWeakSpanStorage.getInstance().getLastKnownUnfinishedRootSpan();
if (sentrySpan != null) {
try {
return (V) sentrySpan.getOpenTelemetrySpan();
} catch (Throwable t) {
return result;
}
}
return result;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T> Span setAttribute(AttributeKey<T> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<SpanContext, IOtelSpanWrapper> sentrySpans =
new WeakConcurrentMap<>(true);
private volatile @NotNull WeakReference<IOtelSpanWrapper> lastKnownRootSpan =
new WeakReference<>(null);

private SentryWeakSpanStorage() {}

Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading