From 1ce96082813e1b82d64c7b04bae168d41b9e7589 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 10 Jul 2025 10:09:22 +0200 Subject: [PATCH 01/19] e2e tests for console app --- .../sentry-samples-console/build.gradle.kts | 80 +++++++++++++++- .../java/io/sentry/samples/console/Main.java | 25 +++-- .../src/test/kotlin/io/sentry/DummyTest.kt | 12 +++ .../ConsoleApplicationSystemTest.kt | 92 +++++++++++++++++++ .../io/sentry/systemtest/util/TestHelper.kt | 35 ++++++- test/system-test-run-all.sh | 1 + test/system-test-run.sh | 10 +- 7 files changed, 232 insertions(+), 23 deletions(-) create mode 100644 sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/DummyTest.kt create mode 100644 sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt diff --git a/sentry-samples/sentry-samples-console/build.gradle.kts b/sentry-samples/sentry-samples-console/build.gradle.kts index 82a2bf61220..f259de9d965 100644 --- a/sentry-samples/sentry-samples-console/build.gradle.kts +++ b/sentry-samples/sentry-samples-console/build.gradle.kts @@ -1,14 +1,88 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { java application + kotlin("jvm") alias(libs.plugins.gradle.versions) + id("com.github.johnrengelman.shadow") version "8.1.1" } application { mainClass.set("io.sentry.samples.console.Main") } +java.sourceCompatibility = JavaVersion.VERSION_17 + +java.targetCompatibility = JavaVersion.VERSION_17 + +repositories { mavenCentral() } + configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() +} + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = JavaVersion.VERSION_17.toString() + } +} + +dependencies { + implementation(projects.sentry) + + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(projects.sentry) + testImplementation(projects.sentrySystemTestSupport) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.slf4j.api) + testImplementation(libs.slf4j.jdk14) +} + +// Configure the Shadow JAR (executable JAR with all dependencies) +tasks.shadowJar { + manifest { + attributes["Main-Class"] = "io.sentry.samples.console.Main" + } + archiveClassifier.set("") // Remove the classifier so it replaces the regular JAR + mergeServiceFiles() +} + +// Make the regular jar task depend on shadowJar +tasks.jar { + enabled = false + dependsOn(tasks.shadowJar) +} + +// Fix the startScripts task dependency +tasks.startScripts { + dependsOn(tasks.shadowJar) +} + +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.*") } } -dependencies { implementation(projects.sentry) } diff --git a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java index a0c0d5dd9eb..71c0144955e 100644 --- a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java @@ -20,6 +20,7 @@ public static void main(String[] args) throws InterruptedException { options -> { // NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in // your Sentry project/dashboard + options.setEnableExternalConfiguration(true); options.setDsn( "https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563"); @@ -66,10 +67,8 @@ public static void main(String[] args) throws InterruptedException { options.setDebug(true); // To change the verbosity, use: // By default it's DEBUG. - options.setDiagnosticLevel( - SentryLevel - .ERROR); // A good option to have SDK debug log in prod is to use only level - // ERROR here. + // options.setDiagnosticLevel(SentryLevel.ERROR); + // A good option to have SDK debug log in prod is to use only level ERROR here. // Exclude frames from some packages from being "inApp" so are hidden by default in Sentry // UI: @@ -83,15 +82,15 @@ public static void main(String[] args) throws InterruptedException { options.setTracesSampleRate(1.0); // set 0.5 to send 50% of traces // Determine traces sample rate based on the sampling context - options.setTracesSampler( - context -> { - // only 10% of transactions with "/product" prefix will be collected - if (!context.getTransactionContext().getName().startsWith("/products")) { - return 0.1; - } else { - return 0.5; - } - }); +// options.setTracesSampler( +// context -> { +// // only 10% of transactions with "/product" prefix will be collected +// if (!context.getTransactionContext().getName().startsWith("/products")) { +// return 0.1; +// } else { +// return 0.5; +// } +// }); }); Sentry.addBreadcrumb( diff --git a/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/DummyTest.kt b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/DummyTest.kt new file mode 100644 index 00000000000..6f762b7e453 --- /dev/null +++ b/sentry-samples/sentry-samples-console/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-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt new file mode 100644 index 00000000000..b709e4f506e --- /dev/null +++ b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -0,0 +1,92 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import java.io.File +import java.util.concurrent.TimeUnit +import org.junit.Test +import org.junit.Before +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue + +class ConsoleApplicationSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8000") + testHelper.reset() + } + + @Test + fun `console application sends expected events when run as JAR`() { + val jarFile = testHelper.findJar("sentry-samples-console") + val process = testHelper.launch( + jarFile, + mapOf( + "SENTRY_DSN" to testHelper.dsn, + "SENTRY_TRACES_SAMPLE_RATE" to "1.0", + "SENTRY_ENABLE_PRETTY_SERIALIZATION_OUTPUT" to "false", + "SENTRY_DEBUG" to "true", + )) + + process.waitFor(30, TimeUnit.SECONDS); + assertEquals(0, process.exitValue()) + + // Verify that we received the expected events + verifyExpectedEvents() + } + + private fun verifyExpectedEvents() { + // Verify we received a "Fatal message!" event + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Fatal message!" && event.level?.name == "FATAL" + } + + // Verify we received a "Some warning!" event + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Some warning!" && event.level?.name == "WARNING" + } + + // Verify we received the RuntimeException + testHelper.ensureErrorReceived { event -> + event.exceptions?.any { ex -> ex.type == "RuntimeException" && ex.value == "Some error!" } == + true + } + + // Verify we received the detailed event with fingerprint + testHelper.ensureErrorReceived { event -> + event.message?.message == "Detailed event" && + event.fingerprints?.contains("NewClientDebug") == true && + event.level?.name == "DEBUG" + } + + // Verify we received transaction events + testHelper.ensureTransactionReceived { transaction, _ -> + transaction.transaction == "transaction name" && + transaction.spans?.any { span -> span.op == "child" } == true + } + + // Verify we received the loop messages (should be 10 of them) + var loopMessageCount = 0 + try { + for (i in 0..9) { + testHelper.ensureErrorReceived { event -> + val matches = event.message?.message?.contains("items we'll wait to flush to Sentry!") == true + if (matches) loopMessageCount++ + matches + } + } + } catch (e: Exception) { + // Some loop messages might be missing, but we should have at least some + } + + assertTrue("Should receive at least 5 loop messages, got $loopMessageCount", loopMessageCount >= 5) + + // Verify we have breadcrumbs + testHelper.ensureErrorReceived { event -> + event.breadcrumbs?.any { breadcrumb -> + breadcrumb.message?.contains("Processed by") == true + } == true + } + } +} diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt index db797646146..a34c5d600ab 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt @@ -11,7 +11,9 @@ import io.sentry.SentryOptions import io.sentry.protocol.SentrySpan import io.sentry.protocol.SentryTransaction import io.sentry.systemtest.graphql.GraphqlTestClient +import java.io.File import java.io.PrintWriter +import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull @@ -22,6 +24,7 @@ class TestHelper(backendUrl: String) { val graphqlClient: GraphqlTestClient val sentryClient: SentryMockServerClient val jsonSerializer: JsonSerializer + val dsn = "http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0" var envelopeCounts: EnvelopeCounts? = null @@ -42,8 +45,7 @@ class TestHelper(backendUrl: String) { assertTrue(envelopeCountsAfter!!.envelopes!! > envelopeCounts!!.envelopes!!) } - fun ensureEnvelopeReceived(callback: ((String) -> Boolean)) { - Thread.sleep(10000) + fun ensureEnvelopeReceived(retryCount: Int = 1, callback: ((String) -> Boolean)) { val envelopes = sentryClient.getEnvelopes() assertNotNull(envelopes.envelopes) envelopes.envelopes.forEach { envelopeString -> @@ -52,7 +54,11 @@ class TestHelper(backendUrl: String) { return } } - throw RuntimeException("Unable to find matching envelope received by relay") + if (retryCount <= 0) { + throw RuntimeException("Unable to find matching envelope received by relay") + } else { + ensureEnvelopeReceived(retryCount - 1, callback) + } } fun ensureNoEnvelopeReceived(callback: ((String) -> Boolean)) { @@ -268,4 +274,27 @@ class TestHelper(backendUrl: String) { return true } + + fun findJar(prefix: String, inDir: String = "build/libs"): File { + val buildDir = File(inDir) + val jarFiles = + buildDir + .listFiles { _, name -> name.startsWith(prefix) && name.endsWith(".jar") } + ?.toList() ?: emptyList() + + if (jarFiles.isEmpty()) { + throw AssertionError("No JAR found in ${buildDir.absolutePath}") + } + + return jarFiles.maxOf { it } + } + + fun launch(jar: File, env: Map): Process { + val processBuilder = ProcessBuilder("java", "-jar", jar.absolutePath) + .inheritIO() // forward i/o to current process + + processBuilder.environment().putAll(env) + + return processBuilder.start() + } } diff --git a/test/system-test-run-all.sh b/test/system-test-run-all.sh index 8389a4db431..007def46242 100755 --- a/test/system-test-run-all.sh +++ b/test/system-test-run-all.sh @@ -10,3 +10,4 @@ ./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" "0" "true" "0" ./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "true" "0" ./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "false" "0" +./test/system-test-run.sh "sentry-samples-console" "0" "true" "0" diff --git a/test/system-test-run.sh b/test/system-test-run.sh index 9560beb3639..c4838923c6a 100755 --- a/test/system-test-run.sh +++ b/test/system-test-run.sh @@ -14,11 +14,13 @@ test/system-test-sentry-server-start.sh MOCK_SERVER_PID=$(cat sentry-mock-server.pid) echo "started mock server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${MOCK_SERVER_PID}" -test/system-test-spring-server-start.sh "${SAMPLE_MODULE}" "${JAVA_AGENT}" "${JAVA_AGENT_AUTO_INIT}" -SUT_PID=$(cat spring-server.pid) -echo "started spring server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${SUT_PID}" +if [[ $SAMPLE_MODULE == *"spring"* ]]; then + test/system-test-spring-server-start.sh "${SAMPLE_MODULE}" "${JAVA_AGENT}" "${JAVA_AGENT_AUTO_INIT}" + SUT_PID=$(cat spring-server.pid) + echo "started spring server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${SUT_PID}" -test/wait-for-spring.sh + test/wait-for-spring.sh +fi ./gradlew :sentry-samples:${SAMPLE_MODULE}:systemTest TESTRUN_RETVAL=$? From a863ac5ed1425318544d6015a4fef4e44bb3d3f6 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 07:45:07 +0200 Subject: [PATCH 02/19] fix test failures by waiting for 10s after first try to find envelopes --- .../src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt index a34c5d600ab..98f358da2ed 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt @@ -57,6 +57,7 @@ class TestHelper(backendUrl: String) { if (retryCount <= 0) { throw RuntimeException("Unable to find matching envelope received by relay") } else { + Thread.sleep(10000) ensureEnvelopeReceived(retryCount - 1, callback) } } From 811c9da1ece3dc0661f4cdd0ae60f7b8e84edf30 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 07:45:45 +0200 Subject: [PATCH 03/19] add system-test-runner.py script to replace bash scripts for running e2e / system tests --- test/system-test-runner.py | 838 +++++++++++++++++++++++++++++++++++++ 1 file changed, 838 insertions(+) create mode 100644 test/system-test-runner.py diff --git a/test/system-test-runner.py b/test/system-test-runner.py new file mode 100644 index 00000000000..92cfc0d302b --- /dev/null +++ b/test/system-test-runner.py @@ -0,0 +1,838 @@ +#!/usr/bin/env python3 + +""" +System Test Runner for Sentry Java + +Usage examples: + # Run all tests + python3 test/system-test-runner.py test --all + + # Run specific module test + python3 test/system-test-runner.py test --module sentry-samples-console + + # Set up infrastructure for manual testing from IDE + python3 test/system-test-runner.py test --module sentry-samples-console --manual-test + + # Start Sentry mock server + python3 test/system-test-runner.py sentry start + + # Start Spring Boot app + python3 test/system-test-runner.py spring start sentry-samples-spring-boot + + # Start Spring Boot app with build + python3 test/system-test-runner.py spring start sentry-samples-spring-boot --build + + # Check status of all services + python3 test/system-test-runner.py status + + # Stop services + python3 test/system-test-runner.py sentry stop + python3 test/system-test-runner.py spring stop +""" + +import subprocess +import sys +import time +import signal +import os +import argparse +import requests +import threading +from pathlib import Path +from typing import Optional, List, Tuple +from dataclasses import dataclass + +@dataclass +class InteractiveSelection: + """Result of interactive module selection.""" + modules: List[Tuple[str, str, str, str]] + manual_test_mode: bool + + def is_empty(self) -> bool: + """Check if no modules were selected.""" + return len(self.modules) == 0 + + def is_single_module(self) -> bool: + """Check if exactly one module was selected.""" + return len(self.modules) == 1 + + def get_first_module(self) -> Tuple[str, str, str, str]: + """Get the first selected module (for manual test mode).""" + if self.is_empty(): + raise ValueError("No modules selected") + return self.modules[0] + +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" + + # 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}") + + def read_pid_file(self, pid_file: str) -> Optional[int]: + """Read PID from file if it exists.""" + try: + if os.path.exists(pid_file): + with open(pid_file, 'r') as f: + return int(f.read().strip()) + except (ValueError, IOError) as e: + print(f"Error reading PID file {pid_file}: {e}") + return None + + def is_process_running(self, pid: int) -> bool: + """Check if a process with given PID is still running.""" + try: + # Send signal 0 to check if process exists + os.kill(pid, 0) + return True + except (OSError, ProcessLookupError): + return False + + def kill_process(self, pid: int, name: str) -> None: + """Kill a process by PID.""" + try: + print(f"Killing existing {name} process with PID {pid}") + os.kill(pid, signal.SIGTERM) + time.sleep(2) # Give it time to terminate gracefully + + # Check if it's still running and force kill if necessary + if self.is_process_running(pid): + print(f"Process {pid} didn't terminate gracefully, force killing...") + os.kill(pid, signal.SIGKILL) + time.sleep(1) + except (OSError, ProcessLookupError): + print(f"Process {pid} was already dead") + + + + def start_sentry_mock_server(self) -> None: + """Start the Sentry mock server.""" + print("Starting Sentry mock server...") + try: + # Start the mock server in the background + with open("sentry-mock-server.txt", "w") as log_file: + 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}") + + # Wait a moment for the server to start + time.sleep(2) + + except Exception as e: + print(f"Failed to start mock server: {e}") + raise + + def stop_sentry_mock_server(self) -> None: + """Stop the Sentry mock server.""" + try: + # Try graceful shutdown first + try: + response = requests.get("http://127.0.0.1:8000/STOP", timeout=5) + print("Sent stop signal to mock server") + except: + 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") + + 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 + + def find_agent_jar(self) -> Optional[str]: + """Find the OpenTelemetry agent JAR file.""" + agent_dir = Path("sentry-opentelemetry/sentry-opentelemetry-agent/build/libs/") + if not agent_dir.exists(): + return None + + for jar_file in agent_dir.glob("*.jar"): + name = jar_file.name + if ("agent" in name and + "javadoc" not in name and + "sources" not in name and + "dontuse" not in name): + return str(jar_file) + return None + + 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" + }) + + # Build command + jar_path = f"sentry-samples/{sample_module}/build/libs/{sample_module}-0.0.1-SNAPSHOT.jar" + cmd = ["java"] + + if java_agent == "1": + agent_jar = self.find_agent_jar() + if agent_jar: + cmd.append(f"-javaagent:{agent_jar}") + print(f"Using Java Agent: {agent_jar}") + + cmd.extend(["-jar", jar_path]) + + try: + # Start the Spring server + with open("spring-server.txt", "w") as log_file: + self.spring_server_process = subprocess.Popen( + cmd, + env=env, + stdout=log_file, + stderr=subprocess.STDOUT + ) + + # 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}") + + except Exception as e: + print(f"Failed to start Spring server: {e}") + raise + + 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...") + + for attempt in range(1, max_attempts + 1): + try: + response = requests.head( + "http://localhost:8080/actuator/health", + auth=("user", "password"), + timeout=5 + ) + if response.status_code == 200: + print("Spring application is ready!") + return True + except: + pass + + print(f"Waiting... (attempt {attempt}/{max_attempts})") + time.sleep(1) + + print("Spring application failed to become ready") + return False + + def get_spring_status(self) -> dict: + """Get status of Spring Boot application.""" + status = { + "process_running": False, + "pid": self.spring_server_pid, + "http_ready": False + } + + if self.spring_server_pid and self.is_process_running(self.spring_server_pid): + status["process_running"] = True + + # Check HTTP endpoint + try: + response = requests.head( + "http://localhost:8080/actuator/health", + auth=("user", "password"), + timeout=2 + ) + if response.status_code == 200: + status["http_ready"] = True + except: + pass + + return status + + def get_sentry_status(self) -> dict: + """Get status of Sentry mock server.""" + status = { + "process_running": False, + "pid": self.mock_server_pid, + "http_ready": False + } + + if self.mock_server_pid and self.is_process_running(self.mock_server_pid): + status["process_running"] = True + + # Check HTTP endpoint + try: + response = requests.get("http://127.0.0.1:8000/envelope-count", timeout=2) + if response.status_code == 200: + status["http_ready"] = True + except: + pass + + return status + + def print_status_summary(self) -> None: + """Print status summary of all services.""" + print("=== Service Status ===") + + sentry_status = self.get_sentry_status() + print(f"Sentry Mock Server:") + print(f" PID: {sentry_status['pid'] or 'None'}") + print(f" Process Running: {'✅' if sentry_status['process_running'] else '❌'}") + print(f" HTTP Ready: {'✅' if sentry_status['http_ready'] else '❌'}") + + spring_status = self.get_spring_status() + print(f"Spring Boot App:") + print(f" PID: {spring_status['pid'] or 'None'}") + print(f" Process Running: {'✅' if spring_status['process_running'] else '❌'}") + print(f" HTTP Ready: {'✅' if spring_status['http_ready'] else '❌'}") + + 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() + try: + 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") + + 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 + + def get_build_task(self, sample_module: str) -> str: + """Get the appropriate build task for a module.""" + return "bootJar" if "spring" in sample_module else "assemble" + + def build_module(self, sample_module: str) -> int: + """Build a sample module using the appropriate task.""" + build_task = self.get_build_task(sample_module) + print(f"Building {sample_module} using {build_task} task") + return self.run_gradle_task(f":sentry-samples:{sample_module}:{build_task}") + + def run_gradle_task(self, task: str) -> int: + """Run a Gradle task and return the exit code.""" + print(f"Running: ./gradlew {task}") + try: + result = subprocess.run(["./gradlew", task], check=False) + return result.returncode + except Exception as e: + print(f"Failed to run Gradle task: {e}") + return 1 + + def setup_test_infrastructure(self, sample_module: str, java_agent: str, + java_agent_auto_init: str, build_before_run: str) -> 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) + if build_result != 0: + print("Build failed") + return build_result + + # Start mock server + print("Starting Sentry mock server...") + self.start_sentry_mock_server() + + # Start Spring server if it's a Spring module + if "spring" in sample_module: + 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(): + print("Spring application failed to start!") + return 1 + print("Spring application is ready!") + + return 0 + + def run_single_test(self, sample_module: str, java_agent: str, + java_agent_auto_init: str, build_before_run: str) -> int: + """Run a single system test.""" + print(f"Running system test for {sample_module}") + + try: + # Set up infrastructure + setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) + if setup_result != 0: + return setup_result + + # Run the system test + test_result = self.run_gradle_task(f":sentry-samples:{sample_module}:systemTest") + + return test_result + + finally: + # Cleanup + if "spring" in sample_module: + self.stop_spring_server() + self.stop_sentry_mock_server() + + def run_all_tests(self) -> int: + """Run all system tests.""" + test_configs = [ + ("sentry-samples-spring-boot", "0", "true", "0"), + ("sentry-samples-spring-boot-opentelemetry-noagent", "0", "true", "0"), + ("sentry-samples-spring-boot-opentelemetry", "1", "true", "0"), + ("sentry-samples-spring-boot-opentelemetry", "1", "false", "0"), + ("sentry-samples-spring-boot-webflux-jakarta", "0", "true", "0"), + ("sentry-samples-spring-boot-webflux", "0", "true", "0"), + ("sentry-samples-spring-boot-jakarta", "0", "true", "0"), + ("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "0", "true", "0"), + ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "true", "0"), + ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "false", "0"), + ("sentry-samples-console", "0", "true", "0"), + ] + + failed_tests = [] + + for sample_module, java_agent, java_agent_auto_init, build_before_run in test_configs: + print(f"\n{'='*60}") + print(f"Running test: {sample_module} (agent={java_agent}, auto_init={java_agent_auto_init})") + print(f"{'='*60}") + + result = self.run_single_test(sample_module, java_agent, java_agent_auto_init, build_before_run) + + if result != 0: + # Find the module number in the full list for interactive reference + module_number = self._find_module_number(sample_module, java_agent, java_agent_auto_init) + failed_tests.append((module_number, sample_module, java_agent, java_agent_auto_init)) + print(f"❌ Test failed: {sample_module}") + else: + print(f"✅ Test passed: {sample_module}") + + # Summary + print(f"\n{'='*60}") + print("TEST SUMMARY") + print(f"{'='*60}") + print(f"Total tests: {len(test_configs)}") + print(f"Passed: {len(test_configs) - len(failed_tests)}") + print(f"Failed: {len(failed_tests)}") + + if failed_tests: + print("\nFailed tests (for interactive mode, use these numbers):") + for module_number, sample_module, java_agent, java_agent_auto_init in failed_tests: + print(f" {module_number}. {sample_module} (agent={java_agent}, auto_init={java_agent_auto_init})") + return 1 + else: + print("\n🎉 All tests passed!") + return 0 + + def run_manual_test_mode(self, sample_module: str, java_agent: str, + java_agent_auto_init: str, build_before_run: str) -> int: + """Set up infrastructure for manual testing from IDE.""" + print(f"Setting up manual test environment for {sample_module}") + + try: + # Set up infrastructure + setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) + if setup_result != 0: + return setup_result + + # Show status and wait for user + print("\n" + "="*60) + print("🚀 Manual test environment ready 🚀") + print("="*60) + self.print_status_summary() + print(f"\nInfrastructure is ready for manual testing of: {sample_module}") + print("You can now run your system tests from your IDE.") + print("\nTest configuration:") + print(f" - Module: {sample_module}") + print(f" - Java Agent: {'Yes' if java_agent == '1' else 'No'}") + print(f" - Agent Auto-init: {java_agent_auto_init}") + print(f" - Mock DSN: http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0") + + if "spring" in sample_module: + print("\nSpring Boot app is running on: http://localhost:8080") + + print("\nPress Enter to stop the infrastructure and exit...") + + # Wait for user input + try: + input() + except KeyboardInterrupt: + print("\nReceived interrupt signal") + + print("\nStopping infrastructure...") + return 0 + + finally: + # Cleanup will happen in the finally block of main() + pass + + def get_available_modules(self) -> List[Tuple[str, str, str, str]]: + """Get list of all available test modules.""" + return [ + ("sentry-samples-spring-boot", "0", "true", "0"), + ("sentry-samples-spring-boot-opentelemetry-noagent", "0", "true", "0"), + ("sentry-samples-spring-boot-opentelemetry", "1", "true", "0"), + ("sentry-samples-spring-boot-opentelemetry", "1", "false", "0"), + ("sentry-samples-spring-boot-webflux-jakarta", "0", "true", "0"), + ("sentry-samples-spring-boot-webflux", "0", "true", "0"), + ("sentry-samples-spring-boot-jakarta", "0", "true", "0"), + ("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "0", "true", "0"), + ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "true", "0"), + ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "false", "0"), + ("sentry-samples-console", "0", "true", "0"), + ] + + def _find_module_number(self, module_name: str, agent: str, auto_init: str) -> int: + """Find the module number in the interactive list (1-based).""" + modules = self.get_available_modules() + for i, (mod_name, mod_agent, mod_auto_init, _) in enumerate(modules, 1): + if mod_name == module_name and mod_agent == agent and mod_auto_init == auto_init: + return i + return 0 # Should not happen, but return 0 if not found + + def parse_selection(self, user_input: str, max_index: int) -> List[int]: + """Parse user selection string into list of indices.""" + if user_input.strip() == "*": + return list(range(max_index)) + + indices = [] + parts = user_input.split(",") + + for part in parts: + part = part.strip() + if "-" in part: + # Handle range like "1-4" + try: + start, end = map(int, part.split("-")) + # Convert from 1-based to 0-based indexing + indices.extend(range(start - 1, end)) + except ValueError: + raise ValueError(f"Invalid range format: {part}") + else: + # Handle single number + try: + # Convert from 1-based to 0-based indexing + indices.append(int(part) - 1) + except ValueError: + raise ValueError(f"Invalid number: {part}") + + # Remove duplicates and sort + indices = sorted(set(indices)) + + # Validate indices + for idx in indices: + if idx < 0 or idx >= max_index: + raise ValueError(f"Index {idx + 1} is out of range (1-{max_index})") + + return indices + + def interactive_module_selection(self) -> InteractiveSelection: + """Display modules and get user selection.""" + modules = self.get_available_modules() + + print("\nAvailable test modules:") + print("=" * 80) + for i, (module, agent, auto_init, build) in enumerate(modules, 1): + agent_text = "with agent" if agent == "1" else "no agent" + auto_init_text = f"auto-init: {auto_init}" + print(f"{i:2d}. {module:<50} ({agent_text}, {auto_init_text})") + + print("\nSelection options:") + print(" * = all modules") + print(" Single: 1, 5, 8") + print(" Range: 1-4, 6-8") + print(" Combined: 1,2,4-5,8") + + selected_modules = [] + while True: + try: + user_input = input("\nEnter your selection: ").strip() + if not user_input: + print("Please enter a selection.") + continue + + selected_indices = self.parse_selection(user_input, len(modules)) + selected_modules = [modules[i] for i in selected_indices] + + # Show confirmation + print(f"\nSelected {len(selected_modules)} module(s):") + for i, (module, agent, auto_init, build) in enumerate(selected_modules, 1): + agent_text = "with agent" if agent == "1" else "no agent" + print(f" {i}. {module} ({agent_text}, auto-init: {auto_init})") + + confirm = input("\nProceed with these selections? [Y/n]: ").strip().lower() + if confirm in ('', 'y', 'yes'): + break + else: + print("Please make a new selection.") + + except ValueError as e: + print(f"Error: {e}") + print("Please try again.") + except KeyboardInterrupt: + print("\nOperation cancelled.") + return InteractiveSelection(modules=[], manual_test_mode=False) + + # Ask about test mode + while True: + try: + mode_input = input("\nRun tests automatically (n = only set up infrastucture for testing in IDE)? [Y/n]: ").strip().lower() + if not mode_input or mode_input in ('y', 'yes'): + return InteractiveSelection(modules=selected_modules, manual_test_mode=False) + elif mode_input in ('n', 'no'): + return InteractiveSelection(modules=selected_modules, manual_test_mode=True) + else: + print("Please enter 'y' or 'n'.") + except KeyboardInterrupt: + print("\nOperation cancelled.") + return InteractiveSelection(modules=[], manual_test_mode=False) + + def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: + """Run tests with interactive module selection.""" + selection = self.interactive_module_selection() + + if selection.is_empty(): + print("No modules selected. Exiting.") + return 0 + + # Handle manual test mode + if selection.manual_test_mode: + if not selection.is_single_module(): + print("Error: Manual test mode can only be used with a single module.") + print("Please select only one module for manual testing.") + return 1 + + sample_module, test_agent, test_auto_init, test_build = selection.get_first_module() + print(f"\nSetting up manual test environment for: {sample_module}") + return self.run_manual_test_mode(sample_module, test_agent, test_auto_init, test_build) + + # Handle automatic test running + failed_tests = [] + + for i, (sample_module, test_agent, test_auto_init, test_build) in enumerate(selection.modules, 1): + print(f"\n{'='*60}") + print(f"Running test {i}/{len(selection.modules)}: {sample_module}") + print(f"Agent: {test_agent}, Auto-init: {test_auto_init}") + print(f"{'='*60}") + + result = self.run_single_test(sample_module, test_agent, test_auto_init, test_build) + + if result != 0: + # Find the module number in the full list for interactive reference + module_number = self._find_module_number(sample_module, test_agent, test_auto_init) + failed_tests.append((module_number, sample_module, test_agent, test_auto_init)) + print(f"❌ Test failed: {sample_module}") + else: + print(f"✅ Test passed: {sample_module}") + + # Summary + print(f"\n{'='*60}") + print("TEST SUMMARY") + print(f"{'='*60}") + print(f"Total tests: {len(selection.modules)}") + print(f"Passed: {len(selection.modules) - len(failed_tests)}") + print(f"Failed: {len(failed_tests)}") + + if failed_tests: + print("\nFailed tests (for interactive mode, use these numbers):") + for module_number, sample_module, test_agent, test_auto_init in failed_tests: + print(f" {module_number}. {sample_module} (agent={test_agent}, auto_init={test_auto_init})") + return 1 + else: + print("\n🎉 All tests passed!") + return 0 + + def cleanup_on_exit(self, signum, frame): + """Cleanup handler for signals.""" + print(f"\nReceived signal {signum}, cleaning up...") + self.stop_spring_server() + self.stop_sentry_mock_server() + sys.exit(1) + +def main(): + parser = argparse.ArgumentParser(description="System Test Runner for Sentry Java") + subparsers = parser.add_subparsers(dest="command", help="Available commands") + + # Test subcommand + test_parser = subparsers.add_parser("test", help="Run system tests") + test_group = test_parser.add_mutually_exclusive_group(required=True) + test_group.add_argument("--all", action="store_true", help="Run all system tests") + test_group.add_argument("--module", help="Sample module to test") + test_group.add_argument("--interactive", "-i", action="store_true", help="Interactive module selection") + test_parser.add_argument("--agent", default="0", help="Use Java agent (0 or 1)") + test_parser.add_argument("--auto-init", default="true", help="Auto-init agent (true or false)") + test_parser.add_argument("--build", default="0", help="Build before running (0 or 1)") + test_parser.add_argument("--manual-test", action="store_true", help="Set up infrastructure but pause for manual testing from IDE") + + # 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") + + spring_start_parser = spring_subparsers.add_parser("start", help="Start Spring Boot application") + spring_start_parser.add_argument("module", help="Sample module to start") + spring_start_parser.add_argument("--agent", default="0", help="Use Java agent (0 or 1)") + spring_start_parser.add_argument("--auto-init", default="true", help="Auto-init agent (true or false)") + spring_start_parser.add_argument("--build", action="store_true", help="Build before starting") + + spring_stop_parser = spring_subparsers.add_parser("stop", help="Stop Spring Boot application") + + spring_wait_parser = spring_subparsers.add_parser("wait", help="Wait for Spring Boot application to be ready") + spring_wait_parser.add_argument("--timeout", type=int, default=20, help="Max attempts to wait (default: 20)") + + spring_status_parser = spring_subparsers.add_parser("status", help="Check Spring Boot application status") + + # Sentry subcommand + sentry_parser = subparsers.add_parser("sentry", help="Manage Sentry mock server") + sentry_subparsers = sentry_parser.add_subparsers(dest="sentry_action", help="Sentry actions") + + sentry_start_parser = sentry_subparsers.add_parser("start", help="Start Sentry mock server") + sentry_stop_parser = sentry_subparsers.add_parser("stop", help="Stop Sentry mock server") + sentry_status_parser = sentry_subparsers.add_parser("status", help="Check Sentry mock server status") + + # Status subcommand + status_parser = subparsers.add_parser("status", help="Show status of all services") + + args = parser.parse_args() + + if not args.command: + parser.print_help() + return 1 + + runner = SystemTestRunner() + + # Set up signal handlers for cleanup + signal.signal(signal.SIGINT, runner.cleanup_on_exit) + signal.signal(signal.SIGTERM, runner.cleanup_on_exit) + + try: + if args.command == "test": + if args.manual_test and args.module: + return runner.run_manual_test_mode(args.module, args.agent, args.auto_init, args.build) + elif args.manual_test and args.all: + print("Error: --manual-test requires a specific --module, cannot be used with --all") + return 1 + elif args.manual_test and args.interactive: + print("Error: --manual-test cannot be used with --interactive") + return 1 + elif args.all: + return runner.run_all_tests() + elif args.module: + return runner.run_single_test(args.module, args.agent, args.auto_init, args.build) + elif args.interactive: + return runner.run_interactive_tests(args.agent, args.auto_init, args.build) + + elif args.command == "spring": + if args.spring_action == "start": + # Build if requested + if args.build: + print("Building before starting Spring application") + build_result = runner.build_module(args.module) + if build_result != 0: + print("Build failed") + return build_result + + runner.start_spring_server(args.module, args.agent, args.auto_init) + if runner.wait_for_spring(): + print("Spring application started successfully!") + return 0 + else: + print("Spring application failed to start!") + return 1 + elif args.spring_action == "stop": + runner.stop_spring_server() + print("Spring application stopped.") + return 0 + elif args.spring_action == "wait": + if runner.wait_for_spring(max_attempts=args.timeout): + print("Spring application is ready!") + return 0 + else: + print("Spring application is not ready!") + return 1 + elif args.spring_action == "status": + status = runner.get_spring_status() + print(f"Spring Boot Application Status:") + print(f" PID: {status['pid'] or 'None'}") + print(f" Process Running: {'✅' if status['process_running'] else '❌'}") + print(f" HTTP Ready: {'✅' if status['http_ready'] else '❌'}") + return 0 if (status['process_running'] and status['http_ready']) else 1 + else: + spring_parser.print_help() + return 1 + + elif args.command == "sentry": + if args.sentry_action == "start": + runner.start_sentry_mock_server() + print("Sentry mock server started successfully!") + return 0 + elif args.sentry_action == "stop": + runner.stop_sentry_mock_server() + print("Sentry mock server stopped.") + return 0 + elif args.sentry_action == "status": + status = runner.get_sentry_status() + print(f"Sentry Mock Server Status:") + print(f" PID: {status['pid'] or 'None'}") + print(f" Process Running: {'✅' if status['process_running'] else '❌'}") + print(f" HTTP Ready: {'✅' if status['http_ready'] else '❌'}") + return 0 if (status['process_running'] and status['http_ready']) else 1 + else: + sentry_parser.print_help() + return 1 + + elif args.command == "status": + runner.print_status_summary() + return 0 + else: + parser.print_help() + return 1 + + except KeyboardInterrupt: + print("\nInterrupted by user") + return 1 + except Exception as e: + print(f"Error: {e}") + return 1 + finally: + # Only cleanup if running tests or manual test mode, not for individual start/stop commands + if args.command == "test": + runner.stop_spring_server() + runner.stop_sentry_mock_server() + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file From 16dcaee3176b320d45deeabb7f9e854f6a7a9b8a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 11:55:31 +0200 Subject: [PATCH 04/19] use py script for ci, cleanup, makefile --- .github/workflows/system-tests-backend.yml | 23 +-- Makefile | 10 +- test/system-test-runner.py | 185 ++++++++++++++++----- 3 files changed, 159 insertions(+), 59 deletions(-) diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index 3c949887c0b..dd4e9c4c90b 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -21,26 +21,27 @@ jobs: fail-fast: false matrix: sample: [ "sentry-samples-spring-boot-jakarta" ] - agent: [ "0" ] + agent: [ "false" ] agent-auto-init: [ "true" ] include: - sample: "sentry-samples-spring-boot" - sample: "sentry-samples-spring-boot-opentelemetry-noagent" - sample: "sentry-samples-spring-boot-opentelemetry" - agent: "1" + agent: "true" agent-auto-init: "true" - sample: "sentry-samples-spring-boot-opentelemetry" - agent: "1" + agent: "true" agent-auto-init: "false" - sample: "sentry-samples-spring-boot-webflux-jakarta" - sample: "sentry-samples-spring-boot-webflux" - sample: "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" - sample: "sentry-samples-spring-boot-jakarta-opentelemetry" - agent: "1" + agent: "true" agent-auto-init: "true" - sample: "sentry-samples-spring-boot-jakarta-opentelemetry" - agent: "1" + agent: "true" agent-auto-init: "false" + - sample: "sentry-samples-console" steps: - uses: actions/checkout@v4 with: @@ -90,17 +91,9 @@ jobs: -e '/.*"sentry-samples-android",/d' \ build.gradle.kts - - name: Build server jar + - name: Build and run system tests run: | - ./gradlew :sentry-samples:${{ matrix.sample }}:bootJar - - - name: Build agent jar - run: | - ./gradlew :sentry-opentelemetry:sentry-opentelemetry-agent:assemble - - - name: Start server and run integration test for sentry-cli commands - run: | - test/system-test-run.sh "${{ matrix.sample }}" "${{ matrix.agent }}" "${{ matrix.agent-auto-init }}" "0" + python3 test/system-test-runner.py test --module "${{ matrix.sample }}" --agent "${{ matrix.agent }}" --auto-init "${{ matrix.agent-auto-init }}" --build "true" - name: Upload test results if: always() diff --git a/Makefile b/Makefile index b16966ba64d..0a0f2bbf8c2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleBenchmarkTestRelease assembleUiTestRelease assembleUiTestCriticalRelease createCoverageReports runUiTestCritical check preMerge publish +.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleBenchmarkTestRelease assembleUiTestRelease assembleUiTestCriticalRelease createCoverageReports runUiTestCritical check preMerge publish systemtest systemtest-interactive all: stop clean javadocs compile createCoverageReports assembleBenchmarks: assembleBenchmarkTestRelease @@ -59,6 +59,14 @@ createCoverageReports: ./gradlew jacocoTestReport ./gradlew koverXmlReportRelease +# Run system tests for sample applications +systemtest: + python3 test/system-test-runner.py test --all + +# Run system tests with interactive module selection +systemtest-interactive: + python3 test/system-test-runner.py test --interactive + # Run tests and lint check: ./gradlew check diff --git a/test/system-test-runner.py b/test/system-test-runner.py index 92cfc0d302b..aed0d7126a2 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -42,11 +42,21 @@ from typing import Optional, List, Tuple from dataclasses import dataclass +def str_to_bool(value: str) -> str: + """Convert true/false string to 1/0 string for internal compatibility.""" + if value.lower() in ('true', '1'): + return "1" + elif value.lower() in ('false', '0'): + return "0" + else: + raise ValueError(f"Invalid boolean value: {value}. Use 'true' or 'false'") + @dataclass class InteractiveSelection: """Result of interactive module selection.""" modules: List[Tuple[str, str, str, str]] manual_test_mode: bool + build_agent: bool def is_empty(self) -> bool: """Check if no modules were selected.""" @@ -61,6 +71,10 @@ def get_first_module(self) -> Tuple[str, str, str, str]: if self.is_empty(): raise ValueError("No modules selected") return self.modules[0] + + def has_agent_modules(self) -> bool: + """Check if any selected modules use the Java agent.""" + return any(str_to_bool(agent) == "1" for _, agent, _, _ in self.modules) class SystemTestRunner: def __init__(self): @@ -184,6 +198,36 @@ def find_agent_jar(self) -> Optional[str]: return str(jar_file) return None + def build_agent_jar(self) -> int: + """Build the OpenTelemetry agent JAR file.""" + print("Building OpenTelemetry agent JAR...") + return self.run_gradle_task(":sentry-opentelemetry:sentry-opentelemetry-agent:assemble") + + def ensure_agent_jar(self, skip_build: bool = False) -> Optional[str]: + """Ensure the OpenTelemetry agent JAR exists, building it if necessary.""" + agent_jar = self.find_agent_jar() + if agent_jar: + return agent_jar + + if skip_build: + print("OpenTelemetry agent JAR not found and build was skipped") + return None + + # Agent JAR doesn't exist, try to build it + print("OpenTelemetry agent JAR not found, building it...") + build_result = self.build_agent_jar() + if build_result != 0: + print("Failed to build OpenTelemetry agent JAR") + return None + + # Try to find it again after building + agent_jar = self.find_agent_jar() + if not agent_jar: + print("OpenTelemetry agent JAR still not found after building") + return None + + return agent_jar + 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}...") @@ -205,10 +249,12 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au cmd = ["java"] if java_agent == "1": - agent_jar = self.find_agent_jar() + agent_jar = self.ensure_agent_jar() if agent_jar: cmd.append(f"-javaagent:{agent_jar}") print(f"Using Java Agent: {agent_jar}") + else: + print("Warning: Java agent was requested but could not be found or built") cmd.extend(["-jar", jar_path]) @@ -372,6 +418,13 @@ def setup_test_infrastructure(self, sample_module: str, java_agent: str, print("Build failed") return build_result + # Ensure agent JAR is available if needed + if java_agent == "1": + agent_jar = self.ensure_agent_jar() + if not agent_jar: + print("Error: Java agent was requested but could not be found or built") + return 1 + # Start mock server print("Starting Sentry mock server...") self.start_sentry_mock_server() @@ -411,28 +464,21 @@ def run_single_test(self, sample_module: str, java_agent: str, def run_all_tests(self) -> int: """Run all system tests.""" - test_configs = [ - ("sentry-samples-spring-boot", "0", "true", "0"), - ("sentry-samples-spring-boot-opentelemetry-noagent", "0", "true", "0"), - ("sentry-samples-spring-boot-opentelemetry", "1", "true", "0"), - ("sentry-samples-spring-boot-opentelemetry", "1", "false", "0"), - ("sentry-samples-spring-boot-webflux-jakarta", "0", "true", "0"), - ("sentry-samples-spring-boot-webflux", "0", "true", "0"), - ("sentry-samples-spring-boot-jakarta", "0", "true", "0"), - ("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "0", "true", "0"), - ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "true", "0"), - ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "false", "0"), - ("sentry-samples-console", "0", "true", "0"), - ] + test_configs = self.get_available_modules() failed_tests = [] for sample_module, java_agent, java_agent_auto_init, build_before_run in test_configs: + # Convert true/false to internal 1/0 format + agent = str_to_bool(java_agent) + auto_init = java_agent_auto_init # already in correct format + build = str_to_bool(build_before_run) + print(f"\n{'='*60}") print(f"Running test: {sample_module} (agent={java_agent}, auto_init={java_agent_auto_init})") print(f"{'='*60}") - result = self.run_single_test(sample_module, java_agent, java_agent_auto_init, build_before_run) + result = self.run_single_test(sample_module, agent, auto_init, build) if result != 0: # Find the module number in the full list for interactive reference @@ -504,17 +550,17 @@ def run_manual_test_mode(self, sample_module: str, java_agent: str, def get_available_modules(self) -> List[Tuple[str, str, str, str]]: """Get list of all available test modules.""" return [ - ("sentry-samples-spring-boot", "0", "true", "0"), - ("sentry-samples-spring-boot-opentelemetry-noagent", "0", "true", "0"), - ("sentry-samples-spring-boot-opentelemetry", "1", "true", "0"), - ("sentry-samples-spring-boot-opentelemetry", "1", "false", "0"), - ("sentry-samples-spring-boot-webflux-jakarta", "0", "true", "0"), - ("sentry-samples-spring-boot-webflux", "0", "true", "0"), - ("sentry-samples-spring-boot-jakarta", "0", "true", "0"), - ("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "0", "true", "0"), - ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "true", "0"), - ("sentry-samples-spring-boot-jakarta-opentelemetry", "1", "false", "0"), - ("sentry-samples-console", "0", "true", "0"), + ("sentry-samples-spring-boot", "false", "true", "false"), + ("sentry-samples-spring-boot-opentelemetry-noagent", "false", "true", "false"), + ("sentry-samples-spring-boot-opentelemetry", "true", "true", "false"), + ("sentry-samples-spring-boot-opentelemetry", "true", "false", "false"), + ("sentry-samples-spring-boot-webflux-jakarta", "false", "true", "false"), + ("sentry-samples-spring-boot-webflux", "false", "true", "false"), + ("sentry-samples-spring-boot-jakarta", "false", "true", "false"), + ("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "false", "true", "false"), + ("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "true", "false"), + ("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "false", "false"), + ("sentry-samples-console", "false", "true", "false"), ] def _find_module_number(self, module_name: str, agent: str, auto_init: str) -> int: @@ -568,7 +614,7 @@ def interactive_module_selection(self) -> InteractiveSelection: print("\nAvailable test modules:") print("=" * 80) for i, (module, agent, auto_init, build) in enumerate(modules, 1): - agent_text = "with agent" if agent == "1" else "no agent" + agent_text = "with agent" if str_to_bool(agent) == "1" else "no agent" auto_init_text = f"auto-init: {auto_init}" print(f"{i:2d}. {module:<50} ({agent_text}, {auto_init_text})") @@ -592,7 +638,7 @@ def interactive_module_selection(self) -> InteractiveSelection: # Show confirmation print(f"\nSelected {len(selected_modules)} module(s):") for i, (module, agent, auto_init, build) in enumerate(selected_modules, 1): - agent_text = "with agent" if agent == "1" else "no agent" + agent_text = "with agent" if str_to_bool(agent) == "1" else "no agent" print(f" {i}. {module} ({agent_text}, auto-init: {auto_init})") confirm = input("\nProceed with these selections? [Y/n]: ").strip().lower() @@ -606,21 +652,45 @@ def interactive_module_selection(self) -> InteractiveSelection: print("Please try again.") except KeyboardInterrupt: print("\nOperation cancelled.") - return InteractiveSelection(modules=[], manual_test_mode=False) + return InteractiveSelection(modules=[], manual_test_mode=False, build_agent=False) # Ask about test mode + manual_test_mode = False while True: try: mode_input = input("\nRun tests automatically (n = only set up infrastucture for testing in IDE)? [Y/n]: ").strip().lower() if not mode_input or mode_input in ('y', 'yes'): - return InteractiveSelection(modules=selected_modules, manual_test_mode=False) + manual_test_mode = False + break elif mode_input in ('n', 'no'): - return InteractiveSelection(modules=selected_modules, manual_test_mode=True) + manual_test_mode = True + break else: print("Please enter 'y' or 'n'.") except KeyboardInterrupt: print("\nOperation cancelled.") - return InteractiveSelection(modules=[], manual_test_mode=False) + return InteractiveSelection(modules=[], manual_test_mode=False, build_agent=False) + + # Ask about building agent if any modules use it + build_agent = False + has_agent_modules = any(str_to_bool(agent) == "1" for _, agent, _, _ in selected_modules) + if has_agent_modules: + while True: + try: + agent_input = input("\nBuild OpenTelemetry agent JAR (recommended to ensure latest version)? [Y/n]: ").strip().lower() + if not agent_input or agent_input in ('y', 'yes'): + build_agent = True + break + elif agent_input in ('n', 'no'): + build_agent = False + break + else: + print("Please enter 'y' or 'n'.") + except KeyboardInterrupt: + print("\nOperation cancelled.") + return InteractiveSelection(modules=[], manual_test_mode=False, build_agent=False) + + return InteractiveSelection(modules=selected_modules, manual_test_mode=manual_test_mode, build_agent=build_agent) def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: """Run tests with interactive module selection.""" @@ -630,6 +700,15 @@ def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: print("No modules selected. Exiting.") return 0 + # Build agent JAR if requested and modules use agent + if selection.build_agent and selection.has_agent_modules(): + print("\nBuilding OpenTelemetry agent JAR...") + build_result = self.build_agent_jar() + if build_result != 0: + print("Failed to build OpenTelemetry agent JAR") + return build_result + print("✅ OpenTelemetry agent JAR built successfully") + # Handle manual test mode if selection.manual_test_mode: if not selection.is_single_module(): @@ -638,19 +717,29 @@ def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: return 1 sample_module, test_agent, test_auto_init, test_build = selection.get_first_module() + # Convert true/false to internal 1/0 format + agent = str_to_bool(test_agent) + auto_init = test_auto_init # already in correct format + build = str_to_bool(test_build) + print(f"\nSetting up manual test environment for: {sample_module}") - return self.run_manual_test_mode(sample_module, test_agent, test_auto_init, test_build) + return self.run_manual_test_mode(sample_module, agent, auto_init, build) # Handle automatic test running failed_tests = [] for i, (sample_module, test_agent, test_auto_init, test_build) in enumerate(selection.modules, 1): + # Convert true/false to internal 1/0 format + agent = str_to_bool(test_agent) + auto_init = test_auto_init # already in correct format + build = str_to_bool(test_build) + print(f"\n{'='*60}") print(f"Running test {i}/{len(selection.modules)}: {sample_module}") print(f"Agent: {test_agent}, Auto-init: {test_auto_init}") print(f"{'='*60}") - result = self.run_single_test(sample_module, test_agent, test_auto_init, test_build) + result = self.run_single_test(sample_module, agent, auto_init, build) if result != 0: # Find the module number in the full list for interactive reference @@ -694,9 +783,9 @@ def main(): test_group.add_argument("--all", action="store_true", help="Run all system tests") test_group.add_argument("--module", help="Sample module to test") test_group.add_argument("--interactive", "-i", action="store_true", help="Interactive module selection") - test_parser.add_argument("--agent", default="0", help="Use Java agent (0 or 1)") + test_parser.add_argument("--agent", default="false", help="Use Java agent (true or false)") test_parser.add_argument("--auto-init", default="true", help="Auto-init agent (true or false)") - test_parser.add_argument("--build", default="0", help="Build before running (0 or 1)") + 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") # Spring subcommand @@ -705,9 +794,9 @@ def main(): spring_start_parser = spring_subparsers.add_parser("start", help="Start Spring Boot application") spring_start_parser.add_argument("module", help="Sample module to start") - spring_start_parser.add_argument("--agent", default="0", help="Use Java agent (0 or 1)") + spring_start_parser.add_argument("--agent", default="false", help="Use Java agent (true or false)") spring_start_parser.add_argument("--auto-init", default="true", help="Auto-init agent (true or false)") - spring_start_parser.add_argument("--build", action="store_true", help="Build before starting") + spring_start_parser.add_argument("--build", default="false", help="Build before starting (true or false)") spring_stop_parser = spring_subparsers.add_parser("stop", help="Stop Spring Boot application") @@ -741,8 +830,13 @@ def main(): try: if args.command == "test": + # Convert true/false arguments to internal 1/0 format + agent = str_to_bool(args.agent) + auto_init = args.auto_init # already accepts true/false + build = str_to_bool(args.build) + if args.manual_test and args.module: - return runner.run_manual_test_mode(args.module, args.agent, args.auto_init, args.build) + return runner.run_manual_test_mode(args.module, agent, auto_init, build) elif args.manual_test and args.all: print("Error: --manual-test requires a specific --module, cannot be used with --all") return 1 @@ -752,21 +846,26 @@ def main(): elif args.all: return runner.run_all_tests() elif args.module: - return runner.run_single_test(args.module, args.agent, args.auto_init, args.build) + return runner.run_single_test(args.module, agent, auto_init, build) elif args.interactive: - return runner.run_interactive_tests(args.agent, args.auto_init, args.build) + return runner.run_interactive_tests(agent, auto_init, build) elif args.command == "spring": if args.spring_action == "start": + # Convert true/false arguments to internal format + agent = str_to_bool(args.agent) + auto_init = args.auto_init # already accepts true/false + build = str_to_bool(args.build) + # Build if requested - if args.build: + if build == "1": print("Building before starting Spring application") build_result = runner.build_module(args.module) if build_result != 0: print("Build failed") return build_result - runner.start_spring_server(args.module, args.agent, args.auto_init) + runner.start_spring_server(args.module, agent, auto_init) if runner.wait_for_spring(): print("Spring application started successfully!") return 0 From 5d9f0107257d0269f8ef231fa569ae79529a691f Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Fri, 11 Jul 2025 10:02:55 +0000 Subject: [PATCH 05/19] Format code --- .../sentry-samples-console/build.gradle.kts | 9 ++--- .../java/io/sentry/samples/console/Main.java | 19 ++++++----- .../ConsoleApplicationSystemTest.kt | 33 +++++++++++-------- .../io/sentry/systemtest/util/TestHelper.kt | 10 +++--- 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/sentry-samples/sentry-samples-console/build.gradle.kts b/sentry-samples/sentry-samples-console/build.gradle.kts index f259de9d965..b9d754db204 100644 --- a/sentry-samples/sentry-samples-console/build.gradle.kts +++ b/sentry-samples/sentry-samples-console/build.gradle.kts @@ -45,9 +45,7 @@ dependencies { // Configure the Shadow JAR (executable JAR with all dependencies) tasks.shadowJar { - manifest { - attributes["Main-Class"] = "io.sentry.samples.console.Main" - } + manifest { attributes["Main-Class"] = "io.sentry.samples.console.Main" } archiveClassifier.set("") // Remove the classifier so it replaces the regular JAR mergeServiceFiles() } @@ -59,9 +57,7 @@ tasks.jar { } // Fix the startScripts task dependency -tasks.startScripts { - dependsOn(tasks.shadowJar) -} +tasks.startScripts { dependsOn(tasks.shadowJar) } configure { test { java.srcDir("src/test/java") } } @@ -85,4 +81,3 @@ tasks.named("test").configure { filter { excludeTestsMatching("io.sentry.systemtest.*") } } - diff --git a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java index 71c0144955e..7ee46649e97 100644 --- a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java @@ -82,15 +82,16 @@ public static void main(String[] args) throws InterruptedException { options.setTracesSampleRate(1.0); // set 0.5 to send 50% of traces // Determine traces sample rate based on the sampling context -// options.setTracesSampler( -// context -> { -// // only 10% of transactions with "/product" prefix will be collected -// if (!context.getTransactionContext().getName().startsWith("/products")) { -// return 0.1; -// } else { -// return 0.5; -// } -// }); + // options.setTracesSampler( + // context -> { + // // only 10% of transactions with "/product" prefix will be collected + // if (!context.getTransactionContext().getName().startsWith("/products")) + // { + // return 0.1; + // } else { + // return 0.5; + // } + // }); }); Sentry.addBreadcrumb( diff --git a/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt index b709e4f506e..18409c5b4e6 100644 --- a/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -1,12 +1,11 @@ package io.sentry.systemtest import io.sentry.systemtest.util.TestHelper -import java.io.File import java.util.concurrent.TimeUnit -import org.junit.Test -import org.junit.Before import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test class ConsoleApplicationSystemTest { lateinit var testHelper: TestHelper @@ -20,16 +19,18 @@ class ConsoleApplicationSystemTest { @Test fun `console application sends expected events when run as JAR`() { val jarFile = testHelper.findJar("sentry-samples-console") - val process = testHelper.launch( - jarFile, - mapOf( - "SENTRY_DSN" to testHelper.dsn, - "SENTRY_TRACES_SAMPLE_RATE" to "1.0", - "SENTRY_ENABLE_PRETTY_SERIALIZATION_OUTPUT" to "false", - "SENTRY_DEBUG" to "true", - )) + val process = + testHelper.launch( + jarFile, + mapOf( + "SENTRY_DSN" to testHelper.dsn, + "SENTRY_TRACES_SAMPLE_RATE" to "1.0", + "SENTRY_ENABLE_PRETTY_SERIALIZATION_OUTPUT" to "false", + "SENTRY_DEBUG" to "true", + ), + ) - process.waitFor(30, TimeUnit.SECONDS); + process.waitFor(30, TimeUnit.SECONDS) assertEquals(0, process.exitValue()) // Verify that we received the expected events @@ -71,7 +72,8 @@ class ConsoleApplicationSystemTest { try { for (i in 0..9) { testHelper.ensureErrorReceived { event -> - val matches = event.message?.message?.contains("items we'll wait to flush to Sentry!") == true + val matches = + event.message?.message?.contains("items we'll wait to flush to Sentry!") == true if (matches) loopMessageCount++ matches } @@ -80,7 +82,10 @@ class ConsoleApplicationSystemTest { // Some loop messages might be missing, but we should have at least some } - assertTrue("Should receive at least 5 loop messages, got $loopMessageCount", loopMessageCount >= 5) + assertTrue( + "Should receive at least 5 loop messages, got $loopMessageCount", + loopMessageCount >= 5, + ) // Verify we have breadcrumbs testHelper.ensureErrorReceived { event -> diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt index 98f358da2ed..56c699e995e 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt @@ -13,7 +13,6 @@ import io.sentry.protocol.SentryTransaction import io.sentry.systemtest.graphql.GraphqlTestClient import java.io.File import java.io.PrintWriter -import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull @@ -279,9 +278,8 @@ class TestHelper(backendUrl: String) { fun findJar(prefix: String, inDir: String = "build/libs"): File { val buildDir = File(inDir) val jarFiles = - buildDir - .listFiles { _, name -> name.startsWith(prefix) && name.endsWith(".jar") } - ?.toList() ?: emptyList() + buildDir.listFiles { _, name -> name.startsWith(prefix) && name.endsWith(".jar") }?.toList() + ?: emptyList() if (jarFiles.isEmpty()) { throw AssertionError("No JAR found in ${buildDir.absolutePath}") @@ -291,8 +289,8 @@ class TestHelper(backendUrl: String) { } fun launch(jar: File, env: Map): Process { - val processBuilder = ProcessBuilder("java", "-jar", jar.absolutePath) - .inheritIO() // forward i/o to current process + val processBuilder = + ProcessBuilder("java", "-jar", jar.absolutePath).inheritIO() // forward i/o to current process processBuilder.environment().putAll(env) From e531d9d5c4db3c769cb8637bb011d8ed8c5c253b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 13:01:24 +0200 Subject: [PATCH 06/19] remove bash scripts --- test/system-test-run-all.sh | 13 ---------- test/system-test-run.sh | 33 ------------------------- test/system-test-sentry-server-start.sh | 4 --- test/system-test-sentry-server-stop.sh | 3 --- test/system-test-spring-server-start.sh | 19 -------------- test/wait-for-spring.sh | 3 --- 6 files changed, 75 deletions(-) delete mode 100755 test/system-test-run-all.sh delete mode 100755 test/system-test-run.sh delete mode 100755 test/system-test-sentry-server-start.sh delete mode 100755 test/system-test-sentry-server-stop.sh delete mode 100755 test/system-test-spring-server-start.sh delete mode 100755 test/wait-for-spring.sh diff --git a/test/system-test-run-all.sh b/test/system-test-run-all.sh deleted file mode 100755 index 007def46242..00000000000 --- a/test/system-test-run-all.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -./test/system-test-run.sh "sentry-samples-spring-boot" "0" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry-noagent" "0" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry" "1" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-opentelemetry" "1" "false" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-webflux-jakarta" "0" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-webflux" "0" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-jakarta" "0" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" "0" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "true" "0" -./test/system-test-run.sh "sentry-samples-spring-boot-jakarta-opentelemetry" "1" "false" "0" -./test/system-test-run.sh "sentry-samples-console" "0" "true" "0" diff --git a/test/system-test-run.sh b/test/system-test-run.sh deleted file mode 100755 index c4838923c6a..00000000000 --- a/test/system-test-run.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env bash - -readonly SAMPLE_MODULE=$1 -readonly JAVA_AGENT=$2 -readonly JAVA_AGENT_AUTO_INIT=$3 -readonly BUILD_BEFORE_RUN=$4 - -if [[ "$BUILD_BEFORE_RUN" == "1" ]]; then - echo "Building before Test run" - ./gradlew :sentry-samples:${SAMPLE_MODULE}:assemble -fi - -test/system-test-sentry-server-start.sh -MOCK_SERVER_PID=$(cat sentry-mock-server.pid) -echo "started mock server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${MOCK_SERVER_PID}" - -if [[ $SAMPLE_MODULE == *"spring"* ]]; then - test/system-test-spring-server-start.sh "${SAMPLE_MODULE}" "${JAVA_AGENT}" "${JAVA_AGENT_AUTO_INIT}" - SUT_PID=$(cat spring-server.pid) - echo "started spring server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${SUT_PID}" - - test/wait-for-spring.sh -fi - -./gradlew :sentry-samples:${SAMPLE_MODULE}:systemTest -TESTRUN_RETVAL=$? - -echo "killing mock server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${MOCK_SERVER_PID}" -kill $SUT_PID -echo "killing spring server ${SAMPLE_MODULE}-${JAVA_AGENT}-${JAVA_AGENT_AUTO_INIT} with PID ${SUT_PID}" -kill $MOCK_SERVER_PID - -exit $TESTRUN_RETVAL diff --git a/test/system-test-sentry-server-start.sh b/test/system-test-sentry-server-start.sh deleted file mode 100755 index 181c77b8d79..00000000000 --- a/test/system-test-sentry-server-start.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env bash - -python3 test/system-test-sentry-server.py > sentry-mock-server.txt 2>&1 & -echo $! > sentry-mock-server.pid diff --git a/test/system-test-sentry-server-stop.sh b/test/system-test-sentry-server-stop.sh deleted file mode 100755 index a9168eddd27..00000000000 --- a/test/system-test-sentry-server-stop.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -curl http://127.0.0.1:8000/STOP diff --git a/test/system-test-spring-server-start.sh b/test/system-test-spring-server-start.sh deleted file mode 100755 index 7e8aef516f3..00000000000 --- a/test/system-test-spring-server-start.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -readonly SAMPLE_MODULE=$1 -readonly JAVA_AGENT=$2 -readonly JAVA_AGENT_AUTO_INIT=$3 - -JAVA_AGENT_STRING="" - -echo "$JAVA_AGENT" - -if [[ "$JAVA_AGENT" == "1" ]]; then - JAVA_AGENT_STRING="-javaagent:$(find ./sentry-opentelemetry/sentry-opentelemetry-agent/build/libs/ -not -name '*javadoc*' -name '*-agent-*' -not -name '*sources*' -not -name '*dontuse*' -type f)" - echo "Using Java Agent: ${JAVA_AGENT_STRING}" -fi - -echo "$JAVA_AGENT_STRING" - -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 java ${JAVA_AGENT_STRING} -jar sentry-samples/${SAMPLE_MODULE}/build/libs/${SAMPLE_MODULE}-0.0.1-SNAPSHOT.jar > spring-server.txt 2>&1 & -echo $! > spring-server.pid diff --git a/test/wait-for-spring.sh b/test/wait-for-spring.sh deleted file mode 100755 index 7f989f793e6..00000000000 --- a/test/wait-for-spring.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -for attempt in {1..20}; do sleep 1; if $(curl --output /dev/null --silent --head --fail http://user:password@localhost:8080/actuator/health); then echo ready; break; fi; echo waiting...; done From 9d019163a6d267d89fc45c19d55f25f5ce1c019d Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 13:28:01 +0200 Subject: [PATCH 07/19] install requests module --- .github/workflows/system-tests-backend.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index dd4e9c4c90b..e81c59082b7 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -51,6 +51,11 @@ jobs: with: python-version: '3.10.5' + - name: Install Python dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install requests + - name: Set up Java uses: actions/setup-java@v4 with: From 105e0d04d69e9a68a473c4b3ebf1d234ed63f4d2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 13:28:07 +0200 Subject: [PATCH 08/19] api --- .../api/sentry-system-test-support.api | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sentry-system-test-support/api/sentry-system-test-support.api b/sentry-system-test-support/api/sentry-system-test-support.api index 4f5bb9634a0..3e2653f717a 100644 --- a/sentry-system-test-support/api/sentry-system-test-support.api +++ b/sentry-system-test-support/api/sentry-system-test-support.api @@ -558,7 +558,8 @@ public final class io/sentry/systemtest/util/TestHelper { public final fun doesTransactionHaveOp (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;)Z public final fun doesTransactionHaveTraceId (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;)Z public final fun ensureEnvelopeCountIncreased ()V - public final fun ensureEnvelopeReceived (Lkotlin/jvm/functions/Function1;)V + public final fun ensureEnvelopeReceived (ILkotlin/jvm/functions/Function1;)V + public static synthetic fun ensureEnvelopeReceived$default (Lio/sentry/systemtest/util/TestHelper;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)V public final fun ensureErrorCount (Lcom/apollographql/apollo3/api/ApolloResponse;I)V public final fun ensureErrorReceived (Lkotlin/jvm/functions/Function1;)V public final fun ensureLogsReceived (Lkotlin/jvm/functions/Function2;)V @@ -567,11 +568,15 @@ public final class io/sentry/systemtest/util/TestHelper { public final fun ensureNoTransactionReceived (Lkotlin/jvm/functions/Function2;)V public final fun ensureTransactionReceived (Lkotlin/jvm/functions/Function2;)V public final fun ensureTransactionWithSpanReceived (Lkotlin/jvm/functions/Function1;)V + public final fun findJar (Ljava/lang/String;Ljava/lang/String;)Ljava/io/File; + public static synthetic fun findJar$default (Lio/sentry/systemtest/util/TestHelper;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Ljava/io/File; + public final fun getDsn ()Ljava/lang/String; public final fun getEnvelopeCounts ()Lio/sentry/systemtest/util/EnvelopeCounts; public final fun getGraphqlClient ()Lio/sentry/systemtest/graphql/GraphqlTestClient; public final fun getJsonSerializer ()Lio/sentry/JsonSerializer; public final fun getRestClient ()Lio/sentry/systemtest/util/RestTestClient; public final fun getSentryClient ()Lio/sentry/systemtest/util/SentryMockServerClient; + public final fun launch (Ljava/io/File;Ljava/util/Map;)Ljava/lang/Process; public final fun logObject (Ljava/lang/Object;)V public final fun reset ()V public final fun setEnvelopeCounts (Lio/sentry/systemtest/util/EnvelopeCounts;)V From 195937ca8cf3be1ab2e1562c720e0896729d2c1e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Jul 2025 14:35:54 +0200 Subject: [PATCH 09/19] fix gh script --- .github/workflows/system-tests-backend.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index e81c59082b7..eee6f7f90cc 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -25,7 +25,11 @@ jobs: agent-auto-init: [ "true" ] include: - sample: "sentry-samples-spring-boot" + agent: "false" + agent-auto-init: "true" - sample: "sentry-samples-spring-boot-opentelemetry-noagent" + agent: "false" + agent-auto-init: "true" - sample: "sentry-samples-spring-boot-opentelemetry" agent: "true" agent-auto-init: "true" @@ -33,8 +37,14 @@ jobs: agent: "true" agent-auto-init: "false" - sample: "sentry-samples-spring-boot-webflux-jakarta" + agent: "false" + agent-auto-init: "true" - sample: "sentry-samples-spring-boot-webflux" + agent: "false" + agent-auto-init: "true" - sample: "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" + agent: "false" + agent-auto-init: "true" - sample: "sentry-samples-spring-boot-jakarta-opentelemetry" agent: "true" agent-auto-init: "true" @@ -42,6 +52,8 @@ jobs: agent: "true" agent-auto-init: "false" - sample: "sentry-samples-console" + agent: "false" + agent-auto-init: "true" steps: - uses: actions/checkout@v4 with: From d5fd3b0c5ef09abaa381ef980e607e0634a4dcd4 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 21 Jul 2025 09:12:06 +0200 Subject: [PATCH 10/19] Implement E2E tests for OTel based console sample --- .../build.gradle.kts | 75 ++++- .../java/io/sentry/samples/console/Main.java | 94 +----- .../src/test/kotlin/sentry/DummyTest.kt | 12 + .../ConsoleApplicationSystemTest.kt | 102 +++++++ .../io/sentry/systemtest/util/TestHelper.kt | 14 +- test/system-test-runner.py | 289 +++++++++--------- 6 files changed, 357 insertions(+), 229 deletions(-) create mode 100644 sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/DummyTest.kt create mode 100644 sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts b/sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts index 945d906df8a..8821c25626b 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts @@ -1,14 +1,83 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { java application + kotlin("jvm") alias(libs.plugins.gradle.versions) + id("com.github.johnrengelman.shadow") version "8.1.1" } application { mainClass.set("io.sentry.samples.console.Main") } +java.sourceCompatibility = JavaVersion.VERSION_17 + +java.targetCompatibility = JavaVersion.VERSION_17 + +repositories { mavenCentral() } + configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() +} + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = JavaVersion.VERSION_17.toString() + } +} + +dependencies { + implementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentless) + + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(projects.sentry) + testImplementation(projects.sentrySystemTestSupport) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.slf4j.api) + testImplementation(libs.slf4j.jdk14) +} + +// Configure the Shadow JAR (executable JAR with all dependencies) +tasks.shadowJar { + manifest { attributes["Main-Class"] = "io.sentry.samples.console.Main" } + archiveClassifier.set("") // Remove the classifier so it replaces the regular JAR + mergeServiceFiles() } -dependencies { implementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentless) } +// Make the regular jar task depend on shadowJar +tasks.jar { + enabled = false + dependsOn(tasks.shadowJar) +} + +// Fix the startScripts task dependency +tasks.startScripts { dependsOn(tasks.shadowJar) } + +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-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java index c27aad737b1..d552968f315 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java @@ -4,6 +4,8 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.sentry.Breadcrumb; import io.sentry.EventProcessor; import io.sentry.Hint; @@ -17,90 +19,22 @@ import io.sentry.protocol.Message; import io.sentry.protocol.User; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; public class Main { public static void main(String[] args) throws InterruptedException { - Sentry.init( - options -> { - // NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in - // your Sentry project/dashboard - options.setDsn( - "https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563"); - - // All events get assigned to the release. See more at - // https://docs.sentry.io/workflow/releases/ - options.setRelease("io.sentry.samples.console@3.0.0+1"); - - // Modifications to event before it goes out. Could replace the event altogether - options.setBeforeSend( - (event, hint) -> { - // Drop an event altogether: - if (event.getTag("SomeTag") != null) { - return null; - } - return event; - }); - - options.setBeforeSendTransaction( - (transaction, hint) -> { - // Drop a transaction: - if (transaction.getTag("SomeTransactionTag") != null) { - return null; - } - - return transaction; - }); - - // Allows inspecting and modifying, returning a new or simply rejecting (returning null) - options.setBeforeBreadcrumb( - (breadcrumb, hint) -> { - // Don't add breadcrumbs with message containing: - if (breadcrumb.getMessage() != null - && breadcrumb.getMessage().contains("bad breadcrumb")) { - return null; - } - return breadcrumb; - }); - - // Configure the background worker which sends events to sentry: - // Wait up to 5 seconds before shutdown while there are events to send. - options.setShutdownTimeoutMillis(5000); - - // Enable SDK logging with Debug level - options.setDebug(true); - // To change the verbosity, use: - // By default it's DEBUG. - // options.setDiagnosticLevel( - // SentryLevel - // .ERROR); // A good option to have SDK debug log in prod is to use - // only level - // ERROR here. - options.setEnablePrettySerializationOutput(false); - - // Exclude frames from some packages from being "inApp" so are hidden by default in Sentry - // UI: - options.addInAppExclude("org.jboss"); - - // Include frames from our package - options.addInAppInclude("io.sentry.samples"); - - // Performance configuration options - // Set what percentage of traces should be collected - options.setTracesSampleRate(1.0); // set 0.5 to send 50% of traces - - // Determine traces sample rate based on the sampling context - // options.setTracesSampler( - // context -> { - // // only 10% of transactions with "/product" prefix will be collected - // if (!context.getTransactionContext().getName().startsWith("/products")) - // { - // return 0.1; - // } else { - // return 0.5; - // } - // }); - }); + AutoConfiguredOpenTelemetrySdk.builder() + .setResultAsGlobal() + .addPropertiesSupplier(() -> { + final Map properties = new HashMap<>(); + properties.put("otel.logs.exporter", "none"); + properties.put("otel.metrics.exporter", "none"); + properties.put("otel.traces.exporter", "none"); + return properties; + }) + .build(); Sentry.addBreadcrumb( "A 'bad breadcrumb' that will be rejected because of 'BeforeBreadcrumb callback above.'"); diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/DummyTest.kt b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/DummyTest.kt new file mode 100644 index 00000000000..6f762b7e453 --- /dev/null +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/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-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt new file mode 100644 index 00000000000..40a34a78aee --- /dev/null +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -0,0 +1,102 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import java.util.concurrent.TimeUnit +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class ConsoleApplicationSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8000") + testHelper.reset() + } + + @Test + fun `console application sends expected events when run as JAR`() { + val jarFile = testHelper.findJar("sentry-samples-console-opentelemetry-noagent") + val process = + testHelper.launch( + jarFile, + mapOf( + "SENTRY_DSN" to testHelper.dsn, +// "SENTRY_AUTO_INIT" to "false", + "SENTRY_TRACES_SAMPLE_RATE" to "1.0", + "SENTRY_ENABLE_PRETTY_SERIALIZATION_OUTPUT" to "false", + "SENTRY_DEBUG" to "true", + "OTEL_METRICS_EXPORTER" to "none", + "OTEL_LOGS_EXPORTER" to "none", + "OTEL_TRACES_EXPORTER" to "none", + ), + enableOtelAutoConfig = true + ) + + process.waitFor(30, TimeUnit.SECONDS) + assertEquals(0, process.exitValue()) + + // Verify that we received the expected events + verifyExpectedEvents() + } + + private fun verifyExpectedEvents() { + // Verify we received a "Fatal message!" event + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Fatal message!" && event.level?.name == "FATAL" + } + + // Verify we received a "Some warning!" event + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Some warning!" && event.level?.name == "WARNING" + } + + // Verify we received the RuntimeException + testHelper.ensureErrorReceived { event -> + event.exceptions?.any { ex -> ex.type == "RuntimeException" && ex.value == "Some error!" } == + true + } + + // Verify we received the detailed event with fingerprint + testHelper.ensureErrorReceived { event -> + event.message?.message == "Detailed event" && + event.fingerprints?.contains("NewClientDebug") == true && + event.level?.name == "DEBUG" + } + + // Verify we received transaction events + testHelper.ensureTransactionReceived { transaction, _ -> + transaction.transaction == "transaction name" && + transaction.spans?.any { span -> span.op == "child" } == true + } + + // Verify we received the loop messages (should be 10 of them) + var loopMessageCount = 0 + try { + for (i in 0..9) { + testHelper.ensureErrorReceived { event -> + val matches = + event.message?.message?.contains("items we'll wait to flush to Sentry!") == true + if (matches) loopMessageCount++ + matches + } + } + } catch (e: Exception) { + // Some loop messages might be missing, but we should have at least some + } + + assertTrue( + "Should receive at least 5 loop messages, got $loopMessageCount", + loopMessageCount >= 5, + ) + + // Verify we have breadcrumbs + testHelper.ensureErrorReceived { event -> + event.breadcrumbs?.any { breadcrumb -> + breadcrumb.message?.contains("Processed by") == true + } == true + } + } +} diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt index 56c699e995e..0117f66d38d 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt @@ -288,9 +288,19 @@ class TestHelper(backendUrl: String) { return jarFiles.maxOf { it } } - fun launch(jar: File, env: Map): Process { + fun launch(jar: File, env: Map, enableOtelAutoConfig: Boolean = false): Process { + val processBuilderList = mutableListOf("java", "--add-opens", "java.base/java.lang=ALL-UNNAMED") + + if (enableOtelAutoConfig) { + processBuilderList.add("-Dotel.java.global-autoconfigure.enabled=true") + } + + processBuilderList.add("-jar") + processBuilderList.add(jar.absolutePath) + val processBuilder = - ProcessBuilder("java", "-jar", jar.absolutePath).inheritIO() // forward i/o to current process + ProcessBuilder(processBuilderList) + .inheritIO() // forward i/o to current process processBuilder.environment().putAll(env) diff --git a/test/system-test-runner.py b/test/system-test-runner.py index aed0d7126a2..bff0f326775 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -57,21 +57,21 @@ class InteractiveSelection: modules: List[Tuple[str, str, str, str]] manual_test_mode: bool build_agent: bool - + def is_empty(self) -> bool: """Check if no modules were selected.""" return len(self.modules) == 0 - + def is_single_module(self) -> bool: """Check if exactly one module was selected.""" return len(self.modules) == 1 - + def get_first_module(self) -> Tuple[str, str, str, str]: """Get the first selected module (for manual test mode).""" if self.is_empty(): raise ValueError("No modules selected") return self.modules[0] - + def has_agent_modules(self) -> bool: """Check if any selected modules use the Java agent.""" return any(str_to_bool(agent) == "1" for _, agent, _, _ in self.modules) @@ -84,16 +84,16 @@ def __init__(self): self.spring_server_pid: Optional[int] = None self.mock_server_pid_file = "sentry-mock-server.pid" self.spring_server_pid_file = "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}") - + def read_pid_file(self, pid_file: str) -> Optional[int]: """Read PID from file if it exists.""" try: @@ -103,7 +103,7 @@ def read_pid_file(self, pid_file: str) -> Optional[int]: except (ValueError, IOError) as e: print(f"Error reading PID file {pid_file}: {e}") return None - + def is_process_running(self, pid: int) -> bool: """Check if a process with given PID is still running.""" try: @@ -112,14 +112,14 @@ def is_process_running(self, pid: int) -> bool: return True except (OSError, ProcessLookupError): return False - + def kill_process(self, pid: int, name: str) -> None: """Kill a process by PID.""" try: print(f"Killing existing {name} process with PID {pid}") os.kill(pid, signal.SIGTERM) time.sleep(2) # Give it time to terminate gracefully - + # Check if it's still running and force kill if necessary if self.is_process_running(pid): print(f"Process {pid} didn't terminate gracefully, force killing...") @@ -127,9 +127,9 @@ def kill_process(self, pid: int, name: str) -> None: time.sleep(1) except (OSError, ProcessLookupError): print(f"Process {pid} was already dead") - - + + def start_sentry_mock_server(self) -> None: """Start the Sentry mock server.""" print("Starting Sentry mock server...") @@ -141,21 +141,21 @@ def start_sentry_mock_server(self) -> None: 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}") - + # Wait a moment for the server to start time.sleep(2) - + except Exception as e: print(f"Failed to start mock server: {e}") raise - + def stop_sentry_mock_server(self) -> None: """Stop the Sentry mock server.""" try: @@ -165,7 +165,7 @@ def stop_sentry_mock_server(self) -> None: print("Sent stop signal to mock server") except: 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}") @@ -174,7 +174,7 @@ def stop_sentry_mock_server(self) -> None: 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: @@ -182,56 +182,56 @@ def stop_sentry_mock_server(self) -> None: if os.path.exists(self.mock_server_pid_file): os.remove(self.mock_server_pid_file) self.mock_server_pid = None - + def find_agent_jar(self) -> Optional[str]: """Find the OpenTelemetry agent JAR file.""" agent_dir = Path("sentry-opentelemetry/sentry-opentelemetry-agent/build/libs/") if not agent_dir.exists(): return None - + for jar_file in agent_dir.glob("*.jar"): name = jar_file.name - if ("agent" in name and - "javadoc" not in name and - "sources" not in name and + if ("agent" in name and + "javadoc" not in name and + "sources" not in name and "dontuse" not in name): return str(jar_file) return None - + def build_agent_jar(self) -> int: """Build the OpenTelemetry agent JAR file.""" print("Building OpenTelemetry agent JAR...") return self.run_gradle_task(":sentry-opentelemetry:sentry-opentelemetry-agent:assemble") - + def ensure_agent_jar(self, skip_build: bool = False) -> Optional[str]: """Ensure the OpenTelemetry agent JAR exists, building it if necessary.""" agent_jar = self.find_agent_jar() if agent_jar: return agent_jar - + if skip_build: print("OpenTelemetry agent JAR not found and build was skipped") return None - + # Agent JAR doesn't exist, try to build it print("OpenTelemetry agent JAR not found, building it...") build_result = self.build_agent_jar() if build_result != 0: print("Failed to build OpenTelemetry agent JAR") return None - + # Try to find it again after building agent_jar = self.find_agent_jar() if not agent_jar: print("OpenTelemetry agent JAR still not found after building") return None - + return agent_jar - + 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({ @@ -239,15 +239,15 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au "SENTRY_AUTO_INIT": java_agent_auto_init, "SENTRY_TRACES_SAMPLE_RATE": "1.0", "OTEL_TRACES_EXPORTER": "none", - "OTEL_METRICS_EXPORTER": "none", + "OTEL_METRICS_EXPORTER": "none", "OTEL_LOGS_EXPORTER": "none", "SENTRY_LOGS_ENABLED": "true" }) - + # Build command jar_path = f"sentry-samples/{sample_module}/build/libs/{sample_module}-0.0.1-SNAPSHOT.jar" cmd = ["java"] - + if java_agent == "1": agent_jar = self.ensure_agent_jar() if agent_jar: @@ -255,9 +255,9 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au print(f"Using Java Agent: {agent_jar}") else: print("Warning: Java agent was requested but could not be found or built") - + cmd.extend(["-jar", jar_path]) - + try: # Start the Spring server with open("spring-server.txt", "w") as log_file: @@ -267,22 +267,22 @@ def start_spring_server(self, sample_module: str, java_agent: str, java_agent_au stdout=log_file, stderr=subprocess.STDOUT ) - + # 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}") - + except Exception as e: print(f"Failed to start Spring server: {e}") raise - + 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...") - + for attempt in range(1, max_attempts + 1): try: response = requests.head( @@ -295,13 +295,13 @@ def wait_for_spring(self, max_attempts: int = 20) -> bool: return True except: pass - + print(f"Waiting... (attempt {attempt}/{max_attempts})") time.sleep(1) - + print("Spring application failed to become ready") return False - + def get_spring_status(self) -> dict: """Get status of Spring Boot application.""" status = { @@ -309,10 +309,10 @@ def get_spring_status(self) -> dict: "pid": self.spring_server_pid, "http_ready": False } - + if self.spring_server_pid and self.is_process_running(self.spring_server_pid): status["process_running"] = True - + # Check HTTP endpoint try: response = requests.head( @@ -324,9 +324,9 @@ def get_spring_status(self) -> dict: status["http_ready"] = True except: pass - + return status - + def get_sentry_status(self) -> dict: """Get status of Sentry mock server.""" status = { @@ -334,10 +334,10 @@ def get_sentry_status(self) -> dict: "pid": self.mock_server_pid, "http_ready": False } - + if self.mock_server_pid and self.is_process_running(self.mock_server_pid): status["process_running"] = True - + # Check HTTP endpoint try: response = requests.get("http://127.0.0.1:8000/envelope-count", timeout=2) @@ -345,25 +345,25 @@ def get_sentry_status(self) -> dict: status["http_ready"] = True except: pass - + return status - + def print_status_summary(self) -> None: """Print status summary of all services.""" print("=== Service Status ===") - + sentry_status = self.get_sentry_status() print(f"Sentry Mock Server:") print(f" PID: {sentry_status['pid'] or 'None'}") print(f" Process Running: {'✅' if sentry_status['process_running'] else '❌'}") print(f" HTTP Ready: {'✅' if sentry_status['http_ready'] else '❌'}") - + spring_status = self.get_spring_status() print(f"Spring Boot App:") print(f" PID: {spring_status['pid'] or 'None'}") print(f" Process Running: {'✅' if spring_status['process_running'] else '❌'}") print(f" HTTP Ready: {'✅' if spring_status['http_ready'] else '❌'}") - + def stop_spring_server(self) -> None: """Stop the Spring Boot server.""" try: @@ -378,7 +378,7 @@ def stop_spring_server(self) -> None: 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: @@ -386,17 +386,17 @@ def stop_spring_server(self) -> None: if os.path.exists(self.spring_server_pid_file): os.remove(self.spring_server_pid_file) self.spring_server_pid = None - + def get_build_task(self, sample_module: str) -> str: """Get the appropriate build task for a module.""" return "bootJar" if "spring" in sample_module else "assemble" - + def build_module(self, sample_module: str) -> int: """Build a sample module using the appropriate task.""" build_task = self.get_build_task(sample_module) print(f"Building {sample_module} using {build_task} task") return self.run_gradle_task(f":sentry-samples:{sample_module}:{build_task}") - + def run_gradle_task(self, task: str) -> int: """Run a Gradle task and return the exit code.""" print(f"Running: ./gradlew {task}") @@ -406,8 +406,8 @@ def run_gradle_task(self, task: str) -> int: except Exception as e: print(f"Failed to run Gradle task: {e}") return 1 - - def setup_test_infrastructure(self, sample_module: str, java_agent: str, + + def setup_test_infrastructure(self, sample_module: str, java_agent: str, java_agent_auto_init: str, build_before_run: str) -> int: """Set up test infrastructure. Returns 0 on success, error code on failure.""" # Build if requested @@ -417,18 +417,18 @@ def setup_test_infrastructure(self, sample_module: str, java_agent: str, if build_result != 0: print("Build failed") return build_result - + # Ensure agent JAR is available if needed if java_agent == "1": agent_jar = self.ensure_agent_jar() if not agent_jar: print("Error: Java agent was requested but could not be found or built") return 1 - + # Start mock server print("Starting Sentry mock server...") self.start_sentry_mock_server() - + # Start Spring server if it's a Spring module if "spring" in sample_module: print(f"Starting Spring server for {sample_module}...") @@ -437,49 +437,49 @@ def setup_test_infrastructure(self, sample_module: str, java_agent: str, print("Spring application failed to start!") return 1 print("Spring application is ready!") - + return 0 - - def run_single_test(self, sample_module: str, java_agent: str, + + def run_single_test(self, sample_module: str, java_agent: str, java_agent_auto_init: str, build_before_run: str) -> int: """Run a single system test.""" print(f"Running system test for {sample_module}") - + try: # Set up infrastructure setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) if setup_result != 0: return setup_result - + # Run the system test test_result = self.run_gradle_task(f":sentry-samples:{sample_module}:systemTest") - + return test_result - + finally: # Cleanup if "spring" in sample_module: self.stop_spring_server() self.stop_sentry_mock_server() - + def run_all_tests(self) -> int: """Run all system tests.""" test_configs = self.get_available_modules() - + failed_tests = [] - + for sample_module, java_agent, java_agent_auto_init, build_before_run in test_configs: # Convert true/false to internal 1/0 format agent = str_to_bool(java_agent) auto_init = java_agent_auto_init # already in correct format build = str_to_bool(build_before_run) - + print(f"\n{'='*60}") print(f"Running test: {sample_module} (agent={java_agent}, auto_init={java_agent_auto_init})") print(f"{'='*60}") - + result = self.run_single_test(sample_module, agent, auto_init, build) - + if result != 0: # Find the module number in the full list for interactive reference module_number = self._find_module_number(sample_module, java_agent, java_agent_auto_init) @@ -487,7 +487,7 @@ def run_all_tests(self) -> int: print(f"❌ Test failed: {sample_module}") else: print(f"✅ Test passed: {sample_module}") - + # Summary print(f"\n{'='*60}") print("TEST SUMMARY") @@ -495,7 +495,7 @@ def run_all_tests(self) -> int: print(f"Total tests: {len(test_configs)}") print(f"Passed: {len(test_configs) - len(failed_tests)}") print(f"Failed: {len(failed_tests)}") - + if failed_tests: print("\nFailed tests (for interactive mode, use these numbers):") for module_number, sample_module, java_agent, java_agent_auto_init in failed_tests: @@ -504,18 +504,18 @@ def run_all_tests(self) -> int: else: print("\n🎉 All tests passed!") return 0 - - def run_manual_test_mode(self, sample_module: str, java_agent: str, + + def run_manual_test_mode(self, sample_module: str, java_agent: str, java_agent_auto_init: str, build_before_run: str) -> int: """Set up infrastructure for manual testing from IDE.""" print(f"Setting up manual test environment for {sample_module}") - + try: # Set up infrastructure setup_result = self.setup_test_infrastructure(sample_module, java_agent, java_agent_auto_init, build_before_run) if setup_result != 0: return setup_result - + # Show status and wait for user print("\n" + "="*60) print("🚀 Manual test environment ready 🚀") @@ -528,25 +528,25 @@ def run_manual_test_mode(self, sample_module: str, java_agent: str, print(f" - Java Agent: {'Yes' if java_agent == '1' else 'No'}") print(f" - Agent Auto-init: {java_agent_auto_init}") print(f" - Mock DSN: http://502f25099c204a2fbf4cb16edc5975d1@localhost:8000/0") - + if "spring" in sample_module: print("\nSpring Boot app is running on: http://localhost:8080") - + print("\nPress Enter to stop the infrastructure and exit...") - + # Wait for user input try: input() except KeyboardInterrupt: print("\nReceived interrupt signal") - + print("\nStopping infrastructure...") return 0 - + finally: # Cleanup will happen in the finally block of main() pass - + def get_available_modules(self) -> List[Tuple[str, str, str, str]]: """Get list of all available test modules.""" return [ @@ -561,8 +561,9 @@ def get_available_modules(self) -> List[Tuple[str, str, str, str]]: ("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "true", "false"), ("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "false", "false"), ("sentry-samples-console", "false", "true", "false"), + ("sentry-samples-console-opentelemetry-noagent", "false", "true", "false"), ] - + def _find_module_number(self, module_name: str, agent: str, auto_init: str) -> int: """Find the module number in the interactive list (1-based).""" modules = self.get_available_modules() @@ -570,15 +571,15 @@ def _find_module_number(self, module_name: str, agent: str, auto_init: str) -> i if mod_name == module_name and mod_agent == agent and mod_auto_init == auto_init: return i return 0 # Should not happen, but return 0 if not found - + def parse_selection(self, user_input: str, max_index: int) -> List[int]: """Parse user selection string into list of indices.""" if user_input.strip() == "*": return list(range(max_index)) - + indices = [] parts = user_input.split(",") - + for part in parts: part = part.strip() if "-" in part: @@ -596,34 +597,34 @@ def parse_selection(self, user_input: str, max_index: int) -> List[int]: indices.append(int(part) - 1) except ValueError: raise ValueError(f"Invalid number: {part}") - + # Remove duplicates and sort indices = sorted(set(indices)) - + # Validate indices for idx in indices: if idx < 0 or idx >= max_index: raise ValueError(f"Index {idx + 1} is out of range (1-{max_index})") - + return indices - + def interactive_module_selection(self) -> InteractiveSelection: """Display modules and get user selection.""" modules = self.get_available_modules() - + print("\nAvailable test modules:") print("=" * 80) for i, (module, agent, auto_init, build) in enumerate(modules, 1): agent_text = "with agent" if str_to_bool(agent) == "1" else "no agent" auto_init_text = f"auto-init: {auto_init}" print(f"{i:2d}. {module:<50} ({agent_text}, {auto_init_text})") - + print("\nSelection options:") print(" * = all modules") print(" Single: 1, 5, 8") print(" Range: 1-4, 6-8") print(" Combined: 1,2,4-5,8") - + selected_modules = [] while True: try: @@ -631,29 +632,29 @@ def interactive_module_selection(self) -> InteractiveSelection: if not user_input: print("Please enter a selection.") continue - + selected_indices = self.parse_selection(user_input, len(modules)) selected_modules = [modules[i] for i in selected_indices] - + # Show confirmation print(f"\nSelected {len(selected_modules)} module(s):") for i, (module, agent, auto_init, build) in enumerate(selected_modules, 1): agent_text = "with agent" if str_to_bool(agent) == "1" else "no agent" print(f" {i}. {module} ({agent_text}, auto-init: {auto_init})") - + confirm = input("\nProceed with these selections? [Y/n]: ").strip().lower() if confirm in ('', 'y', 'yes'): break else: print("Please make a new selection.") - + except ValueError as e: print(f"Error: {e}") print("Please try again.") except KeyboardInterrupt: print("\nOperation cancelled.") return InteractiveSelection(modules=[], manual_test_mode=False, build_agent=False) - + # Ask about test mode manual_test_mode = False while True: @@ -670,7 +671,7 @@ def interactive_module_selection(self) -> InteractiveSelection: except KeyboardInterrupt: print("\nOperation cancelled.") return InteractiveSelection(modules=[], manual_test_mode=False, build_agent=False) - + # Ask about building agent if any modules use it build_agent = False has_agent_modules = any(str_to_bool(agent) == "1" for _, agent, _, _ in selected_modules) @@ -689,17 +690,17 @@ def interactive_module_selection(self) -> InteractiveSelection: except KeyboardInterrupt: print("\nOperation cancelled.") return InteractiveSelection(modules=[], manual_test_mode=False, build_agent=False) - + return InteractiveSelection(modules=selected_modules, manual_test_mode=manual_test_mode, build_agent=build_agent) - + def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: """Run tests with interactive module selection.""" selection = self.interactive_module_selection() - + if selection.is_empty(): print("No modules selected. Exiting.") return 0 - + # Build agent JAR if requested and modules use agent if selection.build_agent and selection.has_agent_modules(): print("\nBuilding OpenTelemetry agent JAR...") @@ -708,39 +709,39 @@ def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: print("Failed to build OpenTelemetry agent JAR") return build_result print("✅ OpenTelemetry agent JAR built successfully") - + # Handle manual test mode if selection.manual_test_mode: if not selection.is_single_module(): print("Error: Manual test mode can only be used with a single module.") print("Please select only one module for manual testing.") return 1 - + sample_module, test_agent, test_auto_init, test_build = selection.get_first_module() # Convert true/false to internal 1/0 format agent = str_to_bool(test_agent) auto_init = test_auto_init # already in correct format build = str_to_bool(test_build) - + print(f"\nSetting up manual test environment for: {sample_module}") return self.run_manual_test_mode(sample_module, agent, auto_init, build) - + # Handle automatic test running failed_tests = [] - + for i, (sample_module, test_agent, test_auto_init, test_build) in enumerate(selection.modules, 1): # Convert true/false to internal 1/0 format agent = str_to_bool(test_agent) auto_init = test_auto_init # already in correct format build = str_to_bool(test_build) - + print(f"\n{'='*60}") print(f"Running test {i}/{len(selection.modules)}: {sample_module}") print(f"Agent: {test_agent}, Auto-init: {test_auto_init}") print(f"{'='*60}") - + result = self.run_single_test(sample_module, agent, auto_init, build) - + if result != 0: # Find the module number in the full list for interactive reference module_number = self._find_module_number(sample_module, test_agent, test_auto_init) @@ -748,7 +749,7 @@ def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: print(f"❌ Test failed: {sample_module}") else: print(f"✅ Test passed: {sample_module}") - + # Summary print(f"\n{'='*60}") print("TEST SUMMARY") @@ -756,7 +757,7 @@ def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: print(f"Total tests: {len(selection.modules)}") print(f"Passed: {len(selection.modules) - len(failed_tests)}") print(f"Failed: {len(failed_tests)}") - + if failed_tests: print("\nFailed tests (for interactive mode, use these numbers):") for module_number, sample_module, test_agent, test_auto_init in failed_tests: @@ -765,7 +766,7 @@ def run_interactive_tests(self, agent: str, auto_init: str, build: str) -> int: else: print("\n🎉 All tests passed!") return 0 - + def cleanup_on_exit(self, signum, frame): """Cleanup handler for signals.""" print(f"\nReceived signal {signum}, cleaning up...") @@ -776,7 +777,7 @@ def cleanup_on_exit(self, signum, frame): def main(): parser = argparse.ArgumentParser(description="System Test Runner for Sentry Java") subparsers = parser.add_subparsers(dest="command", help="Available commands") - + # Test subcommand test_parser = subparsers.add_parser("test", help="Run system tests") test_group = test_parser.add_mutually_exclusive_group(required=True) @@ -787,54 +788,54 @@ def main(): test_parser.add_argument("--auto-init", default="true", help="Auto-init agent (true or false)") 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") - + # 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") - + spring_start_parser = spring_subparsers.add_parser("start", help="Start Spring Boot application") spring_start_parser.add_argument("module", help="Sample module to start") spring_start_parser.add_argument("--agent", default="false", help="Use Java agent (true or false)") spring_start_parser.add_argument("--auto-init", default="true", help="Auto-init agent (true or false)") spring_start_parser.add_argument("--build", default="false", help="Build before starting (true or false)") - + spring_stop_parser = spring_subparsers.add_parser("stop", help="Stop Spring Boot application") - + spring_wait_parser = spring_subparsers.add_parser("wait", help="Wait for Spring Boot application to be ready") spring_wait_parser.add_argument("--timeout", type=int, default=20, help="Max attempts to wait (default: 20)") - + spring_status_parser = spring_subparsers.add_parser("status", help="Check Spring Boot application status") - + # Sentry subcommand sentry_parser = subparsers.add_parser("sentry", help="Manage Sentry mock server") sentry_subparsers = sentry_parser.add_subparsers(dest="sentry_action", help="Sentry actions") - + sentry_start_parser = sentry_subparsers.add_parser("start", help="Start Sentry mock server") sentry_stop_parser = sentry_subparsers.add_parser("stop", help="Stop Sentry mock server") sentry_status_parser = sentry_subparsers.add_parser("status", help="Check Sentry mock server status") - + # Status subcommand status_parser = subparsers.add_parser("status", help="Show status of all services") - + args = parser.parse_args() - + if not args.command: parser.print_help() return 1 - + runner = SystemTestRunner() - + # Set up signal handlers for cleanup signal.signal(signal.SIGINT, runner.cleanup_on_exit) signal.signal(signal.SIGTERM, runner.cleanup_on_exit) - + try: if args.command == "test": # Convert true/false arguments to internal 1/0 format agent = str_to_bool(args.agent) auto_init = args.auto_init # already accepts true/false build = str_to_bool(args.build) - + if args.manual_test and args.module: return runner.run_manual_test_mode(args.module, agent, auto_init, build) elif args.manual_test and args.all: @@ -849,14 +850,14 @@ def main(): return runner.run_single_test(args.module, agent, auto_init, build) elif args.interactive: return runner.run_interactive_tests(agent, auto_init, build) - + elif args.command == "spring": if args.spring_action == "start": # Convert true/false arguments to internal format agent = str_to_bool(args.agent) auto_init = args.auto_init # already accepts true/false build = str_to_bool(args.build) - + # Build if requested if build == "1": print("Building before starting Spring application") @@ -864,7 +865,7 @@ def main(): if build_result != 0: print("Build failed") return build_result - + runner.start_spring_server(args.module, agent, auto_init) if runner.wait_for_spring(): print("Spring application started successfully!") @@ -893,7 +894,7 @@ def main(): else: spring_parser.print_help() return 1 - + elif args.command == "sentry": if args.sentry_action == "start": runner.start_sentry_mock_server() @@ -913,14 +914,14 @@ def main(): else: sentry_parser.print_help() return 1 - + elif args.command == "status": runner.print_status_summary() return 0 else: parser.print_help() return 1 - + except KeyboardInterrupt: print("\nInterrupted by user") return 1 @@ -934,4 +935,4 @@ def main(): runner.stop_sentry_mock_server() if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + sys.exit(main()) From 0ac58fd9d7d75609db5824a8bdf686748d19bbae Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 21 Jul 2025 14:25:35 +0200 Subject: [PATCH 11/19] fixes after merge --- .../java/io/sentry/samples/console/Main.java | 16 ++++++++-------- .../systemtest/ConsoleApplicationSystemTest.kt | 5 ++--- test/system-test-runner.py | 1 + 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java index d552968f315..8af939c32bf 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java @@ -4,7 +4,6 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.context.Scope; -import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.sentry.Breadcrumb; import io.sentry.EventProcessor; @@ -27,13 +26,14 @@ public class Main { public static void main(String[] args) throws InterruptedException { AutoConfiguredOpenTelemetrySdk.builder() .setResultAsGlobal() - .addPropertiesSupplier(() -> { - final Map properties = new HashMap<>(); - properties.put("otel.logs.exporter", "none"); - properties.put("otel.metrics.exporter", "none"); - properties.put("otel.traces.exporter", "none"); - return properties; - }) + .addPropertiesSupplier( + () -> { + final Map properties = new HashMap<>(); + properties.put("otel.logs.exporter", "none"); + properties.put("otel.metrics.exporter", "none"); + properties.put("otel.traces.exporter", "none"); + return properties; + }) .build(); Sentry.addBreadcrumb( diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt index 40a34a78aee..7f5453c9a07 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -24,15 +24,14 @@ class ConsoleApplicationSystemTest { jarFile, mapOf( "SENTRY_DSN" to testHelper.dsn, -// "SENTRY_AUTO_INIT" to "false", + // "SENTRY_AUTO_INIT" to "false", "SENTRY_TRACES_SAMPLE_RATE" to "1.0", "SENTRY_ENABLE_PRETTY_SERIALIZATION_OUTPUT" to "false", "SENTRY_DEBUG" to "true", "OTEL_METRICS_EXPORTER" to "none", "OTEL_LOGS_EXPORTER" to "none", "OTEL_TRACES_EXPORTER" to "none", - ), - enableOtelAutoConfig = true + ) ) process.waitFor(30, TimeUnit.SECONDS) diff --git a/test/system-test-runner.py b/test/system-test-runner.py index 619e6bda104..fda5042c819 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -587,6 +587,7 @@ def get_available_modules(self) -> List[ModuleConfig]: ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "true", "false"), ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "false", "false"), ModuleConfig("sentry-samples-console", "false", "true", "false"), + ModuleConfig("sentry-samples-console-opentelemetry-noagent", "false", "true", "false"), ] def _find_module_number(self, module_name: str, agent: str, auto_init: str) -> int: From b5f239274cfa391d095179d29759dcf89f4867fd Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 21 Jul 2025 12:28:42 +0000 Subject: [PATCH 12/19] Format code --- .../kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt index 7f5453c9a07..427c930653b 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -31,7 +31,7 @@ class ConsoleApplicationSystemTest { "OTEL_METRICS_EXPORTER" to "none", "OTEL_LOGS_EXPORTER" to "none", "OTEL_TRACES_EXPORTER" to "none", - ) + ), ) process.waitFor(30, TimeUnit.SECONDS) From 50b18c1d9b2e0960d23638bc3ec34ca7eb560a07 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 10 Jul 2025 10:09:22 +0200 Subject: [PATCH 13/19] e2e tests for console app --- .../java/io/sentry/samples/console/Main.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java index 16f5a09d1b4..7ee46649e97 100644 --- a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java @@ -66,8 +66,8 @@ public static void main(String[] args) throws InterruptedException { // Enable SDK logging with Debug level options.setDebug(true); // To change the verbosity, use: - // options.setDiagnosticLevel(SentryLevel.ERROR); // By default it's DEBUG. + // options.setDiagnosticLevel(SentryLevel.ERROR); // A good option to have SDK debug log in prod is to use only level ERROR here. // Exclude frames from some packages from being "inApp" so are hidden by default in Sentry @@ -82,15 +82,16 @@ public static void main(String[] args) throws InterruptedException { options.setTracesSampleRate(1.0); // set 0.5 to send 50% of traces // Determine traces sample rate based on the sampling context - // options.setTracesSampler( - // context -> { - // // only 10% of transactions with "/product" prefix will be collected - // if (!context.getTransactionContext().getName().startsWith("/products")) { - // return 0.1; - // } else { - // return 0.5; - // } - // }); + // options.setTracesSampler( + // context -> { + // // only 10% of transactions with "/product" prefix will be collected + // if (!context.getTransactionContext().getName().startsWith("/products")) + // { + // return 0.1; + // } else { + // return 0.5; + // } + // }); }); Sentry.addBreadcrumb( From 846237010c4ab91b6bd5b8c5535a80370cd7efb8 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 21 Jul 2025 09:12:06 +0200 Subject: [PATCH 14/19] Implement E2E tests for OTel based console sample --- .../build.gradle.kts | 75 ++++++++++++- .../java/io/sentry/samples/console/Main.java | 94 +++------------- .../src/test/kotlin/sentry/DummyTest.kt | 12 +++ .../ConsoleApplicationSystemTest.kt | 102 ++++++++++++++++++ .../io/sentry/systemtest/util/TestHelper.kt | 14 ++- test/system-test-runner.py | 1 + 6 files changed, 213 insertions(+), 85 deletions(-) create mode 100644 sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/DummyTest.kt create mode 100644 sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts b/sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts index 945d906df8a..8821c25626b 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts @@ -1,14 +1,83 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + plugins { java application + kotlin("jvm") alias(libs.plugins.gradle.versions) + id("com.github.johnrengelman.shadow") version "8.1.1" } application { mainClass.set("io.sentry.samples.console.Main") } +java.sourceCompatibility = JavaVersion.VERSION_17 + +java.targetCompatibility = JavaVersion.VERSION_17 + +repositories { mavenCentral() } + configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() +} + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = JavaVersion.VERSION_17.toString() + } +} + +dependencies { + implementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentless) + + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(projects.sentry) + testImplementation(projects.sentrySystemTestSupport) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.slf4j.api) + testImplementation(libs.slf4j.jdk14) +} + +// Configure the Shadow JAR (executable JAR with all dependencies) +tasks.shadowJar { + manifest { attributes["Main-Class"] = "io.sentry.samples.console.Main" } + archiveClassifier.set("") // Remove the classifier so it replaces the regular JAR + mergeServiceFiles() } -dependencies { implementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentless) } +// Make the regular jar task depend on shadowJar +tasks.jar { + enabled = false + dependsOn(tasks.shadowJar) +} + +// Fix the startScripts task dependency +tasks.startScripts { dependsOn(tasks.shadowJar) } + +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-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java index c27aad737b1..d552968f315 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java @@ -4,6 +4,8 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.sentry.Breadcrumb; import io.sentry.EventProcessor; import io.sentry.Hint; @@ -17,90 +19,22 @@ import io.sentry.protocol.Message; import io.sentry.protocol.User; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; public class Main { public static void main(String[] args) throws InterruptedException { - Sentry.init( - options -> { - // NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in - // your Sentry project/dashboard - options.setDsn( - "https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563"); - - // All events get assigned to the release. See more at - // https://docs.sentry.io/workflow/releases/ - options.setRelease("io.sentry.samples.console@3.0.0+1"); - - // Modifications to event before it goes out. Could replace the event altogether - options.setBeforeSend( - (event, hint) -> { - // Drop an event altogether: - if (event.getTag("SomeTag") != null) { - return null; - } - return event; - }); - - options.setBeforeSendTransaction( - (transaction, hint) -> { - // Drop a transaction: - if (transaction.getTag("SomeTransactionTag") != null) { - return null; - } - - return transaction; - }); - - // Allows inspecting and modifying, returning a new or simply rejecting (returning null) - options.setBeforeBreadcrumb( - (breadcrumb, hint) -> { - // Don't add breadcrumbs with message containing: - if (breadcrumb.getMessage() != null - && breadcrumb.getMessage().contains("bad breadcrumb")) { - return null; - } - return breadcrumb; - }); - - // Configure the background worker which sends events to sentry: - // Wait up to 5 seconds before shutdown while there are events to send. - options.setShutdownTimeoutMillis(5000); - - // Enable SDK logging with Debug level - options.setDebug(true); - // To change the verbosity, use: - // By default it's DEBUG. - // options.setDiagnosticLevel( - // SentryLevel - // .ERROR); // A good option to have SDK debug log in prod is to use - // only level - // ERROR here. - options.setEnablePrettySerializationOutput(false); - - // Exclude frames from some packages from being "inApp" so are hidden by default in Sentry - // UI: - options.addInAppExclude("org.jboss"); - - // Include frames from our package - options.addInAppInclude("io.sentry.samples"); - - // Performance configuration options - // Set what percentage of traces should be collected - options.setTracesSampleRate(1.0); // set 0.5 to send 50% of traces - - // Determine traces sample rate based on the sampling context - // options.setTracesSampler( - // context -> { - // // only 10% of transactions with "/product" prefix will be collected - // if (!context.getTransactionContext().getName().startsWith("/products")) - // { - // return 0.1; - // } else { - // return 0.5; - // } - // }); - }); + AutoConfiguredOpenTelemetrySdk.builder() + .setResultAsGlobal() + .addPropertiesSupplier(() -> { + final Map properties = new HashMap<>(); + properties.put("otel.logs.exporter", "none"); + properties.put("otel.metrics.exporter", "none"); + properties.put("otel.traces.exporter", "none"); + return properties; + }) + .build(); Sentry.addBreadcrumb( "A 'bad breadcrumb' that will be rejected because of 'BeforeBreadcrumb callback above.'"); diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/DummyTest.kt b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/DummyTest.kt new file mode 100644 index 00000000000..6f762b7e453 --- /dev/null +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/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-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt new file mode 100644 index 00000000000..40a34a78aee --- /dev/null +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -0,0 +1,102 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import java.util.concurrent.TimeUnit +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class ConsoleApplicationSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8000") + testHelper.reset() + } + + @Test + fun `console application sends expected events when run as JAR`() { + val jarFile = testHelper.findJar("sentry-samples-console-opentelemetry-noagent") + val process = + testHelper.launch( + jarFile, + mapOf( + "SENTRY_DSN" to testHelper.dsn, +// "SENTRY_AUTO_INIT" to "false", + "SENTRY_TRACES_SAMPLE_RATE" to "1.0", + "SENTRY_ENABLE_PRETTY_SERIALIZATION_OUTPUT" to "false", + "SENTRY_DEBUG" to "true", + "OTEL_METRICS_EXPORTER" to "none", + "OTEL_LOGS_EXPORTER" to "none", + "OTEL_TRACES_EXPORTER" to "none", + ), + enableOtelAutoConfig = true + ) + + process.waitFor(30, TimeUnit.SECONDS) + assertEquals(0, process.exitValue()) + + // Verify that we received the expected events + verifyExpectedEvents() + } + + private fun verifyExpectedEvents() { + // Verify we received a "Fatal message!" event + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Fatal message!" && event.level?.name == "FATAL" + } + + // Verify we received a "Some warning!" event + testHelper.ensureErrorReceived { event -> + event.message?.formatted == "Some warning!" && event.level?.name == "WARNING" + } + + // Verify we received the RuntimeException + testHelper.ensureErrorReceived { event -> + event.exceptions?.any { ex -> ex.type == "RuntimeException" && ex.value == "Some error!" } == + true + } + + // Verify we received the detailed event with fingerprint + testHelper.ensureErrorReceived { event -> + event.message?.message == "Detailed event" && + event.fingerprints?.contains("NewClientDebug") == true && + event.level?.name == "DEBUG" + } + + // Verify we received transaction events + testHelper.ensureTransactionReceived { transaction, _ -> + transaction.transaction == "transaction name" && + transaction.spans?.any { span -> span.op == "child" } == true + } + + // Verify we received the loop messages (should be 10 of them) + var loopMessageCount = 0 + try { + for (i in 0..9) { + testHelper.ensureErrorReceived { event -> + val matches = + event.message?.message?.contains("items we'll wait to flush to Sentry!") == true + if (matches) loopMessageCount++ + matches + } + } + } catch (e: Exception) { + // Some loop messages might be missing, but we should have at least some + } + + assertTrue( + "Should receive at least 5 loop messages, got $loopMessageCount", + loopMessageCount >= 5, + ) + + // Verify we have breadcrumbs + testHelper.ensureErrorReceived { event -> + event.breadcrumbs?.any { breadcrumb -> + breadcrumb.message?.contains("Processed by") == true + } == true + } + } +} diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt index 56c699e995e..0117f66d38d 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt @@ -288,9 +288,19 @@ class TestHelper(backendUrl: String) { return jarFiles.maxOf { it } } - fun launch(jar: File, env: Map): Process { + fun launch(jar: File, env: Map, enableOtelAutoConfig: Boolean = false): Process { + val processBuilderList = mutableListOf("java", "--add-opens", "java.base/java.lang=ALL-UNNAMED") + + if (enableOtelAutoConfig) { + processBuilderList.add("-Dotel.java.global-autoconfigure.enabled=true") + } + + processBuilderList.add("-jar") + processBuilderList.add(jar.absolutePath) + val processBuilder = - ProcessBuilder("java", "-jar", jar.absolutePath).inheritIO() // forward i/o to current process + ProcessBuilder(processBuilderList) + .inheritIO() // forward i/o to current process processBuilder.environment().putAll(env) diff --git a/test/system-test-runner.py b/test/system-test-runner.py index 619e6bda104..6eb9f01859f 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -587,6 +587,7 @@ def get_available_modules(self) -> List[ModuleConfig]: ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "true", "false"), ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "false", "false"), ModuleConfig("sentry-samples-console", "false", "true", "false"), + ("sentry-samples-console-opentelemetry-noagent", "false", "true", "false"), ] def _find_module_number(self, module_name: str, agent: str, auto_init: str) -> int: From 1dc620eb9dfe901537f63d06b5e3f2b75e5c4f1b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 21 Jul 2025 14:25:35 +0200 Subject: [PATCH 15/19] fixes after merge --- .../java/io/sentry/samples/console/Main.java | 16 ++++++++-------- .../systemtest/ConsoleApplicationSystemTest.kt | 5 ++--- test/system-test-runner.py | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java index d552968f315..8af939c32bf 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java @@ -4,7 +4,6 @@ import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.context.Scope; -import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; import io.sentry.Breadcrumb; import io.sentry.EventProcessor; @@ -27,13 +26,14 @@ public class Main { public static void main(String[] args) throws InterruptedException { AutoConfiguredOpenTelemetrySdk.builder() .setResultAsGlobal() - .addPropertiesSupplier(() -> { - final Map properties = new HashMap<>(); - properties.put("otel.logs.exporter", "none"); - properties.put("otel.metrics.exporter", "none"); - properties.put("otel.traces.exporter", "none"); - return properties; - }) + .addPropertiesSupplier( + () -> { + final Map properties = new HashMap<>(); + properties.put("otel.logs.exporter", "none"); + properties.put("otel.metrics.exporter", "none"); + properties.put("otel.traces.exporter", "none"); + return properties; + }) .build(); Sentry.addBreadcrumb( diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt index 40a34a78aee..7f5453c9a07 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -24,15 +24,14 @@ class ConsoleApplicationSystemTest { jarFile, mapOf( "SENTRY_DSN" to testHelper.dsn, -// "SENTRY_AUTO_INIT" to "false", + // "SENTRY_AUTO_INIT" to "false", "SENTRY_TRACES_SAMPLE_RATE" to "1.0", "SENTRY_ENABLE_PRETTY_SERIALIZATION_OUTPUT" to "false", "SENTRY_DEBUG" to "true", "OTEL_METRICS_EXPORTER" to "none", "OTEL_LOGS_EXPORTER" to "none", "OTEL_TRACES_EXPORTER" to "none", - ), - enableOtelAutoConfig = true + ) ) process.waitFor(30, TimeUnit.SECONDS) diff --git a/test/system-test-runner.py b/test/system-test-runner.py index 6eb9f01859f..fda5042c819 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -587,7 +587,7 @@ def get_available_modules(self) -> List[ModuleConfig]: ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "true", "false"), ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "false", "false"), ModuleConfig("sentry-samples-console", "false", "true", "false"), - ("sentry-samples-console-opentelemetry-noagent", "false", "true", "false"), + ModuleConfig("sentry-samples-console-opentelemetry-noagent", "false", "true", "false"), ] def _find_module_number(self, module_name: str, agent: str, auto_init: str) -> int: From eac786177917af4a75514eeca6ec80ddc4609815 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 29 Jul 2025 13:40:11 +0000 Subject: [PATCH 16/19] Format code --- .../kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt | 2 +- .../src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt index 7f5453c9a07..427c930653b 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -31,7 +31,7 @@ class ConsoleApplicationSystemTest { "OTEL_METRICS_EXPORTER" to "none", "OTEL_LOGS_EXPORTER" to "none", "OTEL_TRACES_EXPORTER" to "none", - ) + ), ) process.waitFor(30, TimeUnit.SECONDS) diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt index 0117f66d38d..73b2acd2ae1 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt @@ -299,8 +299,7 @@ class TestHelper(backendUrl: String) { processBuilderList.add(jar.absolutePath) val processBuilder = - ProcessBuilder(processBuilderList) - .inheritIO() // forward i/o to current process + ProcessBuilder(processBuilderList).inheritIO() // forward i/o to current process processBuilder.environment().putAll(env) From ab5be7768c639770f1dcda935c798d076c57f870 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 29 Jul 2025 15:45:01 +0200 Subject: [PATCH 17/19] api --- sentry-system-test-support/api/sentry-system-test-support.api | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sentry-system-test-support/api/sentry-system-test-support.api b/sentry-system-test-support/api/sentry-system-test-support.api index 3e2653f717a..b32583074d4 100644 --- a/sentry-system-test-support/api/sentry-system-test-support.api +++ b/sentry-system-test-support/api/sentry-system-test-support.api @@ -576,7 +576,8 @@ public final class io/sentry/systemtest/util/TestHelper { public final fun getJsonSerializer ()Lio/sentry/JsonSerializer; public final fun getRestClient ()Lio/sentry/systemtest/util/RestTestClient; public final fun getSentryClient ()Lio/sentry/systemtest/util/SentryMockServerClient; - public final fun launch (Ljava/io/File;Ljava/util/Map;)Ljava/lang/Process; + public final fun launch (Ljava/io/File;Ljava/util/Map;Z)Ljava/lang/Process; + public static synthetic fun launch$default (Lio/sentry/systemtest/util/TestHelper;Ljava/io/File;Ljava/util/Map;ZILjava/lang/Object;)Ljava/lang/Process; public final fun logObject (Ljava/lang/Object;)V public final fun reset ()V public final fun setEnvelopeCounts (Lio/sentry/systemtest/util/EnvelopeCounts;)V From a889829158a4f43b12e543054849720fa1873fe2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 29 Jul 2025 16:15:26 +0200 Subject: [PATCH 18/19] Reduce scope forking when using OpenTelemetry (#4565) * Reduce scope forking in OpenTelemetry * Format code * api * changelog --------- Co-authored-by: Sentry Github Bot --- CHANGELOG.md | 3 +++ .../api/sentry-opentelemetry-bootstrap.api | 1 + .../java/io/sentry/opentelemetry/SentryContextStorage.java | 5 +++++ .../java/io/sentry/opentelemetry/SentryContextWrapper.java | 2 +- 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd255f9fb2e..e7c4fd9fc70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,9 @@ - This was causing Sentry SDK to log warnings: "Sentry Log is disabled and this 'logger' call is a no-op." - Do not use Sentry logging API in Log4j2 if logs are disabled ([#4573](https://github.com/getsentry/sentry-java/pull/4573)) - This was causing Sentry SDK to log warnings: "Sentry Log is disabled and this 'logger' call is a no-op." +- Reduce scope forking when using OpenTelemetry ([#4565](https://github.com/getsentry/sentry-java/pull/4565)) + - `Sentry.withScope` now has the correct current scope passed to the callback. Previously our OpenTelemetry integration forked scopes an additional. + - Overall the SDK is now forking scopes a bit less often. ## 8.17.0 diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api index 8e6b59b4be2..3a63bf04d98 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api @@ -151,6 +151,7 @@ public final class io/sentry/opentelemetry/SentryContextStorage : io/opentelemet public fun (Lio/opentelemetry/context/ContextStorage;)V public fun attach (Lio/opentelemetry/context/Context;)Lio/opentelemetry/context/Scope; public fun current ()Lio/opentelemetry/context/Context; + public fun root ()Lio/opentelemetry/context/Context; } public final class io/sentry/opentelemetry/SentryContextStorageProvider : io/opentelemetry/context/ContextStorageProvider { diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java index 4f3efa40c29..5a916a9ecab 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextStorage.java @@ -38,4 +38,9 @@ public Scope attach(Context toAttach) { public Context current() { return contextStorage.current(); } + + @Override + public Context root() { + return SentryContextWrapper.wrap(ContextStorage.super.root()); + } } diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java index a0213bafe65..1b0581fc9e2 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/SentryContextWrapper.java @@ -32,7 +32,7 @@ public Context with(final @NotNull ContextKey contextKey, V v) { if (isOpentelemetrySpan(contextKey)) { return forkCurrentScope(modifiedContext); } else { - return modifiedContext; + return new SentryContextWrapper(modifiedContext); } } From 2e6b3a30afd2bd307463b835f63049ac75acb3a4 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 29 Jul 2025 16:34:19 +0200 Subject: [PATCH 19/19] SDKs send queue is no longer shutdown immediately on re-init (#4564) * Let queue drain on a restart * Format code * Format code * Update sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt * Let queue drain on a restart * Format code * Format code * Update sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt * adapt tests * changelog --------- Co-authored-by: Sentry Github Bot --- CHANGELOG.md | 2 ++ .../io/sentry/ShutdownHookIntegration.java | 3 +- .../sentry/transport/AsyncHttpTransport.java | 31 ++++++++++--------- .../transport/AsyncHttpTransportTest.kt | 21 ++++++++----- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7c4fd9fc70..85dc62b5559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ - This was causing Sentry SDK to log warnings: "Sentry Log is disabled and this 'logger' call is a no-op." - Do not use Sentry logging API in Log4j2 if logs are disabled ([#4573](https://github.com/getsentry/sentry-java/pull/4573)) - This was causing Sentry SDK to log warnings: "Sentry Log is disabled and this 'logger' call is a no-op." +- SDKs send queue is no longer shutdown immediately on re-init ([#4564](https://github.com/getsentry/sentry-java/pull/4564)) + - This means we're no longer losing events that have been enqueued right before SDK re-init. - Reduce scope forking when using OpenTelemetry ([#4565](https://github.com/getsentry/sentry-java/pull/4565)) - `Sentry.withScope` now has the correct current scope passed to the callback. Previously our OpenTelemetry integration forked scopes an additional. - Overall the SDK is now forking scopes a bit less often. diff --git a/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java b/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java index 3d5bccc3d7b..9a70e31cee6 100644 --- a/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java +++ b/sentry/src/main/java/io/sentry/ShutdownHookIntegration.java @@ -32,7 +32,8 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions Objects.requireNonNull(options, "SentryOptions is required"); if (options.isEnableShutdownHook()) { - thread = new Thread(() -> scopes.flush(options.getFlushTimeoutMillis())); + thread = + new Thread(() -> scopes.flush(options.getFlushTimeoutMillis()), "sentry-shutdownhook"); handleShutdownInProgress( () -> { runtime.addShutdownHook(thread); diff --git a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java index 24f954c0c10..149c04f2f81 100644 --- a/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java +++ b/sentry/src/main/java/io/sentry/transport/AsyncHttpTransport.java @@ -174,20 +174,23 @@ public void close(final boolean isRestarting) throws IOException { executor.shutdown(); options.getLogger().log(SentryLevel.DEBUG, "Shutting down"); try { - // We need a small timeout to be able to save to disk any rejected envelope - long timeout = isRestarting ? 0 : options.getFlushTimeoutMillis(); - if (!executor.awaitTermination(timeout, TimeUnit.MILLISECONDS)) { - options - .getLogger() - .log( - SentryLevel.WARNING, - "Failed to shutdown the async connection async sender within " - + timeout - + " ms. Trying to force it now."); - executor.shutdownNow(); - if (currentRunnable != null) { - // We store to disk any envelope that is currently being sent - executor.getRejectedExecutionHandler().rejectedExecution(currentRunnable, executor); + // only stop sending events on a real shutdown, not on a restart + if (!isRestarting) { + // We need a small timeout to be able to save to disk any rejected envelope + long timeout = options.getFlushTimeoutMillis(); + if (!executor.awaitTermination(timeout, TimeUnit.MILLISECONDS)) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Failed to shutdown the async connection async sender within " + + timeout + + " ms. Trying to force it now."); + executor.shutdownNow(); + if (currentRunnable != null) { + // We store to disk any envelope that is currently being sent + executor.getRejectedExecutionHandler().rejectedExecution(currentRunnable, executor); + } } } } catch (InterruptedException e) { diff --git a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt index c7ed2b46aa8..c3a7f2ce04f 100644 --- a/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt +++ b/sentry/src/test/java/io/sentry/transport/AsyncHttpTransportTest.kt @@ -352,16 +352,22 @@ class AsyncHttpTransportTest { } @Test - fun `close with isRestarting true does not await termination`() { - fixture.sentryOptions.flushTimeoutMillis = 123 + fun `close shuts down the executor and runs executing runnable through rejectedExecutionHandler`() { + val rejectedExecutionHandler = mock() val sut = fixture.getSUT() - sut.close(true) + val runnable = mock() - verify(fixture.executor).awaitTermination(eq(0), eq(TimeUnit.MILLISECONDS)) + // Emulate a runnable currently being executed + sut.injectForField("currentRunnable", runnable) + whenever(fixture.executor.rejectedExecutionHandler).thenReturn(rejectedExecutionHandler) + sut.close(false) + + verify(fixture.executor).shutdownNow() + verify(rejectedExecutionHandler).rejectedExecution(eq(runnable), eq(fixture.executor)) } @Test - fun `close shuts down the executor and runs executing runnable through rejectedExecutionHandler`() { + fun `does not shut down executor immediately on restart`() { val rejectedExecutionHandler = mock() val sut = fixture.getSUT() val runnable = mock() @@ -371,8 +377,9 @@ class AsyncHttpTransportTest { whenever(fixture.executor.rejectedExecutionHandler).thenReturn(rejectedExecutionHandler) sut.close(true) - verify(fixture.executor).shutdownNow() - verify(rejectedExecutionHandler).rejectedExecution(eq(runnable), eq(fixture.executor)) + verify(fixture.executor).shutdown() + verify(fixture.executor, never()).shutdownNow() + verify(rejectedExecutionHandler, never()).rejectedExecution(eq(runnable), eq(fixture.executor)) } @Test