Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.sentry.gradle

import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.PathSensitive
import org.gradle.api.tasks.PathSensitivity
import org.gradle.process.CommandLineArgumentProvider

/**
* Tells a test task's JVM where to find the built OpenTelemetry agent and the app to run with it.
*
* The test launches the app in a child JVM, so neither file is on the test classpath. Declaring
* them here makes them task inputs: the test reruns when either changes and depends on the tasks
* that build them.
*/
abstract class ChildJvmTestArguments : CommandLineArgumentProvider {
@get:InputFiles
@get:PathSensitive(PathSensitivity.NONE)
abstract val agentJar: ConfigurableFileCollection

@get:Classpath abstract val appClasspath: ConfigurableFileCollection

override fun asArguments(): List<String> = buildList {
add("-Dsentry.test.agentJar=${agentJar.singleFile.absolutePath}")
if (!appClasspath.isEmpty) {
add("-Dsentry.test.appClasspath=${appClasspath.asPath}")
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import io.sentry.gradle.ChildJvmTestArguments
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
Expand Down Expand Up @@ -211,19 +212,9 @@ tasks {

test {
jvmArgumentProviders.add(
objects.newInstance<AgentJarArgument>().apply {
agentJar.set(shadowJar.flatMap { it.archiveFile })
}
objects.newInstance<ChildJvmTestArguments>().apply { agentJar.from(shadowJar) }
)
}
}

// Tests launch apps with the built agent attached, so the agent jar is a test input.
abstract class AgentJarArgument : CommandLineArgumentProvider {
@get:InputFile @get:PathSensitive(PathSensitivity.NONE) abstract val agentJar: RegularFileProperty

override fun asArguments() =
listOf("-Dsentry.test.agentJar=${agentJar.get().asFile.absolutePath}")
}

tasks.named("distZip").configure { this.dependsOn("jar") }
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

/**
* An app for the agent to instrument. It only uses the JDK, so a test can run it with nothing but
* this class on the classpath, and it writes its port to the file named by the first argument.
* this class on the classpath, and it writes its port to the file named by {@code PORTFILE}.
*/
public final class HelloServer {
public static void main(final String[] args) throws IOException {
Expand All @@ -28,7 +28,7 @@ public static void main(final String[] args) throws IOException {
});
server.start();
Files.write(
Paths.get(args[0]),
Paths.get(System.getenv("PORTFILE")),
String.valueOf(server.getAddress().getPort()).getBytes(StandardCharsets.UTF_8));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@ package io.sentry.opentelemetry.agent

import com.google.common.truth.Truth.assertThat
import io.sentry.opentelemetry.agent.fixture.HelloServer
import io.sentry.test.ChildJvmApp
import io.sentry.test.MockSentryServer
import io.sentry.test.transaction
import io.sentry.time.Deadline
import io.sentry.time.JavaMonotonicTicker
import java.io.File
import java.net.URL
import java.nio.file.Files
import java.util.concurrent.TimeUnit
import kotlin.test.AfterTest
import kotlin.test.Test

Expand All @@ -20,25 +17,34 @@ import kotlin.test.Test
*/
class AgentSmokeTest {
private val server = MockSentryServer()
private val workDir = Files.createTempDirectory("agent-smoke-test").toFile()
private val appLog = File(workDir, "app.log")
private var app: Process? = null
private var app: ChildJvmApp? = null

@AfterTest
fun teardown() {
app?.destroyForcibly()?.waitFor()
app?.close()
server.close()
if (appLog.exists()) {
println(appLog.readText())
}
workDir.deleteRecursively()
}

@Test
fun `agent sends a transaction for an incoming request`() {
val port = launchWithAgent(HelloServer::class.java)
val app =
ChildJvmApp.start(
mainClass = HelloServer::class.java.name,
classpath =
File(HelloServer::class.java.protectionDomain.codeSource.location.toURI()).path,
jvmArgs = listOf("-javaagent:${System.getProperty("sentry.test.agentJar")}"),
env =
mapOf(
"SENTRY_DSN" to server.dsn,
"SENTRY_TRACES_SAMPLE_RATE" to "1.0",
"OTEL_TRACES_EXPORTER" to "none",
"OTEL_METRICS_EXPORTER" to "none",
"OTEL_LOGS_EXPORTER" to "none",
),
)
.also { app = it }

URL("http://127.0.0.1:$port/hello").readText()
URL("http://127.0.0.1:${app.awaitPort()}/hello").readText()

// The agent's BatchSpanProcessor exports every 5s.
val envelope =
Expand All @@ -47,42 +53,4 @@ class AgentSmokeTest {
}
assertThat(envelope.header.sdkVersion!!.name).isEqualTo("sentry.java.opentelemetry.agent")
}

private fun launchWithAgent(mainClass: Class<*>): Int {
val portFile = File(workDir, "port")
val process =
ProcessBuilder(
File(System.getProperty("java.home"), "bin/java").path,
"-javaagent:${System.getProperty("sentry.test.agentJar")}",
"-cp",
File(mainClass.protectionDomain.codeSource.location.toURI()).path,
mainClass.name,
portFile.path,
)
.redirectErrorStream(true)
.redirectOutput(appLog)
.apply {
environment()["SENTRY_DSN"] = server.dsn
environment()["SENTRY_TRACES_SAMPLE_RATE"] = "1.0"
environment()["OTEL_TRACES_EXPORTER"] = "none"
environment()["OTEL_METRICS_EXPORTER"] = "none"
environment()["OTEL_LOGS_EXPORTER"] = "none"
}
.start()
app = process

val deadline = Deadline.after(JavaMonotonicTicker.getInstance(), 60, TimeUnit.SECONDS)
while (true) {
portFile
.takeIf { it.exists() }
?.readText()
?.toIntOrNull()
?.let {
return it
}
check(process.isAlive) { "App exited with ${process.exitValue()}" }
check(!deadline.hasPassed()) { "App did not start within 60s" }
Thread.sleep(100)
}
}
}
53 changes: 53 additions & 0 deletions sentry-spring-boot-jakarta/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import io.sentry.gradle.ChildJvmTestArguments
import net.ltgt.gradle.errorprone.errorprone
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.springframework.boot.gradle.plugin.SpringBootPlugin
Expand Down Expand Up @@ -139,3 +140,55 @@ tasks.jar {
)
}
}

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

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

dependencies {
"agentFixtureImplementation"(platform(SpringBootPlugin.BOM_COORDINATES))
"agentFixtureImplementation"(projects.sentrySpringBootStarterJakarta)
"agentFixtureImplementation"(projects.sentryLogback)
"agentFixtureImplementation"(kotlin(Config.kotlinStdLib))
"agentFixtureImplementation"(libs.otel)
"agentFixtureImplementation"(libs.springboot3.starter.web)

otelAgentJar(projects.sentryOpentelemetry.sentryOpentelemetryAgent)
}

testing {
suites {
register<JvmTestSuite>("integrationTestOtelAgent") {
useJUnit()
dependencies {
implementation(projects.sentryTestSupport)
implementation(libs.kotlin.test.junit)
implementation(libs.google.truth)
}
targets.all {
testTask.configure {
jvmArgumentProviders.add(
objects.newInstance<ChildJvmTestArguments>().apply {
agentJar.from(otelAgentJar)
appClasspath.from(agentFixture.runtimeClasspath)
}
)
}
}
}
}
}

tasks.named("check") { dependsOn(testing.suites.named("integrationTestOtelAgent")) }
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.sentry.spring.boot.jakarta.agentfixture

import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.api.trace.Tracer
import io.sentry.Sentry
import org.slf4j.LoggerFactory
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.web.context.WebServerPortFileWriter
import org.springframework.context.annotation.Bean
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
open class AgentFixtureApplication {
@Bean open fun tracer(): Tracer = GlobalOpenTelemetry.get().getTracer("agent-fixture")
}

fun main(args: Array<String>) {
SpringApplication(AgentFixtureApplication::class.java)
.apply { addListeners(WebServerPortFileWriter()) }
.run(*args)
}

/** Reports to Sentry through every API an app with the agent uses, then fails the request. */
@RestController
open class PersonController(private val tracer: Tracer) {
private val logger = LoggerFactory.getLogger(PersonController::class.java)

@GetMapping("/person/{id}")
open fun person(@PathVariable("id") id: Long): String {
Sentry.addFeatureFlag("transaction-feature-flag", true)
val span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan()
try {
span.makeCurrent().use {
Sentry.setAttribute("user.type", "admin")
Sentry.setAttribute("feature.version", 2)
Sentry.setAttribute("debug.enabled", true)
Sentry.logger().warn("warn Sentry logging")
Sentry.logger().error("error Sentry logging")
Sentry.logger().info("hello %s %s", "there", "world!")
Sentry.addFeatureFlag("my-feature-flag", true)
val sentrySpan = Sentry.getSpan()!!.startChild("spanCreatedThroughSentryApi")
try {
logger.error("Trying person with id={}", id, RuntimeException("error while loading"))
throw IllegalArgumentException("Something went wrong [id=$id]")
} finally {
sentrySpan.finish()
}
}
} finally {
span.end()
}
}
}
Loading
Loading