From f951e323f65e211eae1330c614ba0038c4ed6712 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 5 Mar 2025 12:20:56 +0100 Subject: [PATCH 1/4] Also use port when checking if a request is made to Sentry DSN --- .../OpenTelemetryAttributesExtractor.java | 7 + .../OtelInternalSpanDetectionUtil.java | 18 +- .../OpenTelemetryAttributesExtractorTest.kt | 14 + .../OtelInternalSpanDetectionUtilTest.kt | 251 ++++++++++++++++++ sentry/src/main/java/io/sentry/DsnUtil.java | 9 +- sentry/src/test/java/io/sentry/DsnUtilTest.kt | 10 + 6 files changed, 295 insertions(+), 14 deletions(-) create mode 100644 sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelInternalSpanDetectionUtilTest.kt diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryAttributesExtractor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryAttributesExtractor.java index 87088ae2377..7d4db373df8 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryAttributesExtractor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OpenTelemetryAttributesExtractor.java @@ -105,6 +105,7 @@ private static Map collectHeaders( return headers; } + @SuppressWarnings("deprecation") public @Nullable String extractUrl( final @NotNull Attributes attributes, final @NotNull SentryOptions options) { final @Nullable String urlFull = attributes.get(UrlAttributes.URL_FULL); @@ -112,6 +113,12 @@ private static Map collectHeaders( return urlFull; } + final @Nullable String deprecatedUrl = + attributes.get(io.opentelemetry.semconv.SemanticAttributes.HTTP_URL); + if (deprecatedUrl != null) { + return deprecatedUrl; + } + final String urlString = buildUrlString(attributes, options); if (!urlString.isEmpty()) { return urlString; diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelInternalSpanDetectionUtil.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelInternalSpanDetectionUtil.java index e4bd5a7e7c8..b1bbdede526 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelInternalSpanDetectionUtil.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelInternalSpanDetectionUtil.java @@ -2,7 +2,6 @@ import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.semconv.UrlAttributes; import io.sentry.DsnUtil; import io.sentry.IScopes; import java.util.Arrays; @@ -17,6 +16,8 @@ public final class OtelInternalSpanDetectionUtil { private static final @NotNull List spanKindsConsideredForSentryRequests = Arrays.asList(SpanKind.CLIENT, SpanKind.INTERNAL); + private static final @NotNull OpenTelemetryAttributesExtractor attributesExtractor = + new OpenTelemetryAttributesExtractor(); @SuppressWarnings("deprecation") public static boolean isSentryRequest( @@ -27,14 +28,8 @@ public static boolean isSentryRequest( return false; } - final @Nullable String httpUrl = - attributes.get(io.opentelemetry.semconv.SemanticAttributes.HTTP_URL); - if (DsnUtil.urlContainsDsnHost(scopes.getOptions(), httpUrl)) { - return true; - } - - final @Nullable String fullUrl = attributes.get(UrlAttributes.URL_FULL); - if (DsnUtil.urlContainsDsnHost(scopes.getOptions(), fullUrl)) { + String url = attributesExtractor.extractUrl(attributes, scopes.getOptions()); + if (DsnUtil.urlContainsDsnHost(scopes.getOptions(), url)) { return true; } @@ -43,10 +38,7 @@ public static boolean isSentryRequest( final @NotNull String spotlightUrl = optionsSpotlightUrl != null ? optionsSpotlightUrl : "http://localhost:8969/stream"; - if (containsSpotlightUrl(fullUrl, spotlightUrl)) { - return true; - } - if (containsSpotlightUrl(httpUrl, spotlightUrl)) { + if (containsSpotlightUrl(url, spotlightUrl)) { return true; } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OpenTelemetryAttributesExtractorTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OpenTelemetryAttributesExtractorTest.kt index 1227509e0d0..e235ba8ca0b 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OpenTelemetryAttributesExtractorTest.kt +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OpenTelemetryAttributesExtractorTest.kt @@ -4,6 +4,7 @@ import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.sdk.internal.AttributesMap import io.opentelemetry.sdk.trace.data.SpanData import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.SemanticAttributes import io.opentelemetry.semconv.ServerAttributes import io.opentelemetry.semconv.UrlAttributes import io.sentry.Scope @@ -202,6 +203,19 @@ class OpenTelemetryAttributesExtractorTest { assertEquals("https://sentry.io/some/path", url) } + @Test + fun `returns deprecated URL if present`() { + givenAttributes( + mapOf( + SemanticAttributes.HTTP_URL to "https://sentry.io/some/path" + ) + ) + + val url = whenExtractingUrl() + + assertEquals("https://sentry.io/some/path", url) + } + @Test fun `returns reconstructed URL if attributes present`() { givenAttributes( diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelInternalSpanDetectionUtilTest.kt b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelInternalSpanDetectionUtilTest.kt new file mode 100644 index 00000000000..6cc62dd1a0e --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/test/kotlin/OtelInternalSpanDetectionUtilTest.kt @@ -0,0 +1,251 @@ +package io.sentry.opentelemetry + +import io.opentelemetry.api.common.AttributeKey +import io.opentelemetry.api.trace.SpanKind +import io.opentelemetry.sdk.internal.AttributesMap +import io.opentelemetry.semconv.HttpAttributes +import io.opentelemetry.semconv.SemanticAttributes +import io.opentelemetry.semconv.ServerAttributes +import io.opentelemetry.semconv.UrlAttributes +import io.sentry.IScopes +import io.sentry.SentryOptions +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class OtelInternalSpanDetectionUtilTest { + + private class Fixture { + val scopes = mock() + val attributes = AttributesMap.create(100, 100) + val options = SentryOptions.empty() + var spanKind: SpanKind = SpanKind.INTERNAL + + init { + whenever(scopes.options).thenReturn(options) + } + } + + private val fixture = Fixture() + + @Test + fun `detects split url as internal (span kind client)`() { + givenDsn("https://publicKey:secretKey@io.sentry:8081/path/id?sample.rate=0.1") + givenSpanKind(SpanKind.CLIENT) + givenAttributes( + mapOf( + HttpAttributes.HTTP_REQUEST_METHOD to "GET", + UrlAttributes.URL_SCHEME to "https", + UrlAttributes.URL_PATH to "/path/to/123", + UrlAttributes.URL_QUERY to "q=123456&b=X", + ServerAttributes.SERVER_ADDRESS to "io.sentry", + ServerAttributes.SERVER_PORT to 8081L + ) + ) + + thenRequestIsConsideredInternal() + } + + @Test + fun `detects full url as internal (span kind client)`() { + givenDsn("https://publicKey:secretKey@io.sentry:8081/path/id?sample.rate=0.1") + givenSpanKind(SpanKind.CLIENT) + givenAttributes( + mapOf( + UrlAttributes.URL_FULL to "https://io.sentry:8081" + ) + ) + + thenRequestIsConsideredInternal() + } + + @Test + fun `detects deprecated url as internal (span kind client)`() { + givenDsn("https://publicKey:secretKey@io.sentry:8081/path/id?sample.rate=0.1") + givenSpanKind(SpanKind.CLIENT) + givenAttributes( + mapOf( + SemanticAttributes.HTTP_URL to "https://io.sentry:8081" + ) + ) + + thenRequestIsConsideredInternal() + } + + @Test + fun `detects split url as internal (span kind internal)`() { + givenDsn("https://publicKey:secretKey@io.sentry:8081/path/id?sample.rate=0.1") + givenSpanKind(SpanKind.INTERNAL) + givenAttributes( + mapOf( + HttpAttributes.HTTP_REQUEST_METHOD to "GET", + UrlAttributes.URL_SCHEME to "https", + UrlAttributes.URL_PATH to "/path/to/123", + UrlAttributes.URL_QUERY to "q=123456&b=X", + ServerAttributes.SERVER_ADDRESS to "io.sentry", + ServerAttributes.SERVER_PORT to 8081L + ) + ) + + thenRequestIsConsideredInternal() + } + + @Test + fun `detects full url as internal (span kind internal)`() { + givenDsn("https://publicKey:secretKey@io.sentry:8081/path/id?sample.rate=0.1") + givenSpanKind(SpanKind.INTERNAL) + givenAttributes( + mapOf( + UrlAttributes.URL_FULL to "https://io.sentry:8081" + ) + ) + + thenRequestIsConsideredInternal() + } + + @Test + fun `detects deprecated url as internal (span kind internal)`() { + givenDsn("https://publicKey:secretKey@io.sentry:8081/path/id?sample.rate=0.1") + givenSpanKind(SpanKind.INTERNAL) + givenAttributes( + mapOf( + SemanticAttributes.HTTP_URL to "https://io.sentry:8081" + ) + ) + + thenRequestIsConsideredInternal() + } + + @Test + fun `does not detect full url as internal (span kind server)`() { + givenDsn("https://publicKey:secretKey@io.sentry:8081/path/id?sample.rate=0.1") + givenSpanKind(SpanKind.SERVER) + givenAttributes( + mapOf( + UrlAttributes.URL_FULL to "https://io.sentry:8081" + ) + ) + + thenRequestIsNotConsideredInternal() + } + + @Test + fun `does not detect full url as internal (span kind producer)`() { + givenDsn("https://publicKey:secretKey@io.sentry:8081/path/id?sample.rate=0.1") + givenSpanKind(SpanKind.PRODUCER) + givenAttributes( + mapOf( + UrlAttributes.URL_FULL to "https://io.sentry:8081" + ) + ) + + thenRequestIsNotConsideredInternal() + } + + @Test + fun `does not detect full url as internal (span kind consumer)`() { + givenDsn("https://publicKey:secretKey@io.sentry:8081/path/id?sample.rate=0.1") + givenSpanKind(SpanKind.CONSUMER) + givenAttributes( + mapOf( + UrlAttributes.URL_FULL to "https://io.sentry:8081" + ) + ) + + thenRequestIsNotConsideredInternal() + } + + @Test + fun `detects full spotlight url as internal`() { + givenDsn("https://publicKey:secretKey@io.sentry:8081/path/id?sample.rate=0.1") + givenSpotlightEnabled(true) + givenSpanKind(SpanKind.CLIENT) + givenAttributes( + mapOf( + UrlAttributes.URL_FULL to "http://localhost:8969/stream" + ) + ) + + thenRequestIsConsideredInternal() + } + + @Test + fun `detects full spotlight url as internal with custom spotlight url`() { + givenDsn("https://publicKey:secretKey@io.sentry:8081/path/id?sample.rate=0.1") + givenSpotlightEnabled(true) + givenSpotlightUrl("http://localhost:8090/stream") + givenSpanKind(SpanKind.CLIENT) + givenAttributes( + mapOf( + UrlAttributes.URL_FULL to "http://localhost:8090/stream" + ) + ) + + thenRequestIsConsideredInternal() + } + + @Test + fun `does not detect mismatching full spotlight url as internal`() { + givenDsn("https://publicKey:secretKey@io.sentry:8081/path/id?sample.rate=0.1") + givenSpotlightEnabled(true) + givenSpanKind(SpanKind.CLIENT) + givenAttributes( + mapOf( + UrlAttributes.URL_FULL to "http://localhost:8080/stream" + ) + ) + + thenRequestIsNotConsideredInternal() + } + + @Test + fun `does not detect mismatching full customized spotlight url as internal`() { + givenDsn("https://publicKey:secretKey@io.sentry:8081/path/id?sample.rate=0.1") + givenSpotlightEnabled(true) + givenSpotlightUrl("http://localhost:8090/stream") + givenSpanKind(SpanKind.CLIENT) + givenAttributes( + mapOf( + UrlAttributes.URL_FULL to "http://localhost:8091/stream" + ) + ) + + thenRequestIsNotConsideredInternal() + } + + private fun givenAttributes(map: Map, Any>) { + map.forEach { k, v -> + fixture.attributes.put(k, v) + } + } + + private fun givenDsn(dsn: String) { + fixture.options.dsn = dsn + } + + private fun givenSpotlightEnabled(enabled: Boolean) { + fixture.options.isEnableSpotlight = enabled + } + + private fun givenSpotlightUrl(url: String) { + fixture.options.spotlightConnectionUrl = url + } + + private fun givenSpanKind(spanKind: SpanKind) { + fixture.spanKind = spanKind + } + + private fun thenRequestIsConsideredInternal() { + assertTrue(checkIfInternal()) + } + + private fun thenRequestIsNotConsideredInternal() { + assertFalse(checkIfInternal()) + } + + private fun checkIfInternal(): Boolean { + return OtelInternalSpanDetectionUtil.isSentryRequest(fixture.scopes, fixture.spanKind, fixture.attributes) + } +} diff --git a/sentry/src/main/java/io/sentry/DsnUtil.java b/sentry/src/main/java/io/sentry/DsnUtil.java index b6902ad2741..f31d1c286af 100644 --- a/sentry/src/main/java/io/sentry/DsnUtil.java +++ b/sentry/src/main/java/io/sentry/DsnUtil.java @@ -31,6 +31,13 @@ public static boolean urlContainsDsnHost(@Nullable SentryOptions options, @Nulla return false; } - return url.toLowerCase(Locale.ROOT).contains(dsnHost.toLowerCase(Locale.ROOT)); + final @NotNull String lowerCaseHost = dsnHost.toLowerCase(Locale.ROOT); + final int dsnPort = sentryUri.getPort(); + + if (dsnPort > 0) { + return url.toLowerCase(Locale.ROOT).contains(lowerCaseHost + ":" + dsnPort); + } else { + return url.toLowerCase(Locale.ROOT).contains(lowerCaseHost); + } } } diff --git a/sentry/src/test/java/io/sentry/DsnUtilTest.kt b/sentry/src/test/java/io/sentry/DsnUtilTest.kt index aa0f1c8e4b5..f231db98bb9 100644 --- a/sentry/src/test/java/io/sentry/DsnUtilTest.kt +++ b/sentry/src/test/java/io/sentry/DsnUtilTest.kt @@ -40,6 +40,16 @@ class DsnUtilTest { assertFalse(DsnUtil.urlContainsDsnHost(optionsWithDsn(DSN), null)) } + @Test + fun `returns false for same host but different port`() { + assertFalse(DsnUtil.urlContainsDsnHost(optionsWithDsn("http://publicKey:secretKey@localhost:8080/path/id?sample.rate=0.1"), "localhost:8081")) + } + + @Test + fun `returns true for same host and port`() { + assertTrue(DsnUtil.urlContainsDsnHost(optionsWithDsn("http://publicKey:secretKey@localhost:8080/path/id?sample.rate=0.1"), "localhost:8080")) + } + private fun optionsWithDsn(dsn: String?): SentryOptions { return SentryOptions().also { it.dsn = dsn From 47602d2d0c4c2527cbb7b5ccf4f3c91dca1ef7c4 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 5 Mar 2025 12:39:17 +0100 Subject: [PATCH 2/4] changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b7c2efd7a0..ee613b98583 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +### Internal + +- Also use port when checking if a request is made to Sentry DSN ([#4231](https://github.com/getsentry/sentry-java/pull/4231)) + - For our OpenTelemetry integration we check if a span is for a request to Sentry + - We now also consider the port when performing this check + ## 8.3.0 ### Features From 282d7cdaf13414a06339d3310645e378a84983f5 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 5 Mar 2025 12:40:24 +0100 Subject: [PATCH 3/4] Add a param to control whether the test script should rebuild before running the tested server --- .github/workflows/system-tests-backend.yml | 2 +- test/system-test-run-all.sh | 18 +++++++++--------- test/system-test-run.sh | 6 ++++++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index 0656a5331ef..93d007b15b9 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -101,7 +101,7 @@ jobs: - name: Start server and run integration test for sentry-cli commands run: | - test/system-test-run.sh "${{ matrix.sample }}" "${{ matrix.agent }}" "${{ matrix.agent-auto-init }}" + test/system-test-run.sh "${{ matrix.sample }}" "${{ matrix.agent }}" "${{ matrix.agent-auto-init }}" "0" - name: Upload test results if: always() diff --git a/test/system-test-run-all.sh b/test/system-test-run-all.sh index e65a500b4d6..cc3fb523670 100755 --- a/test/system-test-run-all.sh +++ b/test/system-test-run-all.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash -./test/system-test-run.sh "sentry-samples-spring-boot" "0" "true" -./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry-noagent" "0" "true" -./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry" "1" "true" -./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry" "1" "false" -./test/system-test-run.sh "sentry-samples-spring-boot-webflux-jakarta" "0" "true" -./test/system-test-run.sh "sentry-samples-spring-boot-webflux" "0" "true" -./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" "0" "true" -./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "true" -./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "false" +./test/system-test-run.sh "sentry-samples-spring-boot" "0" "true" "0" +./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry-noagent" "0" "true" "0" +./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry" "1" "true" "0" +./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry" "1" "false" "0" +./test/system-test-run.sh "sentry-samples-spring-boot-webflux-jakarta" "0" "true" "0" +./test/system-test-run.sh "sentry-samples-spring-boot-webflux" "0" "true" "0" +./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" "0" "true" "0" +./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "true" "0" +./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "false" "0" diff --git a/test/system-test-run.sh b/test/system-test-run.sh index 7f1b47bed4f..9560beb3639 100755 --- a/test/system-test-run.sh +++ b/test/system-test-run.sh @@ -3,6 +3,12 @@ readonly SAMPLE_MODULE=$1 readonly JAVA_AGENT=$2 readonly JAVA_AGENT_AUTO_INIT=$3 +readonly BUILD_BEFORE_RUN=$4 + +if [[ "$BUILD_BEFORE_RUN" == "1" ]]; then + echo "Building before Test run" + ./gradlew :sentry-samples:${SAMPLE_MODULE}:assemble +fi test/system-test-sentry-server-start.sh MOCK_SERVER_PID=$(cat sentry-mock-server.pid) From 0b67b609bdaa4cbaf0e263605fd13d7f64cd3534 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 5 Mar 2025 12:42:32 +0100 Subject: [PATCH 4/4] Add system tests for distributed tracing --- .../jakarta/DistributedTracingController.java | 51 +++++ .../DistributedTracingSystemTest.kt | 182 ++++++++++++++++++ .../systemtest/GraphqlGreetingSystemTest.kt | 4 +- .../systemtest/GraphqlProjectSystemTest.kt | 6 +- .../systemtest/GraphqlTaskSystemTest.kt | 2 +- .../io/sentry/systemtest/PersonSystemTest.kt | 4 +- .../io/sentry/systemtest/TodoSystemTest.kt | 6 +- .../sentry/systemtest/util/RestTestClient.kt | 36 +++- .../io/sentry/systemtest/util/TestHelper.kt | 75 ++++++-- 9 files changed, 336 insertions(+), 30 deletions(-) create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java new file mode 100644 index 00000000000..cfff0be4702 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java @@ -0,0 +1,51 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.opentelemetry.instrumentation.annotations.WithSpan; +import java.nio.charset.Charset; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestClient; + +@RestController +@RequestMapping("/tracing/") +public class DistributedTracingController { + private static final Logger LOGGER = LoggerFactory.getLogger(DistributedTracingController.class); + private final RestClient restClient; + + public DistributedTracingController(RestClient restClient) { + this.restClient = restClient; + } + + @GetMapping("{id}") + @WithSpan("tracingSpanThroughOtelAnnotation") + Person person(@PathVariable Long id) { + return restClient + .get() + .uri("http://localhost:8080/person/{id}", id) + .header( + HttpHeaders.AUTHORIZATION, + "Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset())) + .retrieve() + .body(Person.class); + } + + @PostMapping + Person create(@RequestBody Person person) { + return restClient + .post() + .uri("http://localhost:8080/person/") + .body(person) + .header( + HttpHeaders.AUTHORIZATION, + "Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset())) + .retrieve() + .body(Person.class); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt new file mode 100644 index 00000000000..3b4accef82a --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt @@ -0,0 +1,182 @@ +package io.sentry.systemtest + +import io.sentry.protocol.SentryId +import io.sentry.samples.spring.boot.jakarta.Person +import io.sentry.systemtest.util.TestHelper +import org.junit.Before +import org.springframework.http.HttpStatus +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class DistributedTracingSystemTest { + + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `get person distributed tracing`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + "$traceId-424cffc8f94feeee-1", + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET" + ) + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /tracing/{id}" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /person/{id}" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + } + + @Test + fun `get person distributed tracing with sampled false`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + "$traceId-424cffc8f94feeee-0", + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=false,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET" + ) + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, restClient.lastKnownStatusCode) + + testHelper.ensureNoTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /tracing/{id}" + } + + testHelper.ensureNoTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /person/{id}" + } + } + + @Test + fun `get person distributed tracing without sample_rand`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + "$traceId-424cffc8f94feeee-1", + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET" + ) + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, restClient.lastKnownStatusCode) + + var sampleRand1: String? = null + var sampleRand2: String? = null + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + + val matches = transaction.transaction == "GET /tracing/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRand1 = envelopeHeader.traceContext?.sampleRand + } + + matches + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = transaction.transaction == "GET /person/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRand2 = envelopeHeader.traceContext?.sampleRand + } + + matches + } + + assertEquals(sampleRand1, sampleRand2) + } + + @Test + fun `get person distributed tracing updates sample_rate on deferred decision`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + "$traceId-424cffc8f94feeee", + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=0.5,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET" + ) + assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, restClient.lastKnownStatusCode) + + var sampleRate1: String? = null + var sampleRate2: String? = null + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + + val matches = transaction.transaction == "GET /tracing/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRate1 = envelopeHeader.traceContext?.sampleRate + } + + matches + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = transaction.transaction == "GET /person/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRate2 = envelopeHeader.traceContext?.sampleRate + } + + matches + } + + assertEquals(sampleRate1, sampleRate2) + assertNotEquals(sampleRate1, "0.5") + } + + @Test + fun `create person distributed tracing`() { + val traceId = SentryId() + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = restClient.createPersonDistributedTracing( + person, + "$traceId-424cffc8f94feeee-1", + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET" + ) + assertEquals(HttpStatus.OK, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "POST /tracing/" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "POST /person/" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt index 769ae399bf0..5681c421a28 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt @@ -19,7 +19,7 @@ class GraphqlGreetingSystemTest { val response = testHelper.graphqlClient.greet("world") testHelper.ensureNoErrors(response) - testHelper.ensureTransactionReceived { transaction -> + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithDescription(transaction, "Query.greeting") } } @@ -32,7 +32,7 @@ class GraphqlGreetingSystemTest { testHelper.ensureErrorReceived { error -> error.message?.message?.startsWith("Unresolved RuntimeException for executionId ") ?: false } - testHelper.ensureTransactionReceived { transaction -> + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithDescription(transaction, "Query.greeting") } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt index 74b196e33b6..bfa38fead33 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt @@ -23,7 +23,7 @@ class GraphqlProjectSystemTest { testHelper.ensureNoErrors(response) assertEquals("proj-slug", response?.data?.project?.slug) - testHelper.ensureTransactionReceived { transaction -> + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithDescription(transaction, "Query.project") } } @@ -34,7 +34,7 @@ class GraphqlProjectSystemTest { testHelper.ensureNoErrors(response) assertNotNull(response?.data?.addProject) - testHelper.ensureTransactionReceived { transaction -> + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithDescription(transaction, "Mutation.addProject") } } @@ -48,7 +48,7 @@ class GraphqlProjectSystemTest { testHelper.ensureErrorReceived { error -> error.message?.message?.startsWith("Unresolved RuntimeException for executionId ") ?: false } - testHelper.ensureTransactionReceived { transaction -> + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithDescription(transaction, "Mutation.addProject") } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt index 7a9283ac05a..7b8a1471542 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt @@ -30,7 +30,7 @@ class GraphqlTaskSystemTest { assertEquals("C3", firstTask.creatorId) assertEquals("C3", firstTask.creator?.id) - testHelper.ensureTransactionReceived { transaction -> + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithDescription(transaction, "Query.tasks") } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 8d83bde6309..3eed9c69cad 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -23,7 +23,7 @@ class PersonSystemTest { restClient.getPerson(1L) assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, restClient.lastKnownStatusCode) - testHelper.ensureTransactionReceived { transaction -> + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") } @@ -39,7 +39,7 @@ class PersonSystemTest { assertEquals(person.firstName, returnedPerson!!.firstName) assertEquals(person.lastName, returnedPerson!!.lastName) - testHelper.ensureTransactionReceived { transaction -> + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt index 4c8ee45ea64..a32735e7e6b 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt @@ -22,7 +22,7 @@ class TodoSystemTest { restClient.getTodo(1L) assertEquals(HttpStatus.OK, restClient.lastKnownStatusCode) - testHelper.ensureTransactionReceived { transaction -> + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithOp(transaction, "todoSpanOtelApi") && testHelper.doesTransactionContainSpanWithOp(transaction, "todoSpanSentryApi") && testHelper.doesTransactionContainSpanWithOp(transaction, "http.client") @@ -35,7 +35,7 @@ class TodoSystemTest { restClient.getTodoWebclient(1L) assertEquals(HttpStatus.OK, restClient.lastKnownStatusCode) - testHelper.ensureTransactionReceived { transaction -> + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithOp(transaction, "http.client") } } @@ -46,7 +46,7 @@ class TodoSystemTest { restClient.getTodoRestClient(1L) assertEquals(HttpStatus.OK, restClient.lastKnownStatusCode) - testHelper.ensureTransactionReceived { transaction -> + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> testHelper.doesTransactionContainSpanWithOp(transaction, "todoRestClientSpanOtelApi") && testHelper.doesTransactionContainSpanWithOp(transaction, "todoRestClientSpanSentryApi") && testHelper.doesTransactionContainSpanWithOp(transaction, "http.client") diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/util/RestTestClient.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/util/RestTestClient.kt index 1b1d16c841f..f50632f381c 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/util/RestTestClient.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/util/RestTestClient.kt @@ -33,6 +33,36 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl } } + fun getPersonDistributedTracing(id: Long, sentryTraceHeader: String? = null, baggageHeader: String? = null): Person? { + return try { + val response = restTemplate().exchange("$backendBaseUrl/tracing/{id}", HttpMethod.GET, entityWithAuth(headerCallback = tracingHeaders(sentryTraceHeader, baggageHeader)), Person::class.java, mapOf("id" to id)) + lastKnownStatusCode = response.statusCode + response.body + } catch (e: HttpStatusCodeException) { + lastKnownStatusCode = e.statusCode + null + } + } + + fun createPersonDistributedTracing(person: Person, sentryTraceHeader: String? = null, baggageHeader: String? = null): Person? { + return try { + val response = restTemplate().exchange("$backendBaseUrl/tracing/", HttpMethod.POST, entityWithAuth(person, tracingHeaders(sentryTraceHeader, baggageHeader)), Person::class.java, person) + lastKnownStatusCode = response.statusCode + response.body + } catch (e: HttpStatusCodeException) { + lastKnownStatusCode = e.statusCode + null + } + } + + private fun tracingHeaders(sentryTraceHeader: String?, baggageHeader: String?): (HttpHeaders) -> HttpHeaders { + return { httpHeaders -> + sentryTraceHeader?.let { httpHeaders.set("sentry-trace", it) } + baggageHeader?.let { httpHeaders.set("baggage", it) } + httpHeaders + } + } + fun getTodo(id: Long): Todo? { return try { val response = restTemplate().exchange("$backendBaseUrl/todo/{id}", HttpMethod.GET, entityWithAuth(), Todo::class.java, mapOf("id" to id)) @@ -66,11 +96,13 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl } } - private fun entityWithAuth(request: Any? = null): HttpEntity { + private fun entityWithAuth(request: Any? = null, headerCallback: ((HttpHeaders) -> HttpHeaders)? = null): HttpEntity { val headers = HttpHeaders().also { it.setBasicAuth("user", "password") } - return HttpEntity(request, headers) + val modifiedHeaders = headerCallback?.invoke(headers) ?: headers + + return HttpEntity(request, modifiedHeaders) } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/util/TestHelper.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/util/TestHelper.kt index 12960f4c528..14bac5cd0ea 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/util/TestHelper.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/util/TestHelper.kt @@ -3,6 +3,7 @@ package io.sentry.systemtest.util import com.apollographql.apollo3.api.ApolloResponse import com.apollographql.apollo3.api.Operation import io.sentry.JsonSerializer +import io.sentry.SentryEnvelopeHeader import io.sentry.SentryEvent import io.sentry.SentryItemType import io.sentry.SentryOptions @@ -54,27 +55,55 @@ class TestHelper(backendUrl: String) { throw RuntimeException("Unable to find matching envelope received by relay") } - fun ensureTransactionReceived(callback: ((SentryTransaction) -> Boolean)) { - ensureEnvelopeReceived { envelopeString -> - val deserializeEnvelope = - jsonSerializer.deserializeEnvelope(envelopeString.byteInputStream()) - if (deserializeEnvelope == null) { - return@ensureEnvelopeReceived false - } + fun ensureNoEnvelopeReceived(callback: ((String) -> Boolean)) { + Thread.sleep(10000) + val envelopes = sentryClient.getEnvelopes() - val transactionItem = - deserializeEnvelope.items.firstOrNull { it.header.type == SentryItemType.Transaction } - if (transactionItem == null) { - return@ensureEnvelopeReceived false - } + if (envelopes.envelopes.isNullOrEmpty()) { + return + } - val transaction = transactionItem.getTransaction(jsonSerializer) - if (transaction == null) { - return@ensureEnvelopeReceived false + envelopes.envelopes.forEach { envelopeString -> + val didMatch = callback(envelopeString) + if (didMatch) { + throw RuntimeException("Found unexpected matching envelope received by relay") } + } + } + + fun ensureTransactionReceived(callback: ((SentryTransaction, SentryEnvelopeHeader) -> Boolean)) { + ensureEnvelopeReceived { envelopeString -> + checkIfTransactionMatches(envelopeString, callback) + } + } + + fun ensureNoTransactionReceived(callback: ((SentryTransaction, SentryEnvelopeHeader) -> Boolean)) { + ensureNoEnvelopeReceived { envelopeString -> + checkIfTransactionMatches(envelopeString, callback) + } + } + + private fun checkIfTransactionMatches(envelopeString: String, callback: ((SentryTransaction, SentryEnvelopeHeader) -> Boolean)): Boolean { + val deserializeEnvelope = + jsonSerializer.deserializeEnvelope(envelopeString.byteInputStream()) + if (deserializeEnvelope == null) { + return false + } + + val envelopeHeader = deserializeEnvelope.header + + val transactionItem = + deserializeEnvelope.items.firstOrNull { it.header.type == SentryItemType.Transaction } + if (transactionItem == null) { + return false + } - callback(transaction) + val transaction = transactionItem.getTransaction(jsonSerializer) + if (transaction == null) { + return false } + + return callback(transaction, envelopeHeader) } fun ensureErrorReceived(callback: ((SentryEvent) -> Boolean)) { @@ -106,7 +135,7 @@ class TestHelper(backendUrl: String) { } fun ensureTransactionWithSpanReceived(callback: ((SentrySpan) -> Boolean)) { - ensureTransactionReceived { transaction -> + ensureTransactionReceived { transaction, envelopeHeader -> transaction.spans.forEach { span -> val callbackResult = callback(span) if (callbackResult) { @@ -126,6 +155,7 @@ class TestHelper(backendUrl: String) { PrintWriter(System.out).use { jsonSerializer.serialize(obj, it) } + println() } fun ensureNoErrors(response: ApolloResponse?) { @@ -159,4 +189,15 @@ class TestHelper(backendUrl: String) { return true } + + fun doesTransactionHaveTraceId(transaction: SentryTransaction, traceId: String): Boolean { + val spanContext = transaction.contexts.trace + if (spanContext?.traceId?.toString() != traceId) { + println("Unable to find trace ID $traceId in transaction:") + logObject(transaction) + return false + } + + return true + } }