Skip to content

Commit 1295a3d

Browse files
runningcodeclaude
andauthored
test(otel): Add a smoke test that runs an app with the built agent (#6269)
The agent had no tests; only the sample system tests exercised it. The test launches a JDK-only app in a child JVM with the agent attached and asserts that an incoming request reaches MockSentryServer as an http.server transaction from the agent SDK. The app runs in a child JVM so the test's own JVM stays uninstrumented. Otherwise the agent would turn the test's HTTP calls and the mock server into spans. The agent jar is wired as a test input, so the test reruns whenever the jar changes. Adding Kotlin for the test enrolls the module in API validation, hence the new API dump. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent ff8c06c commit 1295a3d

4 files changed

Lines changed: 153 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
public final class io/sentry/opentelemetry/agent/AgentMarker {
2+
public fun <init> ()V
3+
}
4+

‎sentry-opentelemetry/sentry-opentelemetry-agent/build.gradle.kts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
2+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
23

34
plugins {
45
`java-library`
56
id("io.sentry.animalsniffer")
67
id("io.sentry.javadoc")
8+
alias(libs.plugins.kotlin.jvm)
79
alias(libs.plugins.shadow)
810
}
911

12+
tasks.named<KotlinCompile>("compileKotlin") {
13+
compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
14+
}
15+
1016
fun relocatePackages(shadowJar: ShadowJar) {
1117
// rewrite dependencies calling Logger.getLogger
1218
shadowJar.relocate("java.util.logging.Logger", "io.opentelemetry.javaagent.bootstrap.PatchLogger")
@@ -76,6 +82,11 @@ dependencies {
7682
bootstrapLibs(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap)
7783
javaagentLibs(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization)
7884
upstreamAgent(libs.otel.javaagent)
85+
86+
testImplementation(projects.sentryTestSupport)
87+
testImplementation(kotlin(Config.kotlinStdLib))
88+
testImplementation(libs.kotlin.test.junit)
89+
testImplementation(libs.google.truth)
7990
}
8091

8192
fun isolateClasses(jars: Iterable<File>): CopySpec = copySpec {
@@ -197,6 +208,22 @@ tasks {
197208
}
198209

199210
assemble { dependsOn(shadowJar) }
211+
212+
test {
213+
jvmArgumentProviders.add(
214+
objects.newInstance<AgentJarArgument>().apply {
215+
agentJar.set(shadowJar.flatMap { it.archiveFile })
216+
}
217+
)
218+
}
219+
}
220+
221+
// Tests launch apps with the built agent attached, so the agent jar is a test input.
222+
abstract class AgentJarArgument : CommandLineArgumentProvider {
223+
@get:InputFile @get:PathSensitive(PathSensitivity.NONE) abstract val agentJar: RegularFileProperty
224+
225+
override fun asArguments() =
226+
listOf("-Dsentry.test.agentJar=${agentJar.get().asFile.absolutePath}")
200227
}
201228

202229
tasks.named("distZip").configure { this.dependsOn("jar") }
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.sentry.opentelemetry.agent.fixture;
2+
3+
import com.sun.net.httpserver.HttpServer;
4+
import java.io.IOException;
5+
import java.io.OutputStream;
6+
import java.net.InetAddress;
7+
import java.net.InetSocketAddress;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
10+
import java.nio.file.Paths;
11+
12+
/**
13+
* An app for the agent to instrument. It only uses the JDK, so a test can run it with nothing but
14+
* this class on the classpath, and it writes its port to the file named by the first argument.
15+
*/
16+
public final class HelloServer {
17+
public static void main(final String[] args) throws IOException {
18+
final HttpServer server =
19+
HttpServer.create(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 0), 0);
20+
server.createContext(
21+
"/hello",
22+
exchange -> {
23+
final byte[] body = "hello".getBytes(StandardCharsets.UTF_8);
24+
exchange.sendResponseHeaders(200, body.length);
25+
try (OutputStream out = exchange.getResponseBody()) {
26+
out.write(body);
27+
}
28+
});
29+
server.start();
30+
Files.write(
31+
Paths.get(args[0]),
32+
String.valueOf(server.getAddress().getPort()).getBytes(StandardCharsets.UTF_8));
33+
}
34+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package io.sentry.opentelemetry.agent
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import io.sentry.opentelemetry.agent.fixture.HelloServer
5+
import io.sentry.test.MockSentryServer
6+
import io.sentry.test.transaction
7+
import io.sentry.time.Deadline
8+
import io.sentry.time.JavaMonotonicTicker
9+
import java.io.File
10+
import java.net.URL
11+
import java.nio.file.Files
12+
import java.util.concurrent.TimeUnit
13+
import kotlin.test.AfterTest
14+
import kotlin.test.Test
15+
16+
/**
17+
* Runs an app with the built agent attached, in a child JVM, so the test's own JVM stays
18+
* uninstrumented. Instrumenting it would turn the test's HTTP calls and [MockSentryServer] itself
19+
* into spans.
20+
*/
21+
class AgentSmokeTest {
22+
private val server = MockSentryServer()
23+
private val workDir = Files.createTempDirectory("agent-smoke-test").toFile()
24+
private val appLog = File(workDir, "app.log")
25+
private var app: Process? = null
26+
27+
@AfterTest
28+
fun teardown() {
29+
app?.destroyForcibly()?.waitFor()
30+
server.close()
31+
if (appLog.exists()) {
32+
println(appLog.readText())
33+
}
34+
workDir.deleteRecursively()
35+
}
36+
37+
@Test
38+
fun `agent sends a transaction for an incoming request`() {
39+
val port = launchWithAgent(HelloServer::class.java)
40+
41+
URL("http://127.0.0.1:$port/hello").readText()
42+
43+
// The agent's BatchSpanProcessor exports every 5s.
44+
val envelope =
45+
server.awaitEnvelope(timeoutMillis = 30_000) {
46+
it.transaction()?.contexts?.trace?.operation == "http.server"
47+
}
48+
assertThat(envelope.header.sdkVersion!!.name).isEqualTo("sentry.java.opentelemetry.agent")
49+
}
50+
51+
private fun launchWithAgent(mainClass: Class<*>): Int {
52+
val portFile = File(workDir, "port")
53+
val process =
54+
ProcessBuilder(
55+
File(System.getProperty("java.home"), "bin/java").path,
56+
"-javaagent:${System.getProperty("sentry.test.agentJar")}",
57+
"-cp",
58+
File(mainClass.protectionDomain.codeSource.location.toURI()).path,
59+
mainClass.name,
60+
portFile.path,
61+
)
62+
.redirectErrorStream(true)
63+
.redirectOutput(appLog)
64+
.apply {
65+
environment()["SENTRY_DSN"] = server.dsn
66+
environment()["SENTRY_TRACES_SAMPLE_RATE"] = "1.0"
67+
environment()["OTEL_TRACES_EXPORTER"] = "none"
68+
environment()["OTEL_METRICS_EXPORTER"] = "none"
69+
environment()["OTEL_LOGS_EXPORTER"] = "none"
70+
}
71+
.start()
72+
app = process
73+
74+
val deadline = Deadline.after(JavaMonotonicTicker.getInstance(), 60, TimeUnit.SECONDS)
75+
while (true) {
76+
portFile
77+
.takeIf { it.exists() }
78+
?.readText()
79+
?.toIntOrNull()
80+
?.let {
81+
return it
82+
}
83+
check(process.isAlive) { "App exited with ${process.exitValue()}" }
84+
check(!deadline.hasPassed()) { "App did not start within 60s" }
85+
Thread.sleep(100)
86+
}
87+
}
88+
}

0 commit comments

Comments
 (0)