From c70ca3194757efb9425ea463a07c0c00686d9592 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 20 Oct 2026 18:45:39 +0200 Subject: [PATCH 1/2] test(spring-boot): Port the rest of the Boot 3 OTel agent sample's tests The agent fixture gains the Cache, Metrics, GraphQL and Kafka endpoints, and the agent suite ports the sample's remaining tests in both init modes. The agent traces each GraphQL operation as one span named after it, so the queries are named. Kafka uses an in-JVM KRaft broker that the test JVM starts next to the app. The sample has no system tests left, so it comes off the system-test runner, the backend workflow and the Spring Boot 3 matrix. The matrix no longer builds the agent separately, because the suites build the jar they attach. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/spring-boot-3-matrix.yml | 19 +-- .github/workflows/system-tests-backend.yml | 6 - .../build.gradle.kts | 34 ----- .../src/test/kotlin/io/sentry/DummyTest.kt | 12 -- .../io/sentry/systemtest/CacheSystemTest.kt | 51 -------- .../systemtest/GraphqlGreetingSystemTest.kt | 46 ------- .../systemtest/GraphqlProjectSystemTest.kt | 66 ---------- .../systemtest/GraphqlTaskSystemTest.kt | 40 ------ .../KafkaOtelCoexistenceSystemTest.kt | 37 ------ .../io/sentry/systemtest/MetricsSystemTest.kt | 51 -------- .../src/test/resources/logback.xml | 17 --- sentry-spring-boot-jakarta/build.gradle.kts | 8 ++ .../agentfixture/AgentFixtureApplication.kt | 2 + .../jakarta/agentfixture/CacheController.kt | 43 +++++++ .../agentfixture/GraphqlControllers.kt | 83 +++++++++++++ .../jakarta/agentfixture/KafkaController.kt | 28 +++++ .../jakarta/agentfixture/MetricController.kt | 33 +++++ .../resources/application.properties | 1 + .../resources/graphql/schema.graphqls | 38 ++++++ .../otelagentintegrationtest/AgentApp.kt | 9 +- .../CacheIntegrationTest.kt | 35 ++++++ .../GraphqlIntegrationTest.kt | 116 ++++++++++++++++++ .../KafkaOtelCoexistenceIntegrationTest.kt | 44 +++++++ .../MetricsIntegrationTest.kt | 46 +++++++ test/system-test-runner.py | 4 - 25 files changed, 486 insertions(+), 383 deletions(-) delete mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/DummyTest.kt delete mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt delete mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt delete mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt delete mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt delete mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/KafkaOtelCoexistenceSystemTest.kt delete mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt delete mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/resources/logback.xml create mode 100644 sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/CacheController.kt create mode 100644 sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/GraphqlControllers.kt create mode 100644 sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/KafkaController.kt create mode 100644 sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/MetricController.kt create mode 100644 sentry-spring-boot-jakarta/src/agentFixture/resources/graphql/schema.graphqls create mode 100644 sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/CacheIntegrationTest.kt create mode 100644 sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/GraphqlIntegrationTest.kt create mode 100644 sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/KafkaOtelCoexistenceIntegrationTest.kt create mode 100644 sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/MetricsIntegrationTest.kt diff --git a/.github/workflows/spring-boot-3-matrix.yml b/.github/workflows/spring-boot-3-matrix.yml index 3fae47058db..bf7d92b827a 100644 --- a/.github/workflows/spring-boot-3-matrix.yml +++ b/.github/workflows/spring-boot-3-matrix.yml @@ -71,11 +71,8 @@ jobs: - name: Build sample artifacts run: | ./gradlew \ - :sentry-samples:sentry-samples-spring-boot-jakarta-opentelemetry:bootJar \ - :sentry-samples:sentry-samples-spring-boot-jakarta-opentelemetry:testClasses \ :sentry-samples:sentry-samples-spring-jakarta:war \ - :sentry-samples:sentry-samples-spring-jakarta:testClasses \ - :sentry-opentelemetry:sentry-opentelemetry-agent:assemble + :sentry-samples:sentry-samples-spring-jakarta:testClasses - name: Test sentry-spring-boot-jakarta integration suites run: | @@ -87,20 +84,6 @@ jobs: :sentry-spring-boot-jakarta:integrationTestOtelAgent \ :sentry-spring-boot-jakarta:integrationTestOtelAgentWithoutAutoInit - - name: Test sentry-samples-spring-boot-jakarta-opentelemetry agent init true - run: | - python3 test/system-test-runner.py test \ - --module "sentry-samples-spring-boot-jakarta-opentelemetry" \ - --agent true \ - --auto-init "true" - - - name: Test sentry-samples-spring-boot-jakarta-opentelemetry agent init false - run: | - python3 test/system-test-runner.py test \ - --module "sentry-samples-spring-boot-jakarta-opentelemetry" \ - --agent true \ - --auto-init "false" - - name: Test sentry-samples-spring-jakarta run: | python3 test/system-test-runner.py test \ diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index 11e09436ce9..52f8238075f 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -34,12 +34,6 @@ jobs: - sample: "sentry-samples-spring-boot-opentelemetry" agent: "true" agent-auto-init: "false" - - sample: "sentry-samples-spring-boot-jakarta-opentelemetry" - agent: "true" - agent-auto-init: "true" - - sample: "sentry-samples-spring-boot-jakarta-opentelemetry" - agent: "true" - agent-auto-init: "false" - sample: "sentry-samples-console-otlp" agent: "false" agent-auto-init: "true" diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts index 9b59911bd34..e6d7d9c31ba 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts @@ -7,7 +7,6 @@ plugins { alias(libs.plugins.spring.dependency.management) alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) - id("io.sentry.systemtest") } group = "io.sentry.sample.spring-boot-jakarta" @@ -61,17 +60,6 @@ dependencies { // database query tracing implementation(projects.sentryJdbc) runtimeOnly(libs.hsqldb) - - testImplementation(kotlin(Config.kotlinStdLib)) - testImplementation(projects.sentrySystemTestSupport) - testImplementation(libs.apollo3.kotlin) - testImplementation(libs.kotlin.test.junit) - testImplementation(libs.slf4j2.api) - testImplementation(libs.springboot3.starter.test) { - exclude(group = "org.junit.vintage", module = "junit-vintage-engine") - } - testImplementation("ch.qos.logback:logback-classic:1.5.16") - testImplementation("ch.qos.logback:logback-core:1.5.16") } tasks.register("bootRunWithAgent").configure { @@ -105,25 +93,3 @@ tasks.register("bootRunWithAgent").configure { jvmArgs = listOf("-Dotel.javaagent.debug=true", "-javaagent:$agentJarPath") } - -// The runner launches this sample with -javaagent, so track the agent jar as a systemTest input. -sentrySystemTest { usesOpenTelemetryAgent = true } - -tasks.register("systemTest").configure { - group = "verification" - description = "Runs the System tests" - - val test = project.extensions.getByType()["test"] - testClassesDirs = test.output.classesDirs - classpath = test.runtimeClasspath - - maxParallelForks = 1 - - filter { includeTestsMatching("io.sentry.systemtest*") } -} - -tasks.named("test").configure { - require(this is Test) - - filter { excludeTestsMatching("io.sentry.systemtest.*") } -} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/DummyTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/DummyTest.kt deleted file mode 100644 index 6f762b7e453..00000000000 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/DummyTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.sentry - -import kotlin.test.Test -import kotlin.test.assertTrue - -class DummyTest { - @Test - fun `the only test`() { - // only needed to have more than 0 tests and not fail the build - assertTrue(true) - } -} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt deleted file mode 100644 index b45e9c10853..00000000000 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/CacheSystemTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package io.sentry.systemtest - -import io.sentry.systemtest.util.TestHelper -import kotlin.test.Test -import kotlin.test.assertEquals -import org.junit.Before - -class CacheSystemTest { - lateinit var testHelper: TestHelper - - @Before - fun setup() { - testHelper = TestHelper("http://localhost:8080") - testHelper.reset() - } - - @Test - fun `cache put and get produce spans`() { - val restClient = testHelper.restClient - - // Save a todo (triggers @CachePut -> cache.put span) - val todo = Todo(1L, "test-todo", false) - restClient.saveCachedTodo(todo) - assertEquals(200, restClient.lastKnownStatusCode) - - testHelper.ensureTransactionReceived { transaction, _ -> - testHelper.doesTransactionContainSpanWithOp(transaction, "cache.put") - } - - testHelper.reset() - - // Get the todo (triggers @Cacheable -> cache.get span, should be a hit) - restClient.getCachedTodo(1L) - assertEquals(200, restClient.lastKnownStatusCode) - - testHelper.ensureTransactionReceived { transaction, _ -> - testHelper.doesTransactionContainSpanWithOp(transaction, "cache.get") - } - } - - @Test - fun `cache evict produces span`() { - val restClient = testHelper.restClient - - restClient.deleteCachedTodo(1L) - - testHelper.ensureTransactionReceived { transaction, _ -> - testHelper.doesTransactionContainSpanWithOp(transaction, "cache.evict") - } - } -} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt deleted file mode 100644 index 4286cfa1a86..00000000000 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt +++ /dev/null @@ -1,46 +0,0 @@ -package io.sentry.systemtest - -import io.sentry.systemtest.util.TestHelper -import kotlin.test.Test -import org.junit.Before - -class GraphqlGreetingSystemTest { - lateinit var testHelper: TestHelper - - @Before - fun setup() { - testHelper = TestHelper("http://localhost:8080") - testHelper.reset() - } - - @Test - fun `greeting works`() { - val response = testHelper.graphqlClient.greet("world") - - testHelper.ensureNoErrors(response) - testHelper.ensureTransactionReceived { transaction, envelopeHeader -> - testHelper.doesTransactionContainSpanWithOpAndDescription( - transaction, - "query GreetingQuery", - "query GreetingQuery", - ) - } - } - - @Test - fun `greeting error`() { - val response = testHelper.graphqlClient.greet("crash") - - testHelper.ensureErrorCount(response, 1) - testHelper.ensureErrorReceived { error -> - error.message?.message?.startsWith("Unresolved RuntimeException for executionId ") ?: false - } - testHelper.ensureTransactionReceived { transaction, envelopeHeader -> - testHelper.doesTransactionContainSpanWithOpAndDescription( - transaction, - "query GreetingQuery", - "query GreetingQuery", - ) - } - } -} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt deleted file mode 100644 index f592b2a08ec..00000000000 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt +++ /dev/null @@ -1,66 +0,0 @@ -package io.sentry.systemtest - -import io.sentry.systemtest.util.TestHelper -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import org.junit.Before - -class GraphqlProjectSystemTest { - lateinit var testHelper: TestHelper - - @Before - fun setup() { - testHelper = TestHelper("http://localhost:8080") - testHelper.reset() - } - - @Test - fun `project query works`() { - val response = testHelper.graphqlClient.project("proj-slug") - - testHelper.ensureNoErrors(response) - assertEquals("proj-slug", response?.data?.project?.slug) - testHelper.ensureTransactionReceived { transaction, envelopeHeader -> - testHelper.doesTransactionContainSpanWithOpAndDescription( - transaction, - "query ProjectQuery", - "query ProjectQuery", - ) - } - } - - @Test - fun `project mutation works`() { - val response = testHelper.graphqlClient.addProject("proj-slug") - - testHelper.ensureNoErrors(response) - assertNotNull(response?.data?.addProject) - testHelper.ensureTransactionReceived { transaction, envelopeHeader -> - testHelper.doesTransactionContainSpanWithOpAndDescription( - transaction, - "mutation AddProjectMutation", - "mutation AddProjectMutation", - ) - } - } - - @Test - fun `project mutation error`() { - val response = testHelper.graphqlClient.addProject("addprojectcrash") - - testHelper.ensureErrorCount(response, 1) - assertNull(response?.data?.addProject) - testHelper.ensureErrorReceived { error -> - error.message?.message?.startsWith("Unresolved RuntimeException for executionId ") ?: false - } - testHelper.ensureTransactionReceived { transaction, envelopeHeader -> - testHelper.doesTransactionContainSpanWithOpAndDescription( - transaction, - "mutation AddProjectMutation", - "mutation AddProjectMutation", - ) - } - } -} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt deleted file mode 100644 index f811da5a082..00000000000 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.sentry.systemtest - -import io.sentry.systemtest.util.TestHelper -import kotlin.test.Test -import kotlin.test.assertEquals -import org.junit.Before - -class GraphqlTaskSystemTest { - lateinit var testHelper: TestHelper - - @Before - fun setup() { - testHelper = TestHelper("http://localhost:8080") - testHelper.reset() - } - - @Test - fun `tasks and assignees query works`() { - val response = testHelper.graphqlClient.tasksAndAssignees("project-slug") - - testHelper.ensureNoErrors(response) - - assertEquals(5, response?.data?.tasks?.size) - - val firstTask = response?.data?.tasks?.firstOrNull() ?: throw RuntimeException("no task") - assertEquals("T1", firstTask.id) - assertEquals("A3", firstTask.assigneeId) - assertEquals("A3", firstTask.assignee?.id) - assertEquals("C3", firstTask.creatorId) - assertEquals("C3", firstTask.creator?.id) - - testHelper.ensureTransactionReceived { transaction, envelopeHeader -> - testHelper.doesTransactionContainSpanWithOpAndDescription( - transaction, - "query TasksAndAssigneesQuery", - "query TasksAndAssigneesQuery", - ) - } - } -} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/KafkaOtelCoexistenceSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/KafkaOtelCoexistenceSystemTest.kt deleted file mode 100644 index c401c91463e..00000000000 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/KafkaOtelCoexistenceSystemTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -package io.sentry.systemtest - -import io.sentry.systemtest.util.TestHelper -import kotlin.test.Test -import kotlin.test.assertEquals -import org.junit.Before - -class KafkaOtelCoexistenceSystemTest { - lateinit var testHelper: TestHelper - - @Before - fun setup() { - testHelper = TestHelper("http://localhost:8080") - testHelper.reset() - } - - @Test - fun `Sentry Kafka integration is suppressed when OTel is active`() { - val restClient = testHelper.restClient - - restClient.produceKafkaMessage("otel-coexistence-test") - assertEquals(200, restClient.lastKnownStatusCode) - - testHelper.ensureTransactionReceived { transaction, _ -> - transaction.transaction == "GET /kafka/produce" && - transaction.contexts.trace?.origin == "auto.opentelemetry" && - transaction.sdk?.integrationSet?.contains("SpringKafka") != true - } - - testHelper.ensureTransactionReceived { transaction, _ -> - transaction.contexts.trace?.operation == "queue.process" && - transaction.contexts.trace?.origin == "auto.opentelemetry" && - transaction.contexts.trace?.data?.get("messaging.system") == "kafka" && - transaction.sdk?.integrationSet?.contains("SpringKafka") != true - } - } -} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt deleted file mode 100644 index 039d9d640c7..00000000000 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ /dev/null @@ -1,51 +0,0 @@ -package io.sentry.systemtest - -import io.sentry.systemtest.util.TestHelper -import kotlin.test.Test -import kotlin.test.assertEquals -import org.junit.Before - -class MetricsSystemTest { - lateinit var testHelper: TestHelper - - @Before - fun setup() { - testHelper = TestHelper("http://localhost:8080") - testHelper.reset() - } - - @Test - fun `count metric`() { - val restClient = testHelper.restClient - assertEquals("count metric increased", restClient.getCountMetric()) - assertEquals(200, restClient.lastKnownStatusCode) - - testHelper.ensureMetricsReceived { event, header -> - testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) && - testHelper.doesMetricHaveAttribute(event, "countMetric", "user.type", "admin") && - testHelper.doesMetricHaveAttribute(event, "countMetric", "feature.version", 2) - } - } - - @Test - fun `gauge metric`() { - val restClient = testHelper.restClient - assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) - assertEquals(200, restClient.lastKnownStatusCode) - - testHelper.ensureMetricsReceived { event, header -> - testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) - } - } - - @Test - fun `distribution metric`() { - val restClient = testHelper.restClient - assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) - assertEquals(200, restClient.lastKnownStatusCode) - - testHelper.ensureMetricsReceived { event, header -> - testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) - } - } -} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/resources/logback.xml b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/resources/logback.xml deleted file mode 100644 index a36b8f80f76..00000000000 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/resources/logback.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - %-4relative [%thread] %-5level %logger{35} -%kvp- %msg %n - - - - - - - diff --git a/sentry-spring-boot-jakarta/build.gradle.kts b/sentry-spring-boot-jakarta/build.gradle.kts index 04d250c4a6b..9451cf95117 100644 --- a/sentry-spring-boot-jakarta/build.gradle.kts +++ b/sentry-spring-boot-jakarta/build.gradle.kts @@ -161,15 +161,21 @@ dependencies { "agentFixtureImplementation"(platform(SpringBootPlugin.BOM_COORDINATES)) "agentFixtureImplementation"(projects.sentrySpringBootStarterJakarta) "agentFixtureImplementation"(projects.sentryAsyncProfiler) + "agentFixtureImplementation"(projects.sentryGraphql22) "agentFixtureImplementation"(projects.sentryJdbc) + "agentFixtureImplementation"(projects.sentryKafka) "agentFixtureImplementation"(projects.sentryLogback) "agentFixtureImplementation"(kotlin(Config.kotlinStdLib)) // Reactor's automatic context propagation needs it to carry the Sentry scope across threads. "agentFixtureImplementation"(libs.context.propagation) + "agentFixtureImplementation"(libs.caffeine) "agentFixtureImplementation"(libs.otel) + "agentFixtureImplementation"(libs.springboot3.starter.cache) + "agentFixtureImplementation"(libs.springboot3.starter.graphql) "agentFixtureImplementation"(libs.springboot3.starter.jdbc) "agentFixtureImplementation"(libs.springboot3.starter.web) "agentFixtureImplementation"(libs.springboot3.starter.webflux) + "agentFixtureImplementation"(libs.spring.kafka3) "agentFixtureRuntimeOnly"(libs.hsqldb) otelAgentJar(projects.sentryOpentelemetry.sentryOpentelemetryAgent) @@ -311,6 +317,7 @@ testing { // For TestRestTemplate. Only the app's JVM runs the agent, so it doesn't trace the tests. implementation(libs.springboot3.starter.test) implementation(libs.springboot3.starter.web) + implementation(libs.spring.kafka3.test) } targets.all { testTask.configure { runAgentFixture(autoInit = true) } } } @@ -350,6 +357,7 @@ listOf( "compileIntegrationTestKotlin", "compileIntegrationTestWebfluxKotlin", "compileIntegrationTestOtelNoAgentKotlin", + "compileAgentFixtureKotlin", ) .forEach { tasks.named(it) { compilerOptions.javaParameters = true } diff --git a/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/AgentFixtureApplication.kt b/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/AgentFixtureApplication.kt index c4d0485737f..8fbd0a70ca6 100644 --- a/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/AgentFixtureApplication.kt +++ b/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/AgentFixtureApplication.kt @@ -8,6 +8,7 @@ import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.web.client.RestTemplateBuilder import org.springframework.boot.web.context.WebServerPortFileWriter +import org.springframework.cache.annotation.EnableCaching import org.springframework.context.annotation.Bean import org.springframework.jdbc.core.JdbcTemplate import org.springframework.stereotype.Service @@ -26,6 +27,7 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder // kotlin-spring plugin. @SpringBootApplication +@EnableCaching open class AgentFixtureApplication { @Bean open fun restTemplate(builder: RestTemplateBuilder): RestTemplate = builder.build() diff --git a/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/CacheController.kt b/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/CacheController.kt new file mode 100644 index 00000000000..fd38c186535 --- /dev/null +++ b/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/CacheController.kt @@ -0,0 +1,43 @@ +package io.sentry.spring.boot.jakarta.agentfixture + +import java.util.concurrent.ConcurrentHashMap +import org.springframework.cache.annotation.CacheEvict +import org.springframework.cache.annotation.CachePut +import org.springframework.cache.annotation.Cacheable +import org.springframework.stereotype.Service +import org.springframework.web.bind.annotation.DeleteMapping +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 + +@RestController +@RequestMapping("/cache/") +open class CacheController(private val todoCache: TodoCache) { + @GetMapping("{id}") open fun get(@PathVariable("id") id: Long): Todo? = todoCache.get(id) + + @PostMapping open fun save(@RequestBody todo: Todo): Todo = todoCache.save(todo) + + @DeleteMapping("{id}") open fun delete(@PathVariable("id") id: Long) = todoCache.delete(id) +} + +/** Reads and writes todos through Spring's cache abstraction, which Sentry traces. */ +@Service +open class TodoCache { + private val store = ConcurrentHashMap() + + @Cacheable(value = ["todos"], key = "#id") open fun get(id: Long): Todo? = store[id] + + @CachePut(value = ["todos"], key = "#todo.id") + open fun save(todo: Todo): Todo { + store[todo.id!!] = todo + return todo + } + + @CacheEvict(value = ["todos"], key = "#id") + open fun delete(id: Long) { + store.remove(id) + } +} diff --git a/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/GraphqlControllers.kt b/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/GraphqlControllers.kt new file mode 100644 index 00000000000..0f7ce9530d0 --- /dev/null +++ b/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/GraphqlControllers.kt @@ -0,0 +1,83 @@ +package io.sentry.spring.boot.jakarta.agentfixture + +import java.util.UUID +import java.util.concurrent.CompletableFuture +import org.dataloader.DataLoader +import org.springframework.graphql.data.method.annotation.Argument +import org.springframework.graphql.data.method.annotation.BatchMapping +import org.springframework.graphql.data.method.annotation.MutationMapping +import org.springframework.graphql.data.method.annotation.QueryMapping +import org.springframework.graphql.data.method.annotation.SchemaMapping +import org.springframework.graphql.execution.BatchLoaderRegistry +import org.springframework.stereotype.Controller +import reactor.core.publisher.Mono + +class Project(val slug: String) + +enum class ProjectStatus { + ACTIVE, + COMMUNITY, +} + +class Task(val id: String, val name: String, val assigneeId: String?, val creatorId: String?) + +class Assignee(val id: String, val name: String) + +class Creator(val id: String, val name: String) + +/** Resolvers that throw when given a "crash" argument, so tests can check error reporting. */ +@Controller +open class ProjectController { + @QueryMapping + open fun greeting(@Argument name: String): String { + if (name == "crash") { + throw RuntimeException("causing an error for $name") + } + return "Hello $name!" + } + + @QueryMapping open fun project(@Argument slug: String): Project = Project(slug) + + @SchemaMapping(typeName = "Project", field = "status") + open fun status(project: Project): ProjectStatus = ProjectStatus.COMMUNITY + + @MutationMapping + open fun addProject(@Argument slug: String): String { + if (slug == "addprojectcrash") { + throw RuntimeException("causing a project add error for $slug") + } + return UUID.randomUUID().toString() + } + + @QueryMapping + open fun tasks(@Argument projectSlug: String): List = + listOf( + Task("T1", "Create a new API", "A3", "C3"), + Task("T2", "Update dependencies", "A1", "C1"), + Task("T3", "Document API", "A1", "C1"), + Task("T4", "Merge community PRs", "A2", "C2"), + Task("T5", "Plan more work", null, null), + ) +} + +/** Loads a task's assignee with a batch mapping and its creator with a data loader. */ +@Controller +open class TaskRelationsController(batchLoaderRegistry: BatchLoaderRegistry) { + init { + batchLoaderRegistry + .forTypePair(String::class.java, Creator::class.java) + .registerMappedBatchLoader { ids, _ -> + Mono.just(ids.associateWith { Creator(it, "Name$it") }) + } + } + + @BatchMapping(typeName = "Task", field = "assignee") + open fun assignee(tasks: List): Map = + tasks + .filter { it.assigneeId != null } + .associateWith { Assignee(it.assigneeId!!, "Name${it.assigneeId}") } + + @SchemaMapping(typeName = "Task") + open fun creator(task: Task, creators: DataLoader): CompletableFuture? = + task.creatorId?.let { creators.load(it) } +} diff --git a/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/KafkaController.kt b/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/KafkaController.kt new file mode 100644 index 00000000000..fe2f45dc8ae --- /dev/null +++ b/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/KafkaController.kt @@ -0,0 +1,28 @@ +package io.sentry.spring.boot.jakarta.agentfixture + +import org.springframework.kafka.annotation.KafkaListener +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.stereotype.Component +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +const val KAFKA_TOPIC = "sentry-topic" + +@RestController +@RequestMapping("/kafka") +open class KafkaController(private val kafkaTemplate: KafkaTemplate) { + @GetMapping("/produce") + open fun produce(@RequestParam message: String): String { + kafkaTemplate.send(KAFKA_TOPIC, message) + return "Message sent: $message" + } +} + +@Component +open class KafkaConsumer { + /** Does nothing with the message; Sentry traces the listener invocation. */ + @KafkaListener(topics = [KAFKA_TOPIC], groupId = "sentry-integration-test") + open fun listen(message: String) {} +} diff --git a/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/MetricController.kt b/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/MetricController.kt new file mode 100644 index 00000000000..a775db2838a --- /dev/null +++ b/sentry-spring-boot-jakarta/src/agentFixture/kotlin/io/sentry/spring/boot/jakarta/agentfixture/MetricController.kt @@ -0,0 +1,33 @@ +package io.sentry.spring.boot.jakarta.agentfixture + +import io.sentry.Sentry +import io.sentry.metrics.MetricsUnit +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/metric/") +open class MetricController { + @GetMapping("count") + open fun count(): String { + Sentry.setAttribute("user.type", "admin") + Sentry.setAttribute("feature.version", 2) + Sentry.metrics().count("countMetric") + return "count metric increased" + } + + @GetMapping("gauge/{value}") + open fun gauge(@PathVariable("value") value: Long): String { + Sentry.metrics().gauge("memory.free", value.toDouble(), MetricsUnit.Information.BYTE) + return "gauge metric tracked" + } + + @GetMapping("distribution/{value}") + open fun distribution(@PathVariable("value") value: Long): String { + Sentry.metrics() + .distribution("distributionMetric", value.toDouble(), MetricsUnit.Duration.MILLISECOND) + return "distribution metric tracked" + } +} diff --git a/sentry-spring-boot-jakarta/src/agentFixture/resources/application.properties b/sentry-spring-boot-jakarta/src/agentFixture/resources/application.properties index c5be9125800..945bf9fdf34 100644 --- a/sentry-spring-boot-jakarta/src/agentFixture/resources/application.properties +++ b/sentry-spring-boot-jakarta/src/agentFixture/resources/application.properties @@ -1,2 +1,3 @@ spring.datasource.url=jdbc:p6spy:hsqldb:mem:testdb spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver +spring.kafka.consumer.auto-offset-reset=earliest diff --git a/sentry-spring-boot-jakarta/src/agentFixture/resources/graphql/schema.graphqls b/sentry-spring-boot-jakarta/src/agentFixture/resources/graphql/schema.graphqls new file mode 100644 index 00000000000..fc2a8fd4c27 --- /dev/null +++ b/sentry-spring-boot-jakarta/src/agentFixture/resources/graphql/schema.graphqls @@ -0,0 +1,38 @@ +type Query { + greeting(name: String!): String! + project(slug: ID!): Project + tasks(projectSlug: ID!): [Task] +} + +type Mutation { + addProject(slug: ID!): String! +} + +type Project { + slug: ID! + status: ProjectStatus! +} + +enum ProjectStatus { + ACTIVE + COMMUNITY +} + +type Task { + id: String! + name: String! + assigneeId: String + assignee: Assignee + creatorId: String + creator: Creator +} + +type Assignee { + id: String! + name: String! +} + +type Creator { + id: String! + name: String! +} diff --git a/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/AgentApp.kt b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/AgentApp.kt index d03462f0264..9f9b0fedb98 100644 --- a/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/AgentApp.kt +++ b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/AgentApp.kt @@ -4,6 +4,7 @@ import io.sentry.test.ChildJvmApp import io.sentry.test.MockSentryServer import org.springframework.boot.test.web.client.TestRestTemplate import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.kafka.test.EmbeddedKafkaKraftBroker /** * The agent fixture app, running in a child JVM with the built OpenTelemetry agent attached, and @@ -18,6 +19,9 @@ object AgentApp { val server = MockSentryServer() + // An in-JVM KRaft broker with the fixture's KAFKA_TOPIC, so no Docker is needed. + private val kafka = EmbeddedKafkaKraftBroker(1, 1, "sentry-topic").apply { afterPropertiesSet() } + private val app = ChildJvmApp.start( mainClass = "io.sentry.spring.boot.jakarta.agentfixture.AgentFixtureApplicationKt", @@ -30,6 +34,8 @@ object AgentApp { "SENTRY_DSN" to server.dsn, "SENTRY_TRACES_SAMPLE_RATE" to "1.0", "SENTRY_LOGS_ENABLED" to "true", + "SENTRY_ENABLE_CACHE_TRACING" to "true", + "SENTRY_ENABLE_QUEUE_TRACING" to "true", "SENTRY_PROFILE_SESSION_SAMPLE_RATE" to "1.0", "SENTRY_PROFILE_LIFECYCLE" to "TRACE", "SENTRY_PROFILING_TRACES_DIR_PATH" to @@ -38,7 +44,7 @@ object AgentApp { "OTEL_METRICS_EXPORTER" to "none", "OTEL_LOGS_EXPORTER" to "none", ), - args = listOf("--server.port=0"), + args = listOf("--server.port=0", "--spring.kafka.bootstrap-servers=${kafka.brokersAsString}"), ) init { @@ -46,6 +52,7 @@ object AgentApp { .addShutdownHook( Thread { app.close() + kafka.destroy() server.close() } ) diff --git a/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/CacheIntegrationTest.kt b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/CacheIntegrationTest.kt new file mode 100644 index 00000000000..65e0afc1516 --- /dev/null +++ b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/CacheIntegrationTest.kt @@ -0,0 +1,35 @@ +package io.sentry.spring.boot.jakarta.otelagentintegrationtest + +import com.google.common.truth.Truth.assertThat +import io.sentry.test.hasSpanWithOp +import io.sentry.test.transaction +import kotlin.test.Test +import org.springframework.http.HttpEntity +import org.springframework.http.HttpMethod + +class CacheIntegrationTest : IntegrationTest() { + + @Test + fun `cache put and get produce spans`() { + val saved = restTemplate.postForEntity("/cache/", Todo(1L, "test-todo"), Todo::class.java) + assertThat(saved.statusCode.value()).isEqualTo(200) + assertThat(awaitTransaction("POST /cache/").hasSpanWithOp("cache.put")).isTrue() + + val fetched = restTemplate.getForEntity("/cache/1", Todo::class.java) + assertThat(fetched.statusCode.value()).isEqualTo(200) + assertThat(fetched.body!!.title).isEqualTo("test-todo") + assertThat(awaitTransaction("GET /cache/{id}").hasSpanWithOp("cache.get")).isTrue() + } + + @Test + fun `cache evict produces span`() { + val response = + restTemplate.exchange("/cache/1", HttpMethod.DELETE, HttpEntity.EMPTY, Void::class.java) + assertThat(response.statusCode.value()).isEqualTo(200) + + assertThat(awaitTransaction("DELETE /cache/{id}").hasSpanWithOp("cache.evict")).isTrue() + } + + private fun awaitTransaction(name: String) = + server.awaitEnvelope { it.transaction()?.transaction == name }.transaction()!! +} diff --git a/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/GraphqlIntegrationTest.kt b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/GraphqlIntegrationTest.kt new file mode 100644 index 00000000000..a4168b85903 --- /dev/null +++ b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/GraphqlIntegrationTest.kt @@ -0,0 +1,116 @@ +package io.sentry.spring.boot.jakarta.otelagentintegrationtest + +import com.fasterxml.jackson.databind.JsonNode +import com.google.common.truth.Truth.assertThat +import io.sentry.protocol.SentryTransaction +import io.sentry.test.event +import io.sentry.test.hasSpanWithOpAndDescription +import io.sentry.test.transaction +import kotlin.test.Test + +/** + * The agent traces each GraphQL operation as one span, with the operation's name as its op and + * description, so the queries are named. + */ +class GraphqlIntegrationTest : IntegrationTest() { + + @Test + fun `query is traced`() { + val response = graphql("""query GreetingQuery { greeting(name: "world") }""") + + assertThat(response.has("errors")).isFalse() + assertThat(response["data"]["greeting"].asText()).isEqualTo("Hello world!") + awaitTransactionWithSpan("query GreetingQuery") + } + + @Test + fun `resolver error is reported with its transaction`() { + val response = graphql("""query GreetingQuery { greeting(name: "crash") }""") + + assertThat(response["errors"].size()).isEqualTo(1) + val transaction = awaitTransactionOfError("causing an error for crash") + assertThat(transaction.hasOperationSpan("query GreetingQuery")).isTrue() + } + + @Test + fun `query with a schema mapping is traced`() { + val response = graphql("""query ProjectQuery { project(slug: "proj-slug") { slug status } }""") + + assertThat(response.has("errors")).isFalse() + assertThat(response["data"]["project"]["slug"].asText()).isEqualTo("proj-slug") + awaitTransactionWithSpan("query ProjectQuery") + } + + @Test + fun `mutation is traced`() { + val response = graphql("""mutation AddProjectMutation { addProject(slug: "proj-slug") }""") + + assertThat(response.has("errors")).isFalse() + assertThat(response["data"]["addProject"].asText()).isNotEmpty() + awaitTransactionWithSpan("mutation AddProjectMutation") + } + + @Test + fun `mutation error is reported with its transaction`() { + val response = + graphql("""mutation AddProjectMutation { addProject(slug: "addprojectcrash") }""") + + assertThat(response["errors"].size()).isEqualTo(1) + assertThat(response.path("data").path("addProject").asText(null)).isNull() + val transaction = awaitTransactionOfError("causing a project add error for addprojectcrash") + assertThat(transaction.hasOperationSpan("mutation AddProjectMutation")).isTrue() + } + + @Test + fun `query with batch loaded fields is traced`() { + val response = + graphql( + """query TasksAndAssigneesQuery { tasks(projectSlug: "project-slug") { id assigneeId assignee { id } creatorId creator { id } } }""" + ) + + assertThat(response.has("errors")).isFalse() + val tasks = response["data"]["tasks"] + assertThat(tasks.size()).isEqualTo(5) + assertThat(tasks[0]["id"].asText()).isEqualTo("T1") + assertThat(tasks[0]["assignee"]["id"].asText()).isEqualTo("A3") + assertThat(tasks[0]["creator"]["id"].asText()).isEqualTo("C3") + + awaitTransactionWithSpan("query TasksAndAssigneesQuery") + } + + private fun graphql(query: String): JsonNode { + val response = + restTemplate.postForEntity("/graphql", mapOf("query" to query), JsonNode::class.java) + assertThat(response.statusCode.value()).isEqualTo(200) + return response.body!! + } + + private fun awaitTransactionWithSpan(operation: String): SentryTransaction = + server + .awaitEnvelope { + it.transaction()?.let { t -> + t.transaction == "POST /graphql" && t.hasOperationSpan(operation) + } == true + } + .transaction()!! + + private fun SentryTransaction.hasOperationSpan(operation: String): Boolean = + hasSpanWithOpAndDescription(operation, operation) + + /** Finds the error for [exceptionMessage], then the transaction it belongs to by trace ID. */ + private fun awaitTransactionOfError(exceptionMessage: String): SentryTransaction { + val error = + server + .awaitEnvelope { envelope -> + envelope.event()?.let { event -> + event.message?.message?.startsWith("Unresolved RuntimeException for executionId ") == + true && event.exceptions?.any { it.value == exceptionMessage } == true + } == true + } + .event()!! + val traceId = error.contexts.trace!!.traceId + return server + .awaitEnvelope { it.transaction()?.contexts?.trace?.traceId == traceId } + .transaction()!! + } +} diff --git a/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/KafkaOtelCoexistenceIntegrationTest.kt b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/KafkaOtelCoexistenceIntegrationTest.kt new file mode 100644 index 00000000000..851da7aae25 --- /dev/null +++ b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/KafkaOtelCoexistenceIntegrationTest.kt @@ -0,0 +1,44 @@ +package io.sentry.spring.boot.jakarta.otelagentintegrationtest + +import com.google.common.truth.Truth.assertThat +import io.sentry.protocol.SentryTransaction +import io.sentry.test.transaction +import kotlin.test.Test + +/** With OpenTelemetry active, it traces Kafka instead of Sentry's own Spring Kafka integration. */ +class KafkaOtelCoexistenceIntegrationTest : IntegrationTest() { + + @Test + fun `OpenTelemetry traces both sides of a message instead of Sentry's Kafka integration`() { + val message = "otel-coexistence-test" + assertThat(restTemplate.getForObject("/kafka/produce?message=$message", String::class.java)) + .isEqualTo("Message sent: $message") + + val producer = + server + .awaitEnvelope { + it.transaction()?.let { t -> + t.transaction == "GET /kafka/produce" && t.request?.queryString == "message=$message" + } == true + } + .transaction()!! + assertTracedByOpenTelemetry(producer) + + val traceId = producer.contexts.trace!!.traceId + val consumer = + server + .awaitEnvelope { + it.transaction()?.contexts?.trace?.let { trace -> + trace.operation == "queue.process" && trace.traceId == traceId + } == true + } + .transaction()!! + assertTracedByOpenTelemetry(consumer) + assertThat(consumer.contexts.trace!!.data).containsEntry("messaging.system", "kafka") + } + + private fun assertTracedByOpenTelemetry(transaction: SentryTransaction) { + assertThat(transaction.contexts.trace!!.origin).isEqualTo("auto.opentelemetry") + assertThat(transaction.sdk?.integrationSet.orEmpty()).doesNotContain("SpringKafka") + } +} diff --git a/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/MetricsIntegrationTest.kt b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/MetricsIntegrationTest.kt new file mode 100644 index 00000000000..3ac6b4d2f0c --- /dev/null +++ b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/MetricsIntegrationTest.kt @@ -0,0 +1,46 @@ +package io.sentry.spring.boot.jakarta.otelagentintegrationtest + +import com.google.common.truth.Truth.assertThat +import io.sentry.SentryMetricsEvents +import io.sentry.test.hasMetric +import io.sentry.test.hasMetricWithAttribute +import io.sentry.test.metrics +import kotlin.test.Test + +class MetricsIntegrationTest : IntegrationTest() { + + @Test + fun `count metric carries the scope's attributes`() { + assertThat(restTemplate.getForObject("/metric/count", String::class.java)) + .isEqualTo("count metric increased") + + val metrics = awaitMetric("countMetric") + assertThat(metrics.hasMetric("countMetric", "counter", 1.0)).isTrue() + assertThat(metrics.hasMetricWithAttribute("countMetric", "user.type", "admin")).isTrue() + assertThat(metrics.hasMetricWithAttribute("countMetric", "feature.version", 2)).isTrue() + } + + @Test + fun `gauge metric`() { + assertThat(restTemplate.getForObject("/metric/gauge/14", String::class.java)) + .isEqualTo("gauge metric tracked") + + assertThat(awaitMetric("memory.free").hasMetric("memory.free", "gauge", 14.0)).isTrue() + } + + @Test + fun `distribution metric`() { + assertThat(restTemplate.getForObject("/metric/distribution/23", String::class.java)) + .isEqualTo("distribution metric tracked") + + assertThat( + awaitMetric("distributionMetric").hasMetric("distributionMetric", "distribution", 23.0) + ) + .isTrue() + } + + private fun awaitMetric(name: String): SentryMetricsEvents = + server + .awaitEnvelope { it.metrics()?.items?.any { metric -> metric.name == name } == true } + .metrics()!! +} diff --git a/test/system-test-runner.py b/test/system-test-runner.py index 3fa4b7f3b18..e2d0d0ef802 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -71,12 +71,10 @@ KAFKA_BROKER_REQUIRED_MODULES = { "sentry-samples-console", "sentry-samples-spring-boot-opentelemetry", - "sentry-samples-spring-boot-jakarta-opentelemetry", "sentry-samples-spring-boot-4-opentelemetry", } KAFKA_PROFILE_REQUIRED_MODULES = { "sentry-samples-spring-boot-opentelemetry", - "sentry-samples-spring-boot-jakarta-opentelemetry", "sentry-samples-spring-boot-4-opentelemetry", } @@ -839,8 +837,6 @@ def get_available_modules(self) -> List[ModuleConfig]: ModuleConfig("sentry-samples-spring-jakarta", "false", "true", "false"), ModuleConfig("sentry-samples-spring-boot-opentelemetry", "true", "true", "false"), ModuleConfig("sentry-samples-spring-boot-opentelemetry", "true", "false", "false"), - ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "true", "false"), - ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "false", "false"), ModuleConfig("sentry-samples-spring-boot-4-opentelemetry", "true", "true", "false"), ModuleConfig("sentry-samples-spring-boot-4-opentelemetry", "true", "false", "false"), ModuleConfig("sentry-samples-spring-boot-4-otlp", "false", "true", "false"), From c57bdaee73a50d6636abad67a414bc65b287d716 Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 20 Oct 2026 21:00:55 +0200 Subject: [PATCH 2/2] test(spring-boot): Check the Boot 3 agent GraphQL task query's assignee and creator ids Co-Authored-By: Claude Opus 5.5 (1M context) --- .../jakarta/otelagentintegrationtest/GraphqlIntegrationTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/GraphqlIntegrationTest.kt b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/GraphqlIntegrationTest.kt index a4168b85903..8d54574b613 100644 --- a/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/GraphqlIntegrationTest.kt +++ b/sentry-spring-boot-jakarta/src/integrationTestOtelAgent/kotlin/io/sentry/spring/boot/jakarta/otelagentintegrationtest/GraphqlIntegrationTest.kt @@ -72,6 +72,8 @@ class GraphqlIntegrationTest : IntegrationTest() { val tasks = response["data"]["tasks"] assertThat(tasks.size()).isEqualTo(5) assertThat(tasks[0]["id"].asText()).isEqualTo("T1") + assertThat(tasks[0]["assigneeId"].asText()).isEqualTo("A3") + assertThat(tasks[0]["creatorId"].asText()).isEqualTo("C3") assertThat(tasks[0]["assignee"]["id"].asText()).isEqualTo("A3") assertThat(tasks[0]["creator"]["id"].asText()).isEqualTo("C3")