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 cff2574792..f8abed919e 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api @@ -16,6 +16,12 @@ public final class io/sentry/opentelemetry/OtelContextScopesStorage : io/sentry/ public fun set (Lio/sentry/IScopes;)Lio/sentry/ISentryLifecycleToken; } +public final class io/sentry/opentelemetry/OtelSpanContext : io/sentry/SpanContext { + public fun (Lio/opentelemetry/sdk/trace/ReadWriteSpan;Lio/opentelemetry/api/trace/Span;)V + public fun getStatus ()Lio/sentry/SpanStatus; + public fun setStatus (Lio/sentry/SpanStatus;)V +} + public final class io/sentry/opentelemetry/OtelSpanFactory : io/sentry/ISpanFactory { public fun ()V public fun createSpan (Ljava/lang/String;Ljava/lang/String;Lio/sentry/IScopes;Lio/sentry/SpanOptions;Lio/sentry/ISpan;)Lio/sentry/ISpan; diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanContext.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanContext.java new file mode 100644 index 0000000000..e33bcf061e --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanContext.java @@ -0,0 +1,88 @@ +package io.sentry.opentelemetry; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.sdk.trace.ReadWriteSpan; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.sentry.SpanContext; +import io.sentry.SpanId; +import io.sentry.SpanStatus; +import io.sentry.protocol.SentryId; +import java.lang.ref.WeakReference; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class OtelSpanContext extends SpanContext { + + /** + * OpenTelemetry span which this wrapper wraps. Needs to be referenced weakly as otherwise we'd + * create a circular reference from {@link io.opentelemetry.sdk.trace.data.SpanData} to {@link + * OtelSpanWrapper} and indirectly back to {@link io.opentelemetry.sdk.trace.data.SpanData} via + * {@link Span}. Also see {@link SentryWeakSpanStorage}. + */ + private final @NotNull WeakReference span; + + public OtelSpanContext(final @NotNull ReadWriteSpan span, final @Nullable Span parentSpan) { + // TODO [POTEL] tracesSamplingDecision + super( + new SentryId(span.getSpanContext().getTraceId()), + new SpanId(span.getSpanContext().getSpanId()), + parentSpan == null ? null : new SpanId(parentSpan.getSpanContext().getSpanId()), + span.getName(), + null, + null, + null, + null); + this.span = new WeakReference<>(span); + } + + @Override + public @Nullable SpanStatus getStatus() { + final @Nullable ReadWriteSpan otelSpan = span.get(); + + if (otelSpan != null) { + final @NotNull StatusData otelStatus = otelSpan.toSpanData().getStatus(); + final @NotNull String otelStatusDescription = otelStatus.getDescription(); + if (otelStatusDescription.isEmpty()) { + return otelStatusCodeFallback(otelStatus); + } + final @Nullable SpanStatus spanStatus = SpanStatus.fromApiNameSafely(otelStatusDescription); + if (spanStatus == null) { + return otelStatusCodeFallback(otelStatus); + } + return spanStatus; + } + + return null; + } + + @Override + public void setStatus(@Nullable SpanStatus status) { + if (status != null) { + final @Nullable ReadWriteSpan otelSpan = span.get(); + if (otelSpan != null) { + final @NotNull StatusCode statusCode = translateStatusCode(status); + otelSpan.setStatus(statusCode, status.apiName()); + } + } + } + + private @Nullable SpanStatus otelStatusCodeFallback(final @NotNull StatusData otelStatus) { + if (otelStatus.getStatusCode() == StatusCode.ERROR) { + return SpanStatus.UNKNOWN_ERROR; + } else if (otelStatus.getStatusCode() == StatusCode.OK) { + return SpanStatus.OK; + } + return null; + } + + private @NotNull StatusCode translateStatusCode(final @Nullable SpanStatus status) { + if (status == null) { + return StatusCode.UNSET; + } else if (status == SpanStatus.OK) { + return StatusCode.OK; + } else { + return StatusCode.ERROR; + } + } +} diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java index 2edaa88382..2c363555c1 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanWrapper.java @@ -82,14 +82,7 @@ public OtelSpanWrapper( this.scopes = Objects.requireNonNull(scopes, "scopes are required"); this.span = new WeakReference<>(span); this.startTimestamp = startTimestamp; - final @NotNull SentryId traceId = new SentryId(span.getSpanContext().getTraceId()); - final @NotNull SpanId spanId = new SpanId(span.getSpanContext().getSpanId()); - final @Nullable SpanId parentSpanId = - parentSpan == null ? null : new SpanId(parentSpan.getSpanContext().getSpanId()); - @NotNull String operation = span.getName(); - - // TODO [POTEL] tracesSamplingDecision - this.context = new SpanContext(traceId, spanId, operation, parentSpanId, null); + this.context = new OtelSpanContext(span, parentSpan); } @Override @@ -228,15 +221,12 @@ public void setDescription(@Nullable String description) { } @Override - public void setStatus(@Nullable SpanStatus status) { - // TODO [POTEL] need to find a way to transfer data from this wrapper to SpanExporter - // ^ could go in span attributes - this.context.setStatus(status); + public void setStatus(final @Nullable SpanStatus status) { + context.setStatus(status); } @Override public @Nullable SpanStatus getStatus() { - // TODO [POTEL] need to find a way to transfer data from this wrapper to SpanExporter return context.getStatus(); } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java index 3775eec559..9387489888 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java @@ -198,7 +198,8 @@ private List maybeSend(final @NotNull List spans) { // spanStorage.getScope() // transaction.finishWithScope - transaction.finish(mapOtelStatus(span), new SentryLongDate(span.getEndEpochNanos())); + transaction.finish( + mapOtelStatus(span, transaction), new SentryLongDate(span.getEndEpochNanos())); } return remaining.stream() @@ -244,6 +245,9 @@ private void createAndFinishSpanForOtelSpan( parentSentrySpan.startChild( spanInfo.getOp(), spanInfo.getDescription(), startDate, Instrumenter.OTEL); + // TODO [POTEL] Check if we want to use `instrumentationScopeInfo.name` and append it to + // `auto.otel` + // TODO [POTEL] For spans manually created via Sentry API we should set manual, not auto.otel sentryChildSpan.getSpanContext().setOrigin(TRACE_ORIGN); for (Map.Entry dataField : spanInfo.getDataFields().entrySet()) { sentryChildSpan.setData(dataField.getKey(), dataField.getValue()); @@ -256,7 +260,7 @@ private void createAndFinishSpanForOtelSpan( } sentryChildSpan.finish( - mapOtelStatus(spanData), new SentryLongDate(spanData.getEndEpochNanos())); + mapOtelStatus(spanData, sentryChildSpan), new SentryLongDate(spanData.getEndEpochNanos())); } private void transferSpanDetails( @@ -285,6 +289,8 @@ private void transferSpanDetails( for (Map.Entry entry : tags.entrySet()) { targetSpan.setTag(entry.getKey(), entry.getValue()); } + + targetSpan.setStatus(sourceSpan.getStatus()); } } @@ -463,7 +469,14 @@ private void createOrUpdateSpanNodeAndRefs( } @SuppressWarnings("deprecation") - private SpanStatus mapOtelStatus(final @NotNull SpanData otelSpanData) { + private SpanStatus mapOtelStatus( + final @NotNull SpanData otelSpanData, final @NotNull ISpan sentrySpan) { + final @Nullable SpanStatus existingStatus = sentrySpan.getStatus(); + // TODO [POTEL] do we want the unknown error check here? + if (existingStatus != null && existingStatus != SpanStatus.UNKNOWN_ERROR) { + return existingStatus; + } + final @NotNull StatusData otelStatus = otelSpanData.getStatus(); final @NotNull StatusCode otelStatusCode = otelStatus.getStatusCode(); diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 4eff14881e..4c0b1a230d 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3255,6 +3255,8 @@ public final class io/sentry/SpanStatus : java/lang/Enum, io/sentry/JsonSerializ public static final field UNIMPLEMENTED Lio/sentry/SpanStatus; public static final field UNKNOWN Lio/sentry/SpanStatus; public static final field UNKNOWN_ERROR Lio/sentry/SpanStatus; + public fun apiName ()Ljava/lang/String; + public static fun fromApiNameSafely (Ljava/lang/String;)Lio/sentry/SpanStatus; public static fun fromHttpStatusCode (I)Lio/sentry/SpanStatus; public static fun fromHttpStatusCode (Ljava/lang/Integer;Lio/sentry/SpanStatus;)Lio/sentry/SpanStatus; public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index be428708cb..5d00b8d59b 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -145,6 +145,7 @@ public SpanId getParentSpanId() { } public @NotNull String getOperation() { + // TODO [POTEL] use span name here return op; } @@ -223,12 +224,12 @@ public boolean equals(Object o) { && Objects.equals(parentSpanId, that.parentSpanId) && op.equals(that.op) && Objects.equals(description, that.description) - && status == that.status; + && getStatus() == that.getStatus(); } @Override public int hashCode() { - return Objects.hash(traceId, spanId, parentSpanId, op, description, status); + return Objects.hash(traceId, spanId, parentSpanId, op, description, getStatus()); } // region JsonSerializable @@ -260,8 +261,8 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (description != null) { writer.name(JsonKeys.DESCRIPTION).value(description); } - if (status != null) { - writer.name(JsonKeys.STATUS).value(logger, status); + if (getStatus() != null) { + writer.name(JsonKeys.STATUS).value(logger, getStatus()); } if (origin != null) { writer.name(JsonKeys.ORIGIN).value(logger, origin); diff --git a/sentry/src/main/java/io/sentry/SpanStatus.java b/sentry/src/main/java/io/sentry/SpanStatus.java index b0b1bf78c8..37991abd67 100644 --- a/sentry/src/main/java/io/sentry/SpanStatus.java +++ b/sentry/src/main/java/io/sentry/SpanStatus.java @@ -103,12 +103,27 @@ private boolean matches(int httpStatusCode) { return httpStatusCode >= minHttpStatusCode && httpStatusCode <= maxHttpStatusCode; } + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } + + public static @Nullable SpanStatus fromApiNameSafely(final @Nullable String apiName) { + if (apiName == null) { + return null; + } + try { + return SpanStatus.valueOf(apiName.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ex) { + return null; + } + } + // JsonSerializable @Override public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) throws IOException { - writer.value(name().toLowerCase(Locale.ROOT)); + writer.value(apiName()); } public static final class Deserializer implements JsonDeserializer {