From 917b2e38b358b4909be5e79c3d082308bb75241a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 17 Dec 2024 15:57:16 +0100 Subject: [PATCH] Hold a strong reference to OTel span if created via Sentry API --- .../api/sentry-opentelemetry-bootstrap.api | 52 +++ .../sentry/opentelemetry/OtelSpanFactory.java | 6 +- .../OtelStrongRefSpanWrapper.java | 306 ++++++++++++++++++ 3 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java 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 44614f17b4..df7db47c65 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api @@ -39,6 +39,58 @@ public final class io/sentry/opentelemetry/OtelSpanFactory : io/sentry/ISpanFact public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/TransactionPerformanceCollector;)Lio/sentry/ITransaction; } +public final class io/sentry/opentelemetry/OtelStrongRefSpanWrapper : io/sentry/opentelemetry/IOtelSpanWrapper { + public fun (Lio/opentelemetry/api/trace/Span;Lio/sentry/opentelemetry/IOtelSpanWrapper;)V + public fun finish ()V + public fun finish (Lio/sentry/SpanStatus;)V + public fun finish (Lio/sentry/SpanStatus;Lio/sentry/SentryDate;)V + public fun getContexts ()Lio/sentry/protocol/Contexts; + public fun getData ()Ljava/util/Map; + public fun getData (Ljava/lang/String;)Ljava/lang/Object; + public fun getDescription ()Ljava/lang/String; + public fun getFinishDate ()Lio/sentry/SentryDate; + public fun getMeasurements ()Ljava/util/Map; + public fun getOperation ()Ljava/lang/String; + public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; + public fun getScopes ()Lio/sentry/IScopes; + public fun getSpanContext ()Lio/sentry/SpanContext; + public fun getStartDate ()Lio/sentry/SentryDate; + public fun getStatus ()Lio/sentry/SpanStatus; + public fun getTag (Ljava/lang/String;)Ljava/lang/String; + public fun getTags ()Ljava/util/Map; + public fun getThrowable ()Ljava/lang/Throwable; + public fun getTraceId ()Lio/sentry/protocol/SentryId; + public fun getTransactionName ()Ljava/lang/String; + public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; + public fun isFinished ()Z + public fun isNoOp ()Z + public fun isProfileSampled ()Ljava/lang/Boolean; + public fun isSampled ()Ljava/lang/Boolean; + public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun setContext (Ljava/lang/String;Ljava/lang/Object;)V + public fun setData (Ljava/lang/String;Ljava/lang/Object;)V + public fun setDescription (Ljava/lang/String;)V + public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;)V + public fun setMeasurement (Ljava/lang/String;Ljava/lang/Number;Lio/sentry/MeasurementUnit;)V + public fun setOperation (Ljava/lang/String;)V + public fun setStatus (Lio/sentry/SpanStatus;)V + public fun setTag (Ljava/lang/String;Ljava/lang/String;)V + public fun setThrowable (Ljava/lang/Throwable;)V + public fun setTransactionName (Ljava/lang/String;)V + public fun setTransactionName (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V + public fun startChild (Lio/sentry/SpanContext;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SentryDate;Lio/sentry/Instrumenter;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun startChild (Ljava/lang/String;Ljava/lang/String;Lio/sentry/SpanOptions;)Lio/sentry/ISpan; + public fun storeInContext (Lio/opentelemetry/context/Context;)Lio/opentelemetry/context/Context; + public fun toBaggageHeader (Ljava/util/List;)Lio/sentry/BaggageHeader; + public fun toSentryTrace ()Lio/sentry/SentryTraceHeader; + public fun traceContext ()Lio/sentry/TraceContext; + public fun updateEndDate (Lio/sentry/SentryDate;)Z +} + public final class io/sentry/opentelemetry/OtelTransactionSpanForwarder : io/sentry/ITransaction { public fun (Lio/sentry/opentelemetry/IOtelSpanWrapper;)V public fun finish ()V diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java index 402b83bbfa..547463dcdc 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java @@ -158,7 +158,11 @@ public OtelSpanFactory() { sentrySpan.getSpanContext().setOrigin(spanOptions.getOrigin()); } - return sentrySpan; + if (sentrySpan == null) { + return null; + } else { + return new OtelStrongRefSpanWrapper(otelSpan, sentrySpan); + } } private @NotNull Tracer getTracer() { 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 new file mode 100644 index 0000000000..fb02bd8082 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelStrongRefSpanWrapper.java @@ -0,0 +1,306 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.sentry.BaggageHeader; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; +import io.sentry.ISpan; +import io.sentry.Instrumenter; +import io.sentry.MeasurementUnit; +import io.sentry.SentryDate; +import io.sentry.SentryTraceHeader; +import io.sentry.SpanContext; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import io.sentry.TraceContext; +import io.sentry.TracesSamplingDecision; +import io.sentry.protocol.Contexts; +import io.sentry.protocol.MeasurementValue; +import io.sentry.protocol.SentryId; +import io.sentry.protocol.TransactionNameSource; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * This holds a strong reference to the OpenTelemetry span, preventing it from being garbage + * collected. + * + *

IMPORTANT: Only use this carefully. Please read below. + * + *

This class should only be used in cases where Sentry SDK is used to create an OpenTelemetry + * span under the hood that no one holds a reference to otherwise. + * + *

e.g. ITransaction transaction = Sentry.startTransaction(...) Sentry creates an OTel span under + * the hood, but no one would reference it unless this class is used and returned to the user. By + * doing this, we tie the OTel span to the returned Sentry span/transaction which the user can hold + * on to. + */ +@ApiStatus.Internal +public final class OtelStrongRefSpanWrapper implements IOtelSpanWrapper { + + @SuppressWarnings("UnusedVariable") + private final @NotNull Span otelSpan; + + private final @NotNull IOtelSpanWrapper delegate; + + public OtelStrongRefSpanWrapper(@NotNull Span otelSpan, IOtelSpanWrapper delegate) { + this.otelSpan = otelSpan; + this.delegate = delegate; + } + + @Override + public void setTransactionName(@NotNull String name) { + delegate.setTransactionName(name); + } + + @Override + public void setTransactionName(@NotNull String name, @NotNull TransactionNameSource nameSource) { + delegate.setTransactionName(name, nameSource); + } + + @Override + public @Nullable TransactionNameSource getTransactionNameSource() { + return delegate.getTransactionNameSource(); + } + + @Override + public @Nullable String getTransactionName() { + return delegate.getTransactionName(); + } + + @Override + public @NotNull SentryId getTraceId() { + return delegate.getTraceId(); + } + + @Override + public @NotNull Map getData() { + return delegate.getData(); + } + + @Override + public @NotNull Map getMeasurements() { + return delegate.getMeasurements(); + } + + @Override + public @Nullable Boolean isProfileSampled() { + return delegate.isProfileSampled(); + } + + @Override + public @NotNull IScopes getScopes() { + return delegate.getScopes(); + } + + @Override + public @NotNull Map getTags() { + return delegate.getTags(); + } + + @Override + public @NotNull Context storeInContext(Context context) { + return delegate.storeInContext(context); + } + + @Override + public @NotNull ISpan startChild(@NotNull String operation) { + return delegate.startChild(operation); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, @Nullable String description, @NotNull SpanOptions spanOptions) { + return delegate.startChild(operation, description, spanOptions); + } + + @Override + public @NotNull ISpan startChild( + @NotNull SpanContext spanContext, @NotNull SpanOptions spanOptions) { + return delegate.startChild(spanContext, spanOptions); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, + @Nullable String description, + @Nullable SentryDate timestamp, + @NotNull Instrumenter instrumenter) { + return delegate.startChild(operation, description, timestamp, instrumenter); + } + + @Override + public @NotNull ISpan startChild( + @NotNull String operation, + @Nullable String description, + @Nullable SentryDate timestamp, + @NotNull Instrumenter instrumenter, + @NotNull SpanOptions spanOptions) { + return delegate.startChild(operation, description, timestamp, instrumenter, spanOptions); + } + + @Override + public @NotNull ISpan startChild(@NotNull String operation, @Nullable String description) { + return delegate.startChild(operation, description); + } + + @Override + public @NotNull SentryTraceHeader toSentryTrace() { + return delegate.toSentryTrace(); + } + + @Override + public @Nullable TraceContext traceContext() { + return delegate.traceContext(); + } + + @Override + public @Nullable BaggageHeader toBaggageHeader(@Nullable List thirdPartyBaggageHeaders) { + return delegate.toBaggageHeader(thirdPartyBaggageHeaders); + } + + @Override + public void finish() { + delegate.finish(); + } + + @Override + public void finish(@Nullable SpanStatus status) { + delegate.finish(status); + } + + @Override + public void finish(@Nullable SpanStatus status, @Nullable SentryDate timestamp) { + delegate.finish(status, timestamp); + } + + @Override + public void setOperation(@NotNull String operation) { + delegate.setOperation(operation); + } + + @Override + public @NotNull String getOperation() { + return delegate.getOperation(); + } + + @Override + public void setDescription(@Nullable String description) { + delegate.setDescription(description); + } + + @Override + public @Nullable String getDescription() { + return delegate.getDescription(); + } + + @Override + public void setStatus(@Nullable SpanStatus status) { + delegate.setStatus(status); + } + + @Override + public @Nullable SpanStatus getStatus() { + return delegate.getStatus(); + } + + @Override + public void setThrowable(@Nullable Throwable throwable) { + delegate.setThrowable(throwable); + } + + @Override + public @Nullable Throwable getThrowable() { + return delegate.getThrowable(); + } + + @Override + public @NotNull SpanContext getSpanContext() { + return delegate.getSpanContext(); + } + + @Override + public void setTag(@NotNull String key, @NotNull String value) { + delegate.setTag(key, value); + } + + @Override + public @Nullable String getTag(@NotNull String key) { + return delegate.getTag(key); + } + + @Override + public boolean isFinished() { + return delegate.isFinished(); + } + + @Override + public void setData(@NotNull String key, @NotNull Object value) { + delegate.setData(key, value); + } + + @Override + public @Nullable Object getData(@NotNull String key) { + return delegate.getData(key); + } + + @Override + public void setMeasurement(@NotNull String name, @NotNull Number value) { + delegate.setMeasurement(name, value); + } + + @Override + public void setMeasurement( + @NotNull String name, @NotNull Number value, @NotNull MeasurementUnit unit) { + delegate.setMeasurement(name, value, unit); + } + + @Override + public boolean updateEndDate(@NotNull SentryDate date) { + return delegate.updateEndDate(date); + } + + @Override + public @NotNull SentryDate getStartDate() { + return delegate.getStartDate(); + } + + @Override + public @Nullable SentryDate getFinishDate() { + return delegate.getFinishDate(); + } + + @Override + public boolean isNoOp() { + return delegate.isNoOp(); + } + + @Override + public void setContext(@NotNull String key, @NotNull Object context) { + delegate.setContext(key, context); + } + + @Override + public @NotNull Contexts getContexts() { + return delegate.getContexts(); + } + + @Override + public @Nullable Boolean isSampled() { + return delegate.isSampled(); + } + + @Override + public @Nullable TracesSamplingDecision getSamplingDecision() { + return delegate.getSamplingDecision(); + } + + @Override + public @NotNull ISentryLifecycleToken makeCurrent() { + return delegate.makeCurrent(); + } +}