Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static io.sentry.opentelemetry.OtelSpanUtils.maybeTransferOtelAttribute;

import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.trace.data.SpanData;
Expand All @@ -28,6 +29,7 @@
import io.sentry.SentryLevel;
import io.sentry.SentryLongDate;
import io.sentry.SpanContext;
import io.sentry.SpanDataConvention;
import io.sentry.SpanId;
import io.sentry.SpanOptions;
import io.sentry.SpanStatus;
Expand Down Expand Up @@ -240,6 +242,7 @@ private void createAndFinishSpanForOtelSpan(
setOtelInstrumentationInfo(spanData, sentryChildSpan);
setOtelSpanKind(spanData, sentryChildSpan);
transferSpanDetails(sentrySpanMaybe, sentryChildSpan);
setSentrySpanKind(spanData, sentryChildSpan);

for (SpanNode childNode : spanNode.getChildren()) {
createAndFinishSpanForOtelSpan(childNode, sentryChildSpan, remaining);
Expand Down Expand Up @@ -355,6 +358,10 @@ private void transferSpanDetails(
setOtelInstrumentationInfo(span, sentryTransaction);
setOtelSpanKind(span, sentryTransaction);
transferSpanDetails(sentrySpanMaybe, sentryTransaction);
setSentrySpanKind(span, sentryTransaction);
if (SpanKind.SERVER.equals(span.getKind())) {
maybeTransferOtelAttribute(span, sentryTransaction, HttpAttributes.HTTP_RESPONSE_STATUS_CODE);
}

maybeTransferOtelAttribute(span, sentryTransaction, ThreadIncubatingAttributes.THREAD_ID);
maybeTransferOtelAttribute(span, sentryTransaction, ThreadIncubatingAttributes.THREAD_NAME);
Expand Down Expand Up @@ -555,6 +562,13 @@ private void setOtelSpanKind(final @NotNull SpanData otelSpan, final @NotNull IS
sentrySpan.setData("otel.kind", otelSpan.getKind().name());
}

private void setSentrySpanKind(
final @NotNull SpanData otelSpan, final @NotNull ISpan sentrySpan) {
if (SpanKind.SERVER.equals(otelSpan.getKind())) {
sentrySpan.setData(SpanDataConvention.SENTRY_KIND_KEY, "server");
}
}

@Override
public CompletableResultCode flush() {
scopes.flush(10000);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package io.sentry.opentelemetry

import com.google.common.truth.Truth.assertThat
import io.opentelemetry.api.trace.SpanKind
import io.opentelemetry.context.Context
import io.opentelemetry.sdk.OpenTelemetrySdk
import io.opentelemetry.sdk.trace.SdkTracerProvider
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor
import io.opentelemetry.semconv.HttpAttributes
import io.sentry.ISentryClient
import io.sentry.NoOpScopes
import io.sentry.Sentry
import io.sentry.SentryOptions
import io.sentry.SpanDataConvention
import io.sentry.TransactionContext
import io.sentry.TransactionOptions
import io.sentry.protocol.SentryId
import io.sentry.protocol.SentryTransaction
import io.sentry.test.createSentryClientMock
import io.sentry.test.createTestScopes
import java.util.concurrent.TimeUnit
import kotlin.test.AfterTest
import kotlin.test.Test
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

class SentrySpanExporterTest {
private val options =
SentryOptions().apply {
dsn = "https://key@sentry.io/proj"
tracesSampleRate = 1.0
}
private val client: ISentryClient = createSentryClientMock()
private val scopes = createTestScopes(options).also { it.bindClient(client) }
private val exporter = SentrySpanExporter(scopes)

init {
whenever(
client.captureTransaction(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull())
)
.thenReturn(SentryId())
Sentry.setCurrentScopes(scopes)
}

@AfterTest
fun tearDown() {
Sentry.setCurrentScopes(NoOpScopes.getInstance())
}

@Test
fun `server roots and children get server kind and root response status`() {
withOpenTelemetry { openTelemetry ->
val tracer = openTelemetry.getTracer("test")
val root =
tracer
.spanBuilder("server-root")
.setSpanKind(SpanKind.SERVER)
.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 404L)
.startSpan()
val child =
tracer
.spanBuilder("server-child")
.setParent(root.storeInContext(Context.root()))
.setSpanKind(SpanKind.SERVER)
.startSpan()

child.end()
root.end()
}

val transaction = capturedTransactions().single()
assertThat(transaction.contexts.trace!!.data[SpanDataConvention.SENTRY_KIND_KEY])
.isEqualTo("server")
assertThat(transaction.contexts.trace!!.data[SpanDataConvention.HTTP_STATUS_CODE_KEY])
.isEqualTo(404L)
assertThat(transaction.contexts.trace!!.data["otel.kind"]).isEqualTo("SERVER")
assertThat(transaction.spans).hasSize(1)
assertThat(transaction.spans.single().data!![SpanDataConvention.SENTRY_KIND_KEY])
.isEqualTo("server")
assertThat(transaction.spans.single().data!!["otel.kind"]).isEqualTo("SERVER")
}

@Test
fun `client and internal roots do not get server data`() {
withOpenTelemetry { openTelemetry ->
val tracer = openTelemetry.getTracer("test")
tracer
.spanBuilder("client-root")
.setSpanKind(SpanKind.CLIENT)
.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 404L)
.startSpan()
.end()
tracer
.spanBuilder("internal-root")
.setSpanKind(SpanKind.INTERNAL)
.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, 500L)
.startSpan()
.end()
}

val transactions = capturedTransactions().associateBy { it.transaction }
assertThat(transactions.keys).containsExactly("client-root", "internal-root")
assertThat(transactions["client-root"]!!.contexts.trace!!.data)
.doesNotContainKey(SpanDataConvention.SENTRY_KIND_KEY)
assertThat(transactions["client-root"]!!.contexts.trace!!.data)
.doesNotContainKey(SpanDataConvention.HTTP_STATUS_CODE_KEY)
assertThat(transactions["internal-root"]!!.contexts.trace!!.data)
.doesNotContainKey(SpanDataConvention.SENTRY_KIND_KEY)
assertThat(transactions["internal-root"]!!.contexts.trace!!.data)
.doesNotContainKey(SpanDataConvention.HTTP_STATUS_CODE_KEY)
}

@Test
fun `rpc server root does not fabricate HTTP response status`() {
withOpenTelemetry { openTelemetry ->
openTelemetry
.getTracer("test")
.spanBuilder("rpc-server")
.setSpanKind(SpanKind.SERVER)
.startSpan()
.end()
}

val transaction = capturedTransactions().single()
assertThat(transaction.contexts.trace!!.data[SpanDataConvention.SENTRY_KIND_KEY])
.isEqualTo("server")
assertThat(transaction.contexts.trace!!.data)
.doesNotContainKey(SpanDataConvention.HTTP_STATUS_CODE_KEY)
}

@Test
fun `Sentry server marker survives default OTel internal kind`() {
withOpenTelemetry(includeSentrySpanProcessor = true) { openTelemetry ->
options.spanFactory = OtelSpanFactory(openTelemetry)
val transaction =
scopes.startTransaction(
TransactionContext("native-server", "http.server"),
TransactionOptions(),
)
transaction.setData(SpanDataConvention.SENTRY_KIND_KEY, "server")
transaction.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, 404)
transaction.finish()
}

val transaction = capturedTransactions().single()
assertThat(transaction.contexts.trace!!.data[SpanDataConvention.SENTRY_KIND_KEY])
.isEqualTo("server")
assertThat(transaction.contexts.trace!!.data[SpanDataConvention.HTTP_STATUS_CODE_KEY])
.isEqualTo(404)
assertThat(transaction.contexts.trace!!.data["otel.kind"]).isEqualTo("INTERNAL")
}

private fun withOpenTelemetry(
includeSentrySpanProcessor: Boolean = false,
callback: (OpenTelemetrySdk) -> Unit,
) {
val providerBuilder = SdkTracerProvider.builder()
if (includeSentrySpanProcessor) {
providerBuilder.addSpanProcessor(OtelSentrySpanProcessor(scopes))
}
val provider = providerBuilder.addSpanProcessor(SimpleSpanProcessor.create(exporter)).build()
val openTelemetry = OpenTelemetrySdk.builder().setTracerProvider(provider).build()
try {
callback(openTelemetry)
provider.forceFlush().join(10, TimeUnit.SECONDS)
} finally {
provider.close()
}
}

private fun capturedTransactions(): List<SentryTransaction> {
val transaction = argumentCaptor<SentryTransaction>()
verify(client, atLeastOnce())
.captureTransaction(
transaction.capture(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
anyOrNull(),
)
return transaction.allValues
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.sentry.ITransaction;
import io.sentry.ScopesAdapter;
import io.sentry.SentryTraceHeader;
import io.sentry.SpanDataConvention;
import io.sentry.SpanStatus;
import io.sentry.TransactionContext;
import io.sentry.TransactionOptions;
Expand Down Expand Up @@ -139,8 +140,10 @@ private void doFilterWithTransaction(
final @Nullable ITransaction transaction =
getOrStartTransaction(httpRequest, transactionContext);

boolean filterChainCompleted = false;
try {
filterChain.doFilter(httpRequest, httpResponse);
filterChainCompleted = true;
} catch (Throwable e) {
if (transaction != null) {
// exceptions that are not handled by Spring
Expand All @@ -164,6 +167,10 @@ private void doFilterWithTransaction(
if (transactionName != null) {
transaction.setName(transactionName, transactionNameSource);
transaction.setOperation(TRANSACTION_OP);
// An escaping exception may be mapped to an HTTP status later by the container.
if (filterChainCompleted) {
transaction.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, httpResponse.getStatus());
}
// if exception has been thrown, transaction status is already set to INTERNAL_ERROR, and
// httpResponse.getStatus() returns 200.
if (transaction.getStatus() == null) {
Expand Down Expand Up @@ -240,15 +247,20 @@ private ITransaction startTransaction(
transactionOptions.setBindToScope(true);
transactionOptions.setOrigin(TRACE_ORIGIN);

final @NotNull ITransaction transaction;
if (transactionContext != null) {
transactionContext.setName(name);
transactionContext.setTransactionNameSource(TransactionNameSource.URL);
transactionContext.setOperation("http.server");

return scopes.startTransaction(transactionContext, transactionOptions);
transaction = scopes.startTransaction(transactionContext, transactionOptions);
} else {
transaction =
scopes.startTransaction(
new TransactionContext(name, TransactionNameSource.URL, "http.server"),
transactionOptions);
}

return scopes.startTransaction(
new TransactionContext(name, TransactionNameSource.URL, "http.server"), transactionOptions);
transaction.setData(SpanDataConvention.SENTRY_KIND_KEY, "server");
return transaction;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.sentry.NoOpScopes;
import io.sentry.Sentry;
import io.sentry.SentryTraceHeader;
import io.sentry.SpanDataConvention;
import io.sentry.SpanStatus;
import io.sentry.TransactionContext;
import io.sentry.TransactionOptions;
Expand Down Expand Up @@ -126,6 +127,7 @@ private void finishTransaction(ServerWebExchange exchange, ITransaction transact
if (response != null) {
final @Nullable HttpStatusCode statusCode = response.getStatusCode();
if (statusCode != null) {
transaction.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, statusCode.value());
transaction
.getContexts()
.withResponse(
Expand Down Expand Up @@ -154,16 +156,20 @@ private void finishTransaction(ServerWebExchange exchange, ITransaction transact
transactionOptions.setBindToScope(true);
transactionOptions.setOrigin(origin);

final @NotNull ITransaction transaction;
if (transactionContext != null) {
transactionContext.setName(name);
transactionContext.setTransactionNameSource(TransactionNameSource.URL);
transactionContext.setOperation(TRANSACTION_OP);

return scopes.startTransaction(transactionContext, transactionOptions);
transaction = scopes.startTransaction(transactionContext, transactionOptions);
} else {
transaction =
scopes.startTransaction(
new TransactionContext(name, TransactionNameSource.URL, TRANSACTION_OP),
transactionOptions);
}

return scopes.startTransaction(
new TransactionContext(name, TransactionNameSource.URL, TRANSACTION_OP),
transactionOptions);
transaction.setData(SpanDataConvention.SENTRY_KIND_KEY, "server");
return transaction;
}
}
Loading
Loading