Skip to content
Merged
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
9 changes: 6 additions & 3 deletions .cursor/rules/e2e_tests.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,14 @@ match on values only they produce.
## The OpenTelemetry agent

The agent must not run in the test's JVM, because it would instrument the test's own HTTP client
and server. The Spring Boot modules have an `agentFixture` source set with the app. `AgentApp`
starts it once per test JVM in a child process, with `ChildJvmApp` and the agent jar passed through
and server. The `io.sentry.otel-agent-tests` plugin in `build-logic` gives the Spring Boot modules
an `agentFixture` source set with the app, the `integrationTestOtelAgent` suite and the built agent
jar; each module adds only the dependencies of its Spring Boot line. `AgentApp` starts the app once
per test JVM in a child process, with `ChildJvmApp` and the agent jar passed through
`ChildJvmTestArguments`. The suite's task runs with the agent initializing Sentry, and
`integrationTestOtelAgentWithoutAutoInit` runs the same tests with the Spring Boot starter doing
it. Gradle runs the two in parallel.
it. Gradle runs the two in parallel. `otelAgentTests()` in a `springBootMatrix` block registers
both on each version.

## Running them

Expand Down
4 changes: 4 additions & 0 deletions build-logic/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@ gradlePlugin {
id = "io.sentry.spring-boot-matrix"
implementationClass = "io.sentry.gradle.SpringBootMatrixPlugin"
}
register("sentryOtelAgentTests") {
id = "io.sentry.otel-agent-tests"
implementationClass = "io.sentry.gradle.OtelAgentTestsPlugin"
}
}
}
107 changes: 107 additions & 0 deletions build-logic/src/main/kotlin/io/sentry/gradle/OtelAgentTestsPlugin.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package io.sentry.gradle

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.attributes.Bundling
import org.gradle.api.attributes.Usage
import org.gradle.api.file.FileCollection
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.plugins.jvm.JvmTestSuite
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.testing.Test
import org.gradle.language.base.plugins.LifecycleBasePlugin
import org.gradle.testing.base.TestingExtension

private const val AGENT_FIXTURE = "agentFixture"
private const val OTEL_AGENT_JAR = "otelAgentJar"
private const val SUITE = "integrationTestOtelAgent"
private const val WITHOUT_AUTO_INIT = "${SUITE}WithoutAutoInit"

/**
* Runs a Spring Boot module's OpenTelemetry agent tests against an app in a child JVM, with the
* built agent attached.
*
* The agent must not run in the test's JVM, because it would instrument the test's own HTTP client
* and server. So the app lives in the `agentFixture` source set and runs on its own classpath, like
* a real app. The `integrationTestOtelAgent` suite's task runs the tests with the agent
* initializing Sentry, and `integrationTestOtelAgentWithoutAutoInit` runs them with the Spring Boot
* starter doing it. Each test JVM starts one app, so the second mode needs a task of its own, which
* Gradle runs in parallel with the suite's.
*
* The module adds the app's and the suite's dependencies, which differ per Spring Boot line.
*/
class OtelAgentTestsPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.apply(JavaPlugin::class.java)
val sourceSets = project.extensions.getByType(SourceSetContainer::class.java)
val agentFixture = sourceSets.create(AGENT_FIXTURE)

// A plain project dependency on the agent module resolves to its unshadowed jar, which holds
// only AgentMarker. The agent itself is the shadowed jar, which already bundles its
// dependencies.
project.configurations.create(OTEL_AGENT_JAR) {
isCanBeResolved = true
isCanBeConsumed = false
isTransitive = false
attributes {
attribute(
Usage.USAGE_ATTRIBUTE,
project.objects.named(Usage::class.java, Usage.JAVA_RUNTIME),
)
attribute(
Bundling.BUNDLING_ATTRIBUTE,
project.objects.named(Bundling::class.java, Bundling.SHADOWED),
)
}
}
project.dependencies.add(
OTEL_AGENT_JAR,
project.dependencies.project(
mapOf("path" to ":sentry-opentelemetry:sentry-opentelemetry-agent")
),
)

val testing = project.extensions.getByType(TestingExtension::class.java)
testing.suites.register(SUITE, JvmTestSuite::class.java) {
useJUnit()
targets.all {
testTask.configure { runAgentFixture(autoInit = true, agentFixture.runtimeClasspath) }
}
}

project.tasks.register(WITHOUT_AUTO_INIT, Test::class.java) {
description = "Runs the OpenTelemetry agent tests with SENTRY_AUTO_INIT=false."
group = LifecycleBasePlugin.VERIFICATION_GROUP
val sources = sourceSets.getByName(SUITE)
testClassesDirs = sources.output.classesDirs
classpath = sources.runtimeClasspath
useJUnit()
runAgentFixture(autoInit = false, agentFixture.runtimeClasspath)
}
}
}

/** Registers both OpenTelemetry agent test tasks on each Spring Boot version of the matrix. */
fun SpringBootMatrixExtension.otelAgentTests() {
test(SUITE) { version ->
runAgentFixture(autoInit = true, runtimeClasspath(AGENT_FIXTURE, version))
}
test(SUITE, name = WITHOUT_AUTO_INIT) { version ->
runAgentFixture(autoInit = false, runtimeClasspath(AGENT_FIXTURE, version))
}
}

/**
* Has the tests start the app on [appClasspath] in a child JVM, with the built agent attached.
* [autoInit] decides whether the agent initializes Sentry or, as with `SENTRY_AUTO_INIT=false`, the
* Spring Boot starter.
*/
private fun Test.runAgentFixture(autoInit: Boolean, appClasspath: FileCollection) {
jvmArgumentProviders.add(
project.objects.newInstance(ChildJvmTestArguments::class.java).apply {
agentJar.from(project.configurations.getByName(OTEL_AGENT_JAR))
this.appClasspath.from(appClasspath)
}
)
systemProperty("sentry.test.agentAutoInit", autoInit)
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ fun isolateClasses(jars: Iterable<File>): CopySpec = copySpec {
}
}

// Don't publish non-shadowed jar (shadowJar is in shadowRuntimeElements)
// Only the agent, the shadow jar in shadowRuntimeElements, is published. The plain jar in
// runtimeElements holds just AgentMarker. A project dependency on this module resolves to it, so
// the Spring Boot modules' unit tests get AgentMarker on their classpath without the agent.
with(components["java"] as AdhocComponentWithVariants) {
configurations.forEach {
withVariantsFromConfiguration(configurations["apiElements"]) { skip() }
Expand Down
66 changes: 6 additions & 60 deletions sentry-spring-boot-4/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import io.sentry.gradle.ChildJvmTestArguments
import io.sentry.gradle.otelAgentTests
import net.ltgt.gradle.errorprone.errorprone
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.springframework.boot.gradle.plugin.SpringBootPlugin
Expand All @@ -7,6 +7,7 @@ plugins {
`java-library`
id("io.sentry.javadoc")
id("io.sentry.spring-boot-matrix")
id("io.sentry.otel-agent-tests")
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.spring)
alias(libs.plugins.errorprone)
Expand Down Expand Up @@ -163,22 +164,7 @@ kotlin {
}
}

// The OpenTelemetry agent tests run this app in a child JVM, on its own classpath like a real app
// with the agent attached, rather than on the test's classpath.
val agentFixture = sourceSets.create("agentFixture")

// The built agent jar, rather than the plain jar the unit tests compile against.
val otelAgentJar =
configurations.create("otelAgentJar") {
isCanBeResolved = true
isCanBeConsumed = false
isTransitive = false
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.SHADOWED))
}
}

// The app the OpenTelemetry agent tests run in a child JVM.
dependencies {
"agentFixtureImplementation"(platform(SpringBootPlugin.BOM_COORDINATES))
"agentFixtureImplementation"(projects.sentrySpringBoot4Starter)
Expand All @@ -201,8 +187,6 @@ dependencies {
"agentFixtureImplementation"(libs.springboot4.starter.webclient)
"agentFixtureImplementation"(libs.springboot4.starter.webflux)
"agentFixtureRuntimeOnly"(libs.hsqldb)

otelAgentJar(projects.sentryOpentelemetry.sentryOpentelemetryAgent)
}

// Swaps Logback for Log4j2 where a classpath has both starters, which only
Expand Down Expand Up @@ -337,9 +321,8 @@ testing {
}
}

// Runs agentFixture's app with the agent initializing Sentry.
register<JvmTestSuite>("integrationTestOtelAgent") {
useJUnit()
// The OpenTelemetry agent tests, against agentFixture's app.
named<JvmTestSuite>("integrationTestOtelAgent") {
dependencies {
implementation(platform(SpringBootPlugin.BOM_COORDINATES))
implementation(projects.sentryTestSupport)
Expand All @@ -351,7 +334,6 @@ testing {
implementation(libs.springboot4.starter.test)
implementation(libs.spring.kafka4.test)
}
targets.all { testTask.configure { runAgentFixture(autoInit = true) } }
}

// The servlet app with the OpenTelemetry Spring Boot starter, set up by
Expand Down Expand Up @@ -389,37 +371,6 @@ testing {
}
}

/**
* Has the OpenTelemetry agent tests start agentFixture's app in a child JVM, with the built agent
* attached. [autoInit] decides whether the agent initializes Sentry or, as with
* `SENTRY_AUTO_INIT=false`, the Spring Boot starter. [appClasspath] is the app's classpath for
* another Spring Boot version.
*/
fun Test.runAgentFixture(
autoInit: Boolean,
appClasspath: FileCollection = agentFixture.runtimeClasspath,
) {
jvmArgumentProviders.add(
objects.newInstance<ChildJvmTestArguments>().apply {
agentJar.from(otelAgentJar)
this.appClasspath.from(appClasspath)
}
)
systemProperty("sentry.test.agentAutoInit", autoInit)
}

// The same tests with the Spring Boot starter initializing Sentry. Each test JVM starts one app, so
// this mode needs a task of its own, which Gradle runs in parallel with the suite's.
tasks.register<Test>("integrationTestOtelAgentWithoutAutoInit") {
description = "Runs the OpenTelemetry agent tests with SENTRY_AUTO_INIT=false."
group = LifecycleBasePlugin.VERIFICATION_GROUP
val sources = sourceSets["integrationTestOtelAgent"]
testClassesDirs = sources.output.classesDirs
classpath = sources.runtimeClasspath
useJUnit()
runAgentFixture(autoInit = false)
}

// The Spring Boot Gradle plugin compiles apps with parameter names, which Spring reads, for example
// to resolve `#id` in a cache key.
listOf(
Expand All @@ -441,10 +392,5 @@ springBootMatrix {
test("integrationTestWebflux")
test("integrationTestOtelNoAgent")
test("integrationTestOtlp")
test("integrationTestOtelAgent") { version ->
runAgentFixture(autoInit = true, appClasspath = runtimeClasspath("agentFixture", version))
}
test("integrationTestOtelAgent", name = "integrationTestOtelAgentWithoutAutoInit") { version ->
runAgentFixture(autoInit = false, appClasspath = runtimeClasspath("agentFixture", version))
}
otelAgentTests()
}
66 changes: 6 additions & 60 deletions sentry-spring-boot-jakarta/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import io.sentry.gradle.ChildJvmTestArguments
import io.sentry.gradle.otelAgentTests
import net.ltgt.gradle.errorprone.errorprone
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.springframework.boot.gradle.plugin.SpringBootPlugin
Expand All @@ -7,6 +7,7 @@ plugins {
`java-library`
id("io.sentry.javadoc")
id("io.sentry.spring-boot-matrix")
id("io.sentry.otel-agent-tests")
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.errorprone)
alias(libs.plugins.gradle.versions)
Expand Down Expand Up @@ -146,22 +147,7 @@ tasks.jar {
}
}

// The OpenTelemetry agent tests run this app in a child JVM, on its own classpath like a real app
// with the agent attached, rather than on the test's classpath.
val agentFixture = sourceSets.create("agentFixture")

// The built agent jar, rather than the plain jar the unit tests compile against.
val otelAgentJar =
configurations.create("otelAgentJar") {
isCanBeResolved = true
isCanBeConsumed = false
isTransitive = false
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
attribute(Bundling.BUNDLING_ATTRIBUTE, objects.named(Bundling.SHADOWED))
}
}

// The app the OpenTelemetry agent tests run in a child JVM.
dependencies {
"agentFixtureImplementation"(platform(SpringBootPlugin.BOM_COORDINATES))
"agentFixtureImplementation"(projects.sentrySpringBootStarterJakarta)
Expand All @@ -182,8 +168,6 @@ dependencies {
"agentFixtureImplementation"(libs.springboot3.starter.webflux)
"agentFixtureImplementation"(libs.spring.kafka3)
"agentFixtureRuntimeOnly"(libs.hsqldb)

otelAgentJar(projects.sentryOpentelemetry.sentryOpentelemetryAgent)
}

// Swaps Logback for Log4j2 where a classpath has both starters, which only
Expand Down Expand Up @@ -311,9 +295,8 @@ testing {
}
}

// Runs agentFixture's app with the agent initializing Sentry.
register<JvmTestSuite>("integrationTestOtelAgent") {
useJUnit()
// The OpenTelemetry agent tests, against agentFixture's app.
named<JvmTestSuite>("integrationTestOtelAgent") {
dependencies {
implementation(platform(SpringBootPlugin.BOM_COORDINATES))
implementation(projects.sentryTestSupport)
Expand All @@ -324,42 +307,10 @@ testing {
implementation(libs.springboot3.starter.web)
implementation(libs.spring.kafka3.test)
}
targets.all { testTask.configure { runAgentFixture(autoInit = true) } }
}
}
}

/**
* Has the OpenTelemetry agent tests start agentFixture's app in a child JVM, with the built agent
* attached. [autoInit] decides whether the agent initializes Sentry or, as with
* `SENTRY_AUTO_INIT=false`, the Spring Boot starter. [appClasspath] is the app's classpath for
* another Spring Boot version.
*/
fun Test.runAgentFixture(
autoInit: Boolean,
appClasspath: FileCollection = agentFixture.runtimeClasspath,
) {
jvmArgumentProviders.add(
objects.newInstance<ChildJvmTestArguments>().apply {
agentJar.from(otelAgentJar)
this.appClasspath.from(appClasspath)
}
)
systemProperty("sentry.test.agentAutoInit", autoInit)
}

// The same tests with the Spring Boot starter initializing Sentry. Each test JVM starts one app, so
// this mode needs a task of its own, which Gradle runs in parallel with the suite's.
tasks.register<Test>("integrationTestOtelAgentWithoutAutoInit") {
description = "Runs the OpenTelemetry agent tests with SENTRY_AUTO_INIT=false."
group = LifecycleBasePlugin.VERIFICATION_GROUP
val sources = sourceSets["integrationTestOtelAgent"]
testClassesDirs = sources.output.classesDirs
classpath = sources.runtimeClasspath
useJUnit()
runAgentFixture(autoInit = false)
}

// The Spring Boot Gradle plugin compiles apps with parameter names, which Spring reads, for example
// to resolve `#id` in a cache key.
listOf(
Expand All @@ -379,10 +330,5 @@ springBootMatrix {
test("integrationTestLog4j2")
test("integrationTestWebflux")
test("integrationTestOtelNoAgent")
test("integrationTestOtelAgent") { version ->
runAgentFixture(autoInit = true, appClasspath = runtimeClasspath("agentFixture", version))
}
test("integrationTestOtelAgent", name = "integrationTestOtelAgentWithoutAutoInit") { version ->
runAgentFixture(autoInit = false, appClasspath = runtimeClasspath("agentFixture", version))
}
otelAgentTests()
}
Loading
Loading