From 6497aa187f7370d6abd9709ed7c82c47f3f777f2 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 13 Aug 2025 09:14:26 +0200 Subject: [PATCH 01/10] Add Tomcat support in the system test runner and tests to the spring-jakarta sample --- .gitignore | 3 + gradle/libs.versions.toml | 8 +- .../build.gradle.kts | 33 ++- .../sentry/samples/spring/jakarta/Main.java | 24 ++ .../samples/spring/jakarta/web/Person.java | 7 +- .../spring/jakarta/web/PersonController.java | 8 +- .../src/test/kotlin/io/sentry/DummyTest.kt | 12 + .../io/sentry/systemtest/PersonSystemTest.kt | 46 +++ test/system-test-runner.py | 278 +++++++++++++----- 9 files changed, 339 insertions(+), 80 deletions(-) create mode 100644 sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java create mode 100644 sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/DummyTest.kt create mode 100644 sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt diff --git a/.gitignore b/.gitignore index 90f9e885cf4..be4f11ce3d2 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ distributions/ sentry-spring-boot-starter-jakarta/src/main/resources/META-INF/spring.factories sentry-samples/sentry-samples-spring-boot-jakarta/spy.log sentry-mock-server.txt +tomcat-server.txt spring-server.txt +*.pid spy.log .kotlin +**/tomcat.8080/webapps/ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4379c63d615..690fa002436 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -121,8 +121,8 @@ reactor-core = { module = "io.projectreactor:reactor-core", version = "3.5.3" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } sentry-native-ndk = { module = "io.sentry:sentry-native-ndk", version = "0.8.4" } -servlet-api = { module = "javax.servlet:javax.servlet-api", version = "3.1.0" } -servlet-jakarta-api = { module = "jakarta.servlet:jakarta.servlet-api", version = "5.0.0" } +servlet-api = { module = "javax.servlet:javax.servlet-api", version = "4.0.1" } +servlet-jakarta-api = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.1.0" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-jdk14 = { module = "org.slf4j:slf4j-jdk14", version.ref = "slf4j" } slf4j2-api = { module = "org.slf4j:slf4j-api", version = "2.0.5" } @@ -152,6 +152,10 @@ springboot3-starter-jdbc = { module = "org.springframework.boot:spring-boot-star springboot3-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springboot3" } timber = { module = "com.jakewharton.timber:timber", version = "4.7.1" } +# tomcat libraries +tomcat-catalina = { module = "org.apache.tomcat:tomcat-catalina", version = "9.0.108" } +tomcat-catalina-jakarta = { module = "org.apache.tomcat:tomcat-catalina", version = "11.0.10" } + # test libraries androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version = "1.6.8" } androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTestCore" } diff --git a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts index 2ecc26c4045..03290c7840a 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts @@ -3,6 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { + application alias(libs.plugins.springboot3) apply false alias(libs.plugins.spring.dependency.management) kotlin("jvm") @@ -11,6 +12,8 @@ plugins { alias(libs.plugins.gretty) } +application { mainClass.set("io.sentry.samples.spring.jakarta.Main") } + group = "io.sentry.sample.spring-jakarta" version = "0.0.1-SNAPSHOT" @@ -37,16 +40,42 @@ dependencies { implementation(libs.logback.classic) implementation(libs.servlet.jakarta.api) implementation(libs.slf4j2.api) + + implementation(libs.tomcat.catalina.jakarta) + + testImplementation(projects.sentrySystemTestSupport) + testImplementation(libs.kotlin.test.junit) testImplementation(libs.springboot.starter.test) { exclude(group = "org.junit.vintage", module = "junit-vintage-engine") } } -tasks.withType().configureEach { useJUnitPlatform() } - tasks.withType().configureEach { kotlinOptions { freeCompilerArgs = listOf("-Xjsr305=strict") jvmTarget = JavaVersion.VERSION_17.toString() } } + +configure { test { java.srcDir("src/test/java") } } + +tasks.register("systemTest").configure { + group = "verification" + description = "Runs the System tests" + + outputs.upToDateWhen { false } + + maxParallelForks = 1 + + // Cap JVM args per test + minHeapSize = "128m" + maxHeapSize = "1g" + + 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-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java new file mode 100644 index 00000000000..38d0aeee5ef --- /dev/null +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java @@ -0,0 +1,24 @@ +package io.sentry.samples.spring.jakarta; + +import java.io.File; +import java.io.IOException; + +import org.apache.catalina.LifecycleException; +import org.apache.catalina.startup.Tomcat; + +public class Main { + + public static void main(String[] args) throws LifecycleException, IOException { + String webappPath = "./build/libs"; + String warName = "sentry-samples-spring-jakarta-0.0.1-SNAPSHOT"; + File war = new File(webappPath + "/" + warName + ".war"); + + Tomcat tomcat = new Tomcat(); + tomcat.setPort(8080); + tomcat.getConnector(); + + tomcat.addWebapp("/" + warName, war.getCanonicalPath()); + tomcat.start(); + tomcat.getServer().await(); + } +} diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java index ee1f4f0827e..6138cd6403e 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java @@ -1,10 +1,15 @@ package io.sentry.samples.spring.jakarta.web; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + public class Person { private final String firstName; private final String lastName; - public Person(String firstName, String lastName) { + @JsonCreator + public Person( + @JsonProperty("firstName") String firstName, @JsonProperty("lastName") String lastName) { this.firstName = firstName; this.lastName = lastName; } diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java index c5ee953810c..236a3e83d97 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java @@ -22,16 +22,12 @@ public PersonController(PersonService personService) { } @GetMapping("{id}") - Person person(@PathVariable Long id) { + Person person(@PathVariable("id") Long id) { Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); LOGGER.info("Loading person with id={}", id); - if (id > 10L) { - throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); - } else { - return personService.find(id); - } + throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); } @PostMapping diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/DummyTest.kt b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/DummyTest.kt new file mode 100644 index 00000000000..6f762b7e453 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/DummyTest.kt @@ -0,0 +1,12 @@ +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-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt new file mode 100644 index 00000000000..2e113203fd9 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -0,0 +1,46 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class PersonSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080/sentry-samples-spring-jakarta-0.0.1-SNAPSHOT") + testHelper.reset() + } + + @Test + fun `get person fails`() { + val restClient = testHelper.restClient + restClient.getPerson(1L) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionHaveOp(transaction, "http.server") + } + + Thread.sleep(10000) + + testHelper.ensureLogsReceived { logs, envelopeHeader -> + testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && + testHelper.doesContainLogWithBody(logs, "error Sentry logging") && + testHelper.doesContainLogWithBody(logs, "hello there world!") + } + } + + @Test + fun `create person works`() { + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = restClient.createPerson(person) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + } +} diff --git a/test/system-test-runner.py b/test/system-test-runner.py index fda5042c819..3013726de4e 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -16,6 +16,9 @@ # Start Sentry mock server python3 test/system-test-runner.py sentry start + # Start Spring app served by Tomcat + python3 test/system-test-runner.py tomcat start sentry-samples-spring + # Start Spring Boot app python3 test/system-test-runner.py spring start sentry-samples-spring-boot @@ -35,6 +38,7 @@ import time import signal import os +from enum import Enum import argparse import requests import threading @@ -48,6 +52,19 @@ except: pass +SENTRY_ENVIRONMENT_VARIABLES = { + "SENTRY_DSN": "http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0", + "SENTRY_TRACES_SAMPLE_RATE": "1.0", + "OTEL_TRACES_EXPORTER": "none", + "OTEL_METRICS_EXPORTER": "none", + "OTEL_LOGS_EXPORTER": "none", + "SENTRY_LOGS_ENABLED": "true" +} + +class ServerType(Enum): + TOMCAT = 0 + SPRING = 1 + def str_to_bool(value: str) -> str: """Convert true/false string to 1/0 string for internal compatibility.""" if value.lower() in ('true', '1'): @@ -57,6 +74,25 @@ def str_to_bool(value: str) -> str: else: raise ValueError(f"Invalid boolean value: {value}. Use 'true' or 'false'") +@dataclass +class Server: + name: str + pid_filepath: str + process: Optional[subprocess.Popen] = None + pid: Optional[int] = None + +def store_pid(server: Server): + # Store PID in instance variable and write to file + server.pid = server.process.pid + with open(server.pid_filepath, "w") as pid_file: + pid_file.write(str(server.pid)) + +def cleanup_pid(server: Server): + # Clean up PID file and instance variable + if os.path.exists(server.pid_filepath): + os.remove(server.pid_filepath) + server.pid = None + @dataclass class ModuleConfig: """Configuration for a test module.""" @@ -104,21 +140,15 @@ def has_agent_modules(self) -> bool: class SystemTestRunner: def __init__(self): - self.mock_server_process: Optional[subprocess.Popen] = None - self.spring_server_process: Optional[subprocess.Popen] = None - self.mock_server_pid: Optional[int] = None - self.spring_server_pid: Optional[int] = None - self.mock_server_pid_file = "sentry-mock-server.pid" - self.spring_server_pid_file = "spring-server.pid" + self.mock_server = Server(name="Mock", pid_filepath="sentry-mock-server.pid") + self.tomcat_server = Server(name="Tomcat", pid_filepath="tomcat-server.pid") + self.spring_server = Server(name="Spring", pid_filepath="spring-server.pid") # Load existing PIDs if available - self.mock_server_pid = self.read_pid_file(self.mock_server_pid_file) - self.spring_server_pid = self.read_pid_file(self.spring_server_pid_file) - - if self.mock_server_pid: - print(f"Found existing mock server PID: {self.mock_server_pid}") - if self.spring_server_pid: - print(f"Found existing Spring server PID: {self.spring_server_pid}") + for server in (self.mock_server, self.tomcat_server, self.spring_server): + server.pid = self.read_pid_file(server.pid_filepath) + if server.pid: + print(f"Found existing {server.name} server PID: {server.pid}") def read_pid_file(self, pid_file: str) -> Optional[int]: """Read PID from file if it exists.""" @@ -162,18 +192,15 @@ def start_sentry_mock_server(self) -> None: try: # Start the mock server in the background with open("sentry-mock-server.txt", "w") as log_file: - self.mock_server_process = subprocess.Popen( + self.mock_server.process = subprocess.Popen( ["python3", "test/system-test-sentry-server.py"], stdout=log_file, stderr=subprocess.STDOUT ) # Store PID in instance variable and write to file - self.mock_server_pid = self.mock_server_process.pid - with open(self.mock_server_pid_file, "w") as pid_file: - pid_file.write(str(self.mock_server_pid)) - - print(f"Started mock server with PID {self.mock_server_pid}") + store_pid(self.mock_server) + print(f"Started mock server with PID {self.mock_server.pid}") # Wait a moment for the server to start time.sleep(2) @@ -193,21 +220,19 @@ def stop_sentry_mock_server(self) -> None: print("Could not send graceful stop signal") # Kill the process - try process object first, then PID from file - if self.mock_server_process and self.mock_server_process.poll() is None: - print(f"Killing mock server process object with PID {self.mock_server_process.pid}") - self.mock_server_process.kill() - self.mock_server_process.wait(timeout=5) - elif self.mock_server_pid and self.is_process_running(self.mock_server_pid): - print(f"Killing mock server from PID file with PID {self.mock_server_pid}") - self.kill_process(self.mock_server_pid, "mock server") + if self.mock_server.process and self.mock_server.process.poll() is None: + print(f"Killing mock server process object with PID {self.mock_server.process.pid}") + self.mock_server.process.kill() + self.mock_server.process.wait(timeout=5) + elif self.mock_server.pid and self.is_process_running(self.mock_server.pid): + print(f"Killing mock server from PID file with PID {self.mock_server.pid}") + self.kill_process(self.mock_server.pid, "mock server") except Exception as e: print(f"Error stopping mock server: {e}") finally: # Clean up PID file and instance variable - if os.path.exists(self.mock_server_pid_file): - os.remove(self.mock_server_pid_file) - self.mock_server_pid = None + cleanup_pid(self.mock_server) def find_agent_jar(self) -> Optional[str]: """Find the OpenTelemetry agent JAR file.""" @@ -254,21 +279,35 @@ def ensure_agent_jar(self, skip_build: bool = False) -> Optional[str]: return agent_jar + def start_tomcat_server(self, sample_module: str) -> None: + # Build environment variables + env = os.environ.copy() + env.update(SENTRY_ENVIRONMENT_VARIABLES) + + try: + # Start the Tomcat server + with open("tomcat-server.txt", "w") as log_file: + self.tomcat_server.process = subprocess.Popen( + ["./gradlew", f"sentry-samples:{sample_module}:run"], + env=env, + stdout=log_file, + stderr=subprocess.STDOUT + ) + + store_pid(self.tomcat_server) + print(f"Started Tomcat server with PID {self.tomcat_server.pid}") + except Exception as e: + print(f"Failed to start Tomcat server: {e}") + raise + def start_spring_server(self, sample_module: str, java_agent: str, java_agent_auto_init: str) -> None: """Start a Spring Boot server for testing.""" print(f"Starting Spring server for {sample_module}...") # Build environment variables env = os.environ.copy() - env.update({ - "SENTRY_DSN": "http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0", - "SENTRY_AUTO_INIT": java_agent_auto_init, - "SENTRY_TRACES_SAMPLE_RATE": "1.0", - "OTEL_TRACES_EXPORTER": "none", - "OTEL_METRICS_EXPORTER": "none", - "OTEL_LOGS_EXPORTER": "none", - "SENTRY_LOGS_ENABLED": "true" - }) + env.update(SENTRY_ENVIRONMENT_VARIABLES) + env["SENTRY_AUTO_INIT"] = java_agent_auto_init # Build command jar_path = f"sentry-samples/{sample_module}/build/libs/{sample_module}-0.0.1-SNAPSHOT.jar" @@ -287,7 +326,7 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au try: # Start the Spring server with open("spring-server.txt", "w") as log_file: - self.spring_server_process = subprocess.Popen( + self.spring_server.process = subprocess.Popen( cmd, env=env, stdout=log_file, @@ -295,16 +334,35 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au ) # Store PID in instance variable and write to file - self.spring_server_pid = self.spring_server_process.pid - with open(self.spring_server_pid_file, "w") as pid_file: - pid_file.write(str(self.spring_server_pid)) - - print(f"Started Spring server with PID {self.spring_server_pid}") + store_pid(self.spring_server) + print(f"Started Spring server with PID {self.spring_server.pid}") except Exception as e: print(f"Failed to start Spring server: {e}") raise + def wait_for_tomcat(self, module_name: str, max_attempts: int = 20) -> bool: + """Wait for Tomcat to be ready.""" + print("Waiting for Tomcat to be ready...") + + for attempt in range(1, max_attempts + 1): + try: + response = requests.head( + f"http://localhost:8080/{module_name}-0.0.1-SNAPSHOT/person/1", + timeout=5 + ) + if response.status_code != 404: + print("Tomcat is ready!") + return True + except: + pass + + print(f"Waiting... (attempt {attempt}/{max_attempts})") + time.sleep(1) + + print("Tomcat failed to become ready") + return False + def wait_for_spring(self, max_attempts: int = 20) -> bool: """Wait for Spring Boot application to be ready.""" print("Waiting for Spring application to be ready...") @@ -332,11 +390,11 @@ def get_spring_status(self) -> dict: """Get status of Spring Boot application.""" status = { "process_running": False, - "pid": self.spring_server_pid, + "pid": self.spring_server.pid, "http_ready": False } - if self.spring_server_pid and self.is_process_running(self.spring_server_pid): + if self.spring_server.pid and self.is_process_running(self.spring_server.pid): status["process_running"] = True # Check HTTP endpoint @@ -357,11 +415,11 @@ def get_sentry_status(self) -> dict: """Get status of Sentry mock server.""" status = { "process_running": False, - "pid": self.mock_server_pid, + "pid": self.mock_server.pid, "http_ready": False } - if self.mock_server_pid and self.is_process_running(self.mock_server_pid): + if self.mock_server.pid and self.is_process_running(self.mock_server.pid): status["process_running"] = True # Check HTTP endpoint @@ -390,36 +448,60 @@ def print_status_summary(self) -> None: print(f" Process Running: {'✅' if spring_status['process_running'] else '❌'}") print(f" HTTP Ready: {'✅' if spring_status['http_ready'] else '❌'}") + def stop_tomcat_server(self) -> None: + """Stop the Tomcat server.""" + try: + # Kill the process - try process object first, then PID from file + if self.tomcat_server.process and self.tomcat_server.process.poll() is None: + print(f"Killing Tomcat server process object with PID {self.tomcat_server.process.pid}") + self.tomcat_server.process.kill() + try: + self.tomcat_server.process.wait(timeout=10) + except subprocess.TimeoutExpired: + print("Tomcat server did not terminate gracefully") + elif self.tomcat_server.pid and self.is_process_running(self.tomcat_server.pid): + print(f"Killing Tomcat server from PID file with PID {self.tomcat_server.pid}") + self.kill_process(self.tomcat_server.pid, "Tomcat server") + + except Exception as e: + print(f"Error stopping Tomcat server: {e}") + finally: + # Clean up PID file and instance variable + cleanup_pid(self.tomcat_server) + def stop_spring_server(self) -> None: """Stop the Spring Boot server.""" try: # Kill the process - try process object first, then PID from file - if self.spring_server_process and self.spring_server_process.poll() is None: - print(f"Killing Spring server process object with PID {self.spring_server_process.pid}") - self.spring_server_process.kill() + if self.spring_server.process and self.spring_server.process.poll() is None: + print(f"Killing Spring server process object with PID {self.spring_server.process.pid}") + self.spring_server.process.kill() try: - self.spring_server_process.wait(timeout=10) + self.spring_server.process.wait(timeout=10) except subprocess.TimeoutExpired: print("Spring server did not terminate gracefully") - elif self.spring_server_pid and self.is_process_running(self.spring_server_pid): - print(f"Killing Spring server from PID file with PID {self.spring_server_pid}") - self.kill_process(self.spring_server_pid, "Spring server") + elif self.spring_server.pid and self.is_process_running(self.spring_server.pid): + print(f"Killing Spring server from PID file with PID {self.spring_server.pid}") + self.kill_process(self.spring_server.pid, "Spring server") except Exception as e: print(f"Error stopping Spring server: {e}") finally: # Clean up PID file and instance variable - if os.path.exists(self.spring_server_pid_file): - os.remove(self.spring_server_pid_file) - self.spring_server_pid = None + cleanup_pid(self.spring_server) - def get_build_task(self, sample_module: str) -> str: + def get_build_task(self, server_type: Optional[ServerType]) -> str: """Get the appropriate build task for a module.""" - return "bootJar" if "spring" in sample_module else "assemble" + if server_type == ServerType.TOMCAT: + return "war" + elif server_type == ServerType.SPRING: + return "bootJar" - def build_module(self, sample_module: str) -> int: + return "assemble" + + def build_module(self, sample_module: str, server_type: Optional[ServerType]) -> int: """Build a sample module using the appropriate task.""" - build_task = self.get_build_task(sample_module) + build_task = self.get_build_task(server_type) print(f"Building {sample_module} using {build_task} task") return self.run_gradle_task(f":sentry-samples:{sample_module}:{build_task}") @@ -434,12 +516,13 @@ def run_gradle_task(self, task: str) -> int: return 1 def setup_test_infrastructure(self, sample_module: str, java_agent: str, - java_agent_auto_init: str, build_before_run: str) -> int: + java_agent_auto_init: str, build_before_run: str, + server_type: Optional[ServerType]) -> int: """Set up test infrastructure. Returns 0 on success, error code on failure.""" # Build if requested if build_before_run == "1": print("Building before test run") - build_result = self.build_module(sample_module) + build_result = self.build_module(sample_module, server_type) if build_result != 0: print("Build failed") return build_result @@ -455,8 +538,16 @@ def setup_test_infrastructure(self, sample_module: str, java_agent: str, print("Starting Sentry mock server...") self.start_sentry_mock_server() - # Start Spring server if it's a Spring module - if "spring" in sample_module: + # Start Tomcat server given a Spring module + if server_type == ServerType.TOMCAT: + print(f"Starting Tomcat server for {sample_module}...") + self.start_tomcat_server(sample_module) + if not self.wait_for_tomcat(sample_module): + print("Tomcat application failed to start!") + return 1 + print("Tomcat application is ready!") + # Start Spring server if it's a Spring boot module + elif server_type == ServerType.SPRING: print(f"Starting Spring server for {sample_module}...") self.start_spring_server(sample_module, java_agent, java_agent_auto_init) if not self.wait_for_spring(): @@ -471,9 +562,19 @@ def run_single_test(self, sample_module: str, java_agent: str, """Run a single system test.""" print(f"Running system test for {sample_module}") + server_type = None + if "spring" in sample_module and "spring-boot" not in sample_module: + server_type = ServerType.TOMCAT + elif "spring" in sample_module: + server_type = ServerType.SPRING + try: # Set up infrastructure - setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) + setup_result = self.setup_test_infrastructure(sample_module, + java_agent, + java_agent_auto_init, + build_before_run, + server_type) if setup_result != 0: return setup_result @@ -484,7 +585,9 @@ def run_single_test(self, sample_module: str, java_agent: str, finally: # Cleanup - if "spring" in sample_module: + if server_type == ServerType.TOMCAT: + self.stop_tomcat_server() + elif server_type == ServerType.SPRING: self.stop_spring_server() self.stop_sentry_mock_server() @@ -576,6 +679,7 @@ def run_manual_test_mode(self, sample_module: str, java_agent: str, def get_available_modules(self) -> List[ModuleConfig]: """Get list of all available test modules.""" return [ + ModuleConfig("sentry-samples-spring-jakarta", "false", "true", "false"), ModuleConfig("sentry-samples-spring-boot", "false", "true", "false"), ModuleConfig("sentry-samples-spring-boot-opentelemetry-noagent", "false", "true", "false"), ModuleConfig("sentry-samples-spring-boot-opentelemetry", "true", "true", "false"), @@ -817,6 +921,16 @@ def main(): test_parser.add_argument("--build", default="false", help="Build before running (true or false)") test_parser.add_argument("--manual-test", action="store_true", help="Set up infrastructure but pause for manual testing from IDE") + # Tomcat subcommand + tomcat_parser = subparsers.add_parser("tomcat", help="Manage Servlet applications deployed on Tomcat") + tomcat_subparsers = tomcat_parser.add_subparsers(dest="tomcat_action", help="Tomcat actions") + + tomcat_start_parser = tomcat_subparsers.add_parser("start", help="Start tomcat application") + tomcat_start_parser.add_argument("module", help="Sample module to start") + tomcat_start_parser.add_argument("--build", default="false", help="Build before starting (true or false)") + + tomcat_subparsers.add_parser("stop", help="Stop Spring Boot application") + # Spring subcommand spring_parser = subparsers.add_parser("spring", help="Manage Spring Boot applications") spring_subparsers = spring_parser.add_subparsers(dest="spring_action", help="Spring actions") @@ -879,6 +993,32 @@ def main(): elif args.interactive: return runner.run_interactive_tests(agent, auto_init, build) + elif args.command == "tomcat": + if args.tomcat_action == "start": + # Convert true/false arguments to internal format + build = str_to_bool(args.build) + + runner.start_tomcat_server(args.module) + + # Build if requested + if build == "1": + print("Building before starting Tomcat application") + build_result = runner.build_module(args.module, ServerType.TOMCAT) + if build_result != 0: + print("Build failed") + return build_result + + if runner.wait_for_tomcat(args.module): + print("Tomcat application started successfully!") + return 0 + else: + print("Tomcat application failed to start!") + return 1 + elif args.tomcat_action == "stop": + runner.stop_tomcat_server() + print("Tomcat application stopped.") + return 0 + elif args.command == "spring": if args.spring_action == "start": # Convert true/false arguments to internal format @@ -889,7 +1029,7 @@ def main(): # Build if requested if build == "1": print("Building before starting Spring application") - build_result = runner.build_module(args.module) + build_result = runner.build_module(args.module, ServerType.SPRING) if build_result != 0: print("Build failed") return build_result From 90ab6daa5091413903e53b9079e9504d544e2d29 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 13 Aug 2025 09:30:34 +0200 Subject: [PATCH 02/10] Create webapps directory and revert library changes only needed for the spring sample --- gradle/libs.versions.toml | 4 ++-- .../sentry-samples-spring-jakarta/build.gradle.kts | 1 + .../java/io/sentry/samples/spring/jakarta/Main.java | 12 ++++++++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 690fa002436..827f3a10fa7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -121,7 +121,7 @@ reactor-core = { module = "io.projectreactor:reactor-core", version = "3.5.3" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } sentry-native-ndk = { module = "io.sentry:sentry-native-ndk", version = "0.8.4" } -servlet-api = { module = "javax.servlet:javax.servlet-api", version = "4.0.1" } +servlet-api = { module = "javax.servlet:javax.servlet-api", version = "3.1.0" } servlet-jakarta-api = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.1.0" } slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-jdk14 = { module = "org.slf4j:slf4j-jdk14", version.ref = "slf4j" } @@ -153,8 +153,8 @@ springboot3-starter-actuator = { module = "org.springframework.boot:spring-boot- timber = { module = "com.jakewharton.timber:timber", version = "4.7.1" } # tomcat libraries -tomcat-catalina = { module = "org.apache.tomcat:tomcat-catalina", version = "9.0.108" } tomcat-catalina-jakarta = { module = "org.apache.tomcat:tomcat-catalina", version = "11.0.10" } +tomcat-embed-jasper-jakarta = { module = "org.apache.tomcat.embed:tomcat-embed-jasper", version = "11.0.10" } # test libraries androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version = "1.6.8" } diff --git a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts index 03290c7840a..3c9e2a85f40 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(libs.slf4j2.api) implementation(libs.tomcat.catalina.jakarta) + implementation(libs.tomcat.embed.jasper.jakarta) testImplementation(projects.sentrySystemTestSupport) testImplementation(libs.kotlin.test.junit) diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java index 38d0aeee5ef..268a6265e04 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java @@ -9,9 +9,17 @@ public class Main { public static void main(String[] args) throws LifecycleException, IOException { - String webappPath = "./build/libs"; + File webappsDirectory = new File("./tomcat.8080/webapps"); + if (!webappsDirectory.exists()) { + boolean didCreateDirectories = webappsDirectory.mkdirs(); + if (!didCreateDirectories) { + throw new RuntimeException("Failed to create directory required by Tomcat: " + webappsDirectory.getAbsolutePath()); + } + } + + String pathToWar = "./build/libs"; String warName = "sentry-samples-spring-jakarta-0.0.1-SNAPSHOT"; - File war = new File(webappPath + "/" + warName + ".war"); + File war = new File(pathToWar + "/" + warName + ".war"); Tomcat tomcat = new Tomcat(); tomcat.setPort(8080); From b6fe8bc0077b5cadd6a613df2851c9d0998ad82b Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 13 Aug 2025 09:40:45 +0200 Subject: [PATCH 03/10] Adapt test instead of endpoint in spring-jakarta sample --- .../sentry/samples/spring/jakarta/web/PersonController.java | 6 +++++- .../test/kotlin/io/sentry/systemtest/PersonSystemTest.kt | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java index 236a3e83d97..dab805281e1 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java @@ -27,7 +27,11 @@ Person person(@PathVariable("id") Long id) { Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); LOGGER.info("Loading person with id={}", id); - throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); + if (id > 10L) { + throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); + } else { + return personService.find(id); + } } @PostMapping diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 2e113203fd9..80d21b1d934 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -17,7 +17,7 @@ class PersonSystemTest { @Test fun `get person fails`() { val restClient = testHelper.restClient - restClient.getPerson(1L) + restClient.getPerson(11L) assertEquals(500, restClient.lastKnownStatusCode) testHelper.ensureTransactionReceived { transaction, envelopeHeader -> From dcc9cc8ae6c258d78e6456d9cadd8f472b02b22f Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 13 Aug 2025 08:00:26 +0000 Subject: [PATCH 04/10] Format code --- .../src/main/java/io/sentry/samples/spring/jakarta/Main.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java index 268a6265e04..0d67ee55c79 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/Main.java @@ -2,7 +2,6 @@ import java.io.File; import java.io.IOException; - import org.apache.catalina.LifecycleException; import org.apache.catalina.startup.Tomcat; @@ -13,7 +12,8 @@ public static void main(String[] args) throws LifecycleException, IOException { if (!webappsDirectory.exists()) { boolean didCreateDirectories = webappsDirectory.mkdirs(); if (!didCreateDirectories) { - throw new RuntimeException("Failed to create directory required by Tomcat: " + webappsDirectory.getAbsolutePath()); + throw new RuntimeException( + "Failed to create directory required by Tomcat: " + webappsDirectory.getAbsolutePath()); } } From e1edcf2107acd0bae0c13b3b532879890f1c77d0 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 13 Aug 2025 10:49:23 +0200 Subject: [PATCH 05/10] Add task dependency from run on war in spring-jakarta sample --- .../sentry-samples-spring-jakarta/build.gradle.kts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts index 3c9e2a85f40..df7ffed567e 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts @@ -14,6 +14,11 @@ plugins { application { mainClass.set("io.sentry.samples.spring.jakarta.Main") } +// Ensure WAR is up to date before run task +tasks.named("run") { + dependsOn(tasks.named("war")) +} + group = "io.sentry.sample.spring-jakarta" version = "0.0.1-SNAPSHOT" From a90c5afc50f454e21bb30c1c2f22543453f33b0c Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Wed, 13 Aug 2025 08:52:55 +0000 Subject: [PATCH 06/10] Format code --- sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts index df7ffed567e..60f36b98515 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts @@ -15,9 +15,7 @@ plugins { application { mainClass.set("io.sentry.samples.spring.jakarta.Main") } // Ensure WAR is up to date before run task -tasks.named("run") { - dependsOn(tasks.named("war")) -} +tasks.named("run") { dependsOn(tasks.named("war")) } group = "io.sentry.sample.spring-jakarta" From 9a6212364f22ff495ad6fcc5487dd7a9f0a2a280 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 13 Aug 2025 12:31:12 +0200 Subject: [PATCH 07/10] Add missing argument in interactive mode and build before launching in the system-test-runner --- test/system-test-runner.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/test/system-test-runner.py b/test/system-test-runner.py index 3013726de4e..c379c873c7e 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -65,6 +65,14 @@ class ServerType(Enum): TOMCAT = 0 SPRING = 1 +def get_server_type_for_module(sample_module: str) -> Optional[ServerType]: + if "spring" in sample_module and "spring-boot" not in sample_module: + return ServerType.TOMCAT + elif "spring" in sample_module: + return ServerType.SPRING + + return None + def str_to_bool(value: str) -> str: """Convert true/false string to 1/0 string for internal compatibility.""" if value.lower() in ('true', '1'): @@ -562,11 +570,7 @@ def run_single_test(self, sample_module: str, java_agent: str, """Run a single system test.""" print(f"Running system test for {sample_module}") - server_type = None - if "spring" in sample_module and "spring-boot" not in sample_module: - server_type = ServerType.TOMCAT - elif "spring" in sample_module: - server_type = ServerType.SPRING + server_type = get_server_type_for_module(sample_module) try: # Set up infrastructure @@ -639,9 +643,10 @@ def run_manual_test_mode(self, sample_module: str, java_agent: str, """Set up infrastructure for manual testing from IDE.""" print(f"Setting up manual test environment for {sample_module}") + server_type = get_server_type_for_module(sample_module) try: # Set up infrastructure - setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) + setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run, server_type) if setup_result != 0: return setup_result @@ -658,7 +663,9 @@ def run_manual_test_mode(self, sample_module: str, java_agent: str, print(f" - Agent Auto-init: {java_agent_auto_init}") print(f" - Mock DSN: http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0") - if "spring" in sample_module: + if server_type == ServerType.TOMCAT: + print(f"\nTomcat app is running on: http://localhost:8080/{sample_module}-0.0.1-SNAPSHOT") + elif server_type == ServerType.SPRING: print("\nSpring Boot app is running on: http://localhost:8080") print("\nPress Enter to stop the infrastructure and exit...") @@ -998,8 +1005,6 @@ def main(): # Convert true/false arguments to internal format build = str_to_bool(args.build) - runner.start_tomcat_server(args.module) - # Build if requested if build == "1": print("Building before starting Tomcat application") @@ -1008,6 +1013,8 @@ def main(): print("Build failed") return build_result + runner.start_tomcat_server(args.module) + if runner.wait_for_tomcat(args.module): print("Tomcat application started successfully!") return 0 From 65fea057657b0c423f34caf97bec33ebd9d50d52 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 13 Aug 2025 12:35:54 +0200 Subject: [PATCH 08/10] Add spring-jakarta sample to CI system tests config file --- .github/workflows/system-tests-backend.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index e6bf48a139f..671132f77fc 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -54,6 +54,9 @@ jobs: - sample: "sentry-samples-console" agent: "false" agent-auto-init: "true" + - sample: "sentry-samples-spring-jakarta" + agent: "false" + agent-auto-init: "true" steps: - uses: actions/checkout@v4 with: From 771345849b0165e366ab79b6c26a6ad1670a1a25 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 13 Aug 2025 12:46:54 +0200 Subject: [PATCH 09/10] Stop tomcat server on signal in system-test-runner --- test/system-test-runner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/system-test-runner.py b/test/system-test-runner.py index c379c873c7e..b2e27ebaa62 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -911,6 +911,7 @@ def cleanup_on_exit(self, signum, frame): print(f"\nReceived signal {signum}, cleaning up...") self.stop_spring_server() self.stop_sentry_mock_server() + self.stop_tomcat_server() sys.exit(1) def main(): From aa79e4353c7a8e87087df6ceeec292eb8cb7f301 Mon Sep 17 00:00:00 2001 From: Alexander Alderman Webb Date: Wed, 13 Aug 2025 12:58:31 +0200 Subject: [PATCH 10/10] Stop Tomcat process in all cases --- test/system-test-runner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/system-test-runner.py b/test/system-test-runner.py index a62641bf48f..f2aab974df3 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -1138,6 +1138,7 @@ def main(): if args.command == "test": runner.stop_spring_server() runner.stop_sentry_mock_server() + runner.stop_tomcat_server() if __name__ == "__main__": sys.exit(main())