Skip to content

Commit 92bcefc

Browse files
committed
test(opentelemetry): Add an end-to-end test of the agentless OpenTelemetry setup
AgentlessEndToEndTest builds the OpenTelemetry SDK through its autoconfiguration, which loads Sentry's customizer, and checks what reaches MockSentryServer: events, a transaction traced through OpenTelemetry with its child span and profile, and metrics. It ports sentry-samples-console-opentelemetry-noagent's system test, which no CI job ran. The customizer only initializes Sentry when the environment asks it to, so the test task sets SENTRY_AUTO_INIT. These are the module's first tests, so it applies the Kotlin plugin and gets an API dump. The sample has no system test left, so it comes off the system-test runner and drops the Kotlin plugin it only needed for the test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent a202cd3 commit 92bcefc

7 files changed

Lines changed: 186 additions & 168 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
public final class io/sentry/opentelemetry/agent/AgentlessMarker {
2+
public fun <init> ()V
3+
}
4+
5+
public final class io/sentry/opentelemetry/agentless/BuildConfig {
6+
public static final field SENTRY_OPENTELEMETRY_AGENTLESS_SDK_NAME Ljava/lang/String;
7+
public static final field VERSION_NAME Ljava/lang/String;
8+
}
9+

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2+
13
plugins {
24
`java-library`
35
id("io.sentry.javadoc")
6+
alias(libs.plugins.kotlin.jvm)
47
alias(libs.plugins.buildconfig)
58
id("io.sentry.animalsniffer")
69
}
710

11+
tasks.named<KotlinCompile>("compileKotlin") {
12+
compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
13+
}
14+
815
dependencies {
916
api(projects.sentry)
1017
implementation(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap)
@@ -13,8 +20,17 @@ dependencies {
1320
api(libs.otel.semconv)
1421
api(libs.otel.semconv.incubating)
1522
api(libs.otel.extension.autoconfigure)
23+
24+
testImplementation(projects.sentryAsyncProfiler)
25+
testImplementation(projects.sentryTestSupport)
26+
testImplementation(kotlin(Config.kotlinStdLib))
27+
testImplementation(libs.kotlin.test.junit)
28+
testImplementation(libs.google.truth)
1629
}
1730

31+
// Sentry's OpenTelemetry customizer only initializes Sentry when the environment asks it to.
32+
tasks.test { environment("SENTRY_AUTO_INIT", "true") }
33+
1834
buildConfig {
1935
useJavaOutput()
2036
packageName("io.sentry.opentelemetry.agentless")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package io.sentry.opentelemetry.agentless
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import io.opentelemetry.api.GlobalOpenTelemetry
5+
import io.opentelemetry.sdk.OpenTelemetrySdk
6+
import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk
7+
import io.sentry.Breadcrumb
8+
import io.sentry.EventProcessor
9+
import io.sentry.Hint
10+
import io.sentry.Sentry
11+
import io.sentry.SentryEvent
12+
import io.sentry.SentryLevel
13+
import io.sentry.protocol.Message
14+
import io.sentry.test.MockSentryServer
15+
import io.sentry.test.event
16+
import io.sentry.test.hasFlag
17+
import io.sentry.test.hasMetric
18+
import io.sentry.test.hasMetricWithAttribute
19+
import io.sentry.test.metrics
20+
import io.sentry.test.profileChunk
21+
import io.sentry.test.transaction
22+
import java.util.concurrent.TimeUnit
23+
import kotlin.test.AfterTest
24+
import kotlin.test.Test
25+
26+
/**
27+
* Sets Sentry up the way an app without the agent does: the OpenTelemetry SDK's autoconfiguration
28+
* loads Sentry's customizer, which initializes Sentry and has it trace through OpenTelemetry.
29+
*/
30+
class AgentlessEndToEndTest {
31+
private val server = MockSentryServer()
32+
private var openTelemetry: OpenTelemetrySdk? = null
33+
34+
@AfterTest
35+
fun teardown() {
36+
openTelemetry?.close()
37+
GlobalOpenTelemetry.resetForTest()
38+
Sentry.close()
39+
server.close()
40+
SENTRY_PROPERTIES.forEach { System.clearProperty(it) }
41+
}
42+
43+
@Test
44+
fun `Sentry set up by OpenTelemetry reports events, a profiled transaction and metrics`() {
45+
// The customizer's Sentry.init reads these as external configuration.
46+
System.setProperty("sentry.dsn", server.dsn)
47+
System.setProperty("sentry.traces-sample-rate", "1.0")
48+
System.setProperty("sentry.profile-session-sample-rate", "1.0")
49+
System.setProperty("sentry.profile-lifecycle", "TRACE")
50+
// The profiler rejects a traces path with a dot anywhere in it, which the module's absolute
51+
// path can have.
52+
System.setProperty(
53+
"sentry.profiling-traces-dir-path",
54+
"${System.getProperty("java.io.tmpdir")}/sentry-agentless-end-to-end-test",
55+
)
56+
val openTelemetry =
57+
AutoConfiguredOpenTelemetrySdk.builder()
58+
.setResultAsGlobal()
59+
.addPropertiesSupplier {
60+
mapOf(
61+
"otel.logs.exporter" to "none",
62+
"otel.metrics.exporter" to "none",
63+
"otel.traces.exporter" to "none",
64+
)
65+
}
66+
.build()
67+
.openTelemetrySdk
68+
this.openTelemetry = openTelemetry
69+
70+
Sentry.configureScope {
71+
it.addEventProcessor(
72+
object : EventProcessor {
73+
override fun process(event: SentryEvent, hint: Hint): SentryEvent = event.apply {
74+
if ((level?.ordinal ?: 0) > SentryLevel.INFO.ordinal) {
75+
addBreadcrumb(Breadcrumb("Processed by the event processor"))
76+
}
77+
}
78+
}
79+
)
80+
}
81+
Sentry.withScope {
82+
it.level = SentryLevel.FATAL
83+
Sentry.captureMessage("Fatal message!")
84+
}
85+
Sentry.captureMessage("Some warning!", SentryLevel.WARNING)
86+
Sentry.addFeatureFlag("my-feature-flag", true)
87+
Sentry.setAttribute("user.type", "admin")
88+
Sentry.setAttribute("feature.version", 2)
89+
Sentry.metrics().count("countMetric")
90+
Sentry.metrics().gauge("gaugeMetric", 5.0)
91+
Sentry.metrics().distribution("distributionMetric", 7.0)
92+
Sentry.captureException(RuntimeException("Some error!"))
93+
Sentry.captureEvent(
94+
SentryEvent().apply {
95+
message = Message().apply { message = "Detailed event" }
96+
fingerprints = listOf("NewClientDebug")
97+
level = SentryLevel.DEBUG
98+
}
99+
)
100+
repeat(10) { i ->
101+
Sentry.captureEvent(
102+
SentryEvent().apply { message = Message().apply { message = "event $i" } }
103+
)
104+
}
105+
val transaction = Sentry.startTransaction("transaction name", "op")
106+
val span = transaction.startChild("child")
107+
Thread.sleep(100)
108+
span.finish()
109+
transaction.finish()
110+
Sentry.flush(5000)
111+
// Sentry exports OpenTelemetry spans in batches every 5 seconds.
112+
openTelemetry.sdkTracerProvider.forceFlush().join(5, TimeUnit.SECONDS)
113+
114+
val fatal =
115+
server.awaitEnvelope { it.event()?.message?.formatted == "Fatal message!" }.event()!!
116+
assertThat(fatal.level).isEqualTo(SentryLevel.FATAL)
117+
assertThat(fatal.breadcrumbs!!.map { it.message }).contains("Processed by the event processor")
118+
119+
val warning =
120+
server.awaitEnvelope { it.event()?.message?.formatted == "Some warning!" }.event()!!
121+
assertThat(warning.level).isEqualTo(SentryLevel.WARNING)
122+
123+
val error =
124+
server
125+
.awaitEnvelope { it.event()?.exceptions?.any { e -> e.value == "Some error!" } == true }
126+
.event()!!
127+
assertThat(error.hasFlag("my-feature-flag", true)).isTrue()
128+
129+
val detailed =
130+
server.awaitEnvelope { it.event()?.message?.message == "Detailed event" }.event()!!
131+
assertThat(detailed.fingerprints).containsExactly("NewClientDebug")
132+
assertThat(detailed.level).isEqualTo(SentryLevel.DEBUG)
133+
134+
repeat(10) { i -> server.awaitEnvelope { it.event()?.message?.message == "event $i" } }
135+
136+
val sent =
137+
server.awaitEnvelope { it.transaction()?.transaction == "transaction name" }.transaction()!!
138+
assertThat(sent.spans.map { it.op }).contains("child")
139+
val profilerId = sent.contexts.profile?.profilerId
140+
assertThat(profilerId).isNotNull()
141+
server.awaitEnvelope(timeoutMillis = 30_000) { it.profileChunk()?.profilerId == profilerId }
142+
143+
val metrics = server.awaitEnvelope { it.metrics() != null }.metrics()!!
144+
assertThat(metrics.hasMetric("countMetric", "counter", 1.0)).isTrue()
145+
assertThat(metrics.hasMetric("gaugeMetric", "gauge", 5.0)).isTrue()
146+
assertThat(metrics.hasMetric("distributionMetric", "distribution", 7.0)).isTrue()
147+
assertThat(metrics.hasMetricWithAttribute("countMetric", "user.type", "admin")).isTrue()
148+
assertThat(metrics.hasMetricWithAttribute("countMetric", "feature.version", 2)).isTrue()
149+
}
150+
151+
private companion object {
152+
val SENTRY_PROPERTIES =
153+
listOf(
154+
"sentry.dsn",
155+
"sentry.traces-sample-rate",
156+
"sentry.profile-session-sample-rate",
157+
"sentry.profile-lifecycle",
158+
"sentry.profiling-traces-dir-path",
159+
)
160+
}
161+
}
Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,15 @@
1-
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2-
31
plugins {
42
java
53
application
6-
alias(libs.plugins.kotlin.jvm)
74
alias(libs.plugins.gradle.versions)
85
alias(libs.plugins.shadow)
9-
id("io.sentry.systemtest")
106
}
117

128
application { mainClass.set("io.sentry.samples.console.Main") }
139

14-
tasks.withType<KotlinCompile>().configureEach {
15-
kotlin {
16-
compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict")
17-
}
18-
}
19-
2010
dependencies {
2111
implementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentless)
2212
implementation(projects.sentryAsyncProfiler)
23-
24-
testImplementation(kotlin(Config.kotlinStdLib))
25-
testImplementation(projects.sentry)
26-
testImplementation(projects.sentrySystemTestSupport)
27-
testImplementation(libs.kotlin.test.junit)
28-
testImplementation(libs.slf4j.api)
29-
testImplementation(libs.slf4j.jdk14)
3013
}
3114

3215
// Configure the Shadow JAR (executable JAR with all dependencies)
@@ -45,22 +28,3 @@ tasks.jar {
4528

4629
// Fix the startScripts task dependency
4730
tasks.startScripts { dependsOn(tasks.shadowJar) }
48-
49-
tasks.register<Test>("systemTest").configure {
50-
group = "verification"
51-
description = "Runs the System tests"
52-
53-
val test = project.extensions.getByType<SourceSetContainer>()["test"]
54-
testClassesDirs = test.output.classesDirs
55-
classpath = test.runtimeClasspath
56-
57-
maxParallelForks = 1
58-
59-
filter { includeTestsMatching("io.sentry.systemtest*") }
60-
}
61-
62-
tasks.named("test").configure {
63-
require(this is Test)
64-
65-
filter { excludeTestsMatching("io.sentry.systemtest.*") }
66-
}

‎sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/DummyTest.kt‎

Lines changed: 0 additions & 12 deletions
This file was deleted.

‎sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt‎

Lines changed: 0 additions & 119 deletions
This file was deleted.

0 commit comments

Comments
 (0)