From 044ead9589ff749ce77887191d477c3f1a925b2c Mon Sep 17 00:00:00 2001 From: Nelson Osacky Date: Tue, 20 Oct 2026 20:02:05 +0200 Subject: [PATCH] test(opentelemetry): Add an end-to-end test of the OTLP event linking OtlpEndToEndTest captures a message, a log and a metric inside an OpenTelemetry span with OpenTelemetryOtlpEventProcessor installed, and checks what reaches MockSentryServer: the event carries the span's trace and span IDs, and the log and the metric arrive. It ports the OTLP-specific part of the console OTLP sample's system test, which never checked the link. The sample's other assertions are the console sample's, which SentryEndToEndTest covers. The sample has no system test left, so it comes off the system-test runner and the backend workflow, whose base row moves to the Spring Boot 4 OTLP sample, and drops the Kotlin plugin it only needed for the test. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/system-tests-backend.yml | 5 +- .../build.gradle.kts | 1 + .../src/test/kotlin/OtlpEndToEndTest.kt | 66 +++++++++++ .../build.gradle.kts | 41 ------- .../src/test/kotlin/io/sentry/DummyTest.kt | 12 -- .../ConsoleApplicationSystemTest.kt | 107 ------------------ test/system-test-runner.py | 1 - 7 files changed, 68 insertions(+), 165 deletions(-) create mode 100644 sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtlpEndToEndTest.kt delete mode 100644 sentry-samples/sentry-samples-console-otlp/src/test/kotlin/io/sentry/DummyTest.kt delete mode 100644 sentry-samples/sentry-samples-console-otlp/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index eb29d4c92da..49eb31ba68f 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -24,16 +24,13 @@ jobs: strategy: fail-fast: false matrix: - sample: [ "sentry-samples-console-otlp" ] + sample: [ "sentry-samples-spring-boot-4-otlp" ] agent: [ "false" ] agent-auto-init: [ "true" ] include: - sample: "sentry-samples-logback" agent: "false" agent-auto-init: "true" - - sample: "sentry-samples-spring-boot-4-otlp" - agent: "false" - agent-auto-init: "true" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/sentry-opentelemetry/sentry-opentelemetry-otlp/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-otlp/build.gradle.kts index ec240c681ae..2d2304fe39f 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-otlp/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-otlp/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { testImplementation(projects.sentryTestSupport) testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(libs.awaitility.kotlin) + testImplementation(libs.google.truth) testImplementation(libs.kotlin.test.junit) testImplementation(libs.mockito.kotlin) diff --git a/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtlpEndToEndTest.kt b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtlpEndToEndTest.kt new file mode 100644 index 00000000000..fb3db9731e8 --- /dev/null +++ b/sentry-opentelemetry/sentry-opentelemetry-otlp/src/test/kotlin/OtlpEndToEndTest.kt @@ -0,0 +1,66 @@ +package io.sentry.opentelemetry.otlp + +import com.google.common.truth.Truth.assertThat +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.trace.SdkTracerProvider +import io.sentry.Sentry +import io.sentry.test.MockSentryServer +import io.sentry.test.event +import io.sentry.test.hasMetric +import io.sentry.test.initForTest +import io.sentry.test.logs +import io.sentry.test.metrics +import kotlin.test.AfterTest +import kotlin.test.Test + +/** + * An app that exports its OpenTelemetry spans over OTLP and reports everything else to Sentry, as + * the console OTLP sample does. + */ +class OtlpEndToEndTest { + private val server = MockSentryServer() + private val openTelemetry = + OpenTelemetrySdk.builder().setTracerProvider(SdkTracerProvider.builder().build()).build() + + @AfterTest + fun teardown() { + Sentry.close() + openTelemetry.close() + server.close() + } + + @Test + fun `an event inside an OpenTelemetry span is linked to it`() { + initForTest { + it.dsn = server.dsn + it.logs.isEnabled = true + it.addEventProcessor(OpenTelemetryOtlpEventProcessor()) + } + + val span = openTelemetry.getTracer("otlp-end-to-end-test").spanBuilder("otelSpan").startSpan() + span.makeCurrent().use { + Sentry.captureMessage("this message is connected to the outerSpan") + Sentry.logger().error("Some error log message") + Sentry.metrics().count("invocations") + } + span.end() + Sentry.flush(5000) + + val event = + server + .awaitEnvelope { + it.event()?.message?.formatted == "this message is connected to the outerSpan" + } + .event()!! + // The span's trace goes over OTLP, so the event points to it rather than to a Sentry span. + val trace = event.contexts.trace!! + assertThat(trace.traceId.toString()).isEqualTo(span.spanContext.traceId) + assertThat(trace.spanId.toString()).isEqualTo(span.spanContext.spanId) + + val logs = server.awaitEnvelope { it.logs() != null }.logs()!! + assertThat(logs.items.map { it.body }).contains("Some error log message") + + val metrics = server.awaitEnvelope { it.metrics() != null }.metrics()!! + assertThat(metrics.hasMetric("invocations", "counter", 1.0)).isTrue() + } +} diff --git a/sentry-samples/sentry-samples-console-otlp/build.gradle.kts b/sentry-samples/sentry-samples-console-otlp/build.gradle.kts index 491583175ae..12fa00dddd7 100644 --- a/sentry-samples/sentry-samples-console-otlp/build.gradle.kts +++ b/sentry-samples/sentry-samples-console-otlp/build.gradle.kts @@ -1,12 +1,8 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { java application - alias(libs.plugins.kotlin.jvm) alias(libs.plugins.gradle.versions) alias(libs.plugins.shadow) - id("io.sentry.systemtest") } application { mainClass.set("io.sentry.samples.console.Main") } @@ -20,30 +16,12 @@ configure { targetCompatibility = JavaVersion.VERSION_17 } -tasks.withType().configureEach { - compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 -} - -tasks.withType().configureEach { - kotlin { - compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") - compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 - } -} - dependencies { implementation(projects.sentryOpentelemetry.sentryOpentelemetryOtlp) implementation(projects.sentryAsyncProfiler) implementation(libs.otel.semconv) implementation(libs.otel.semconv.incubating) implementation(libs.otel.exporter.logging) - - 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) @@ -62,22 +40,3 @@ tasks.jar { // Fix the startScripts task dependency tasks.startScripts { dependsOn(tasks.shadowJar) } - -tasks.register("systemTest").configure { - group = "verification" - description = "Runs the System tests" - - val test = project.extensions.getByType()["test"] - testClassesDirs = test.output.classesDirs - classpath = test.runtimeClasspath - - maxParallelForks = 1 - - 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-otlp/src/test/kotlin/io/sentry/DummyTest.kt b/sentry-samples/sentry-samples-console-otlp/src/test/kotlin/io/sentry/DummyTest.kt deleted file mode 100644 index 6f762b7e453..00000000000 --- a/sentry-samples/sentry-samples-console-otlp/src/test/kotlin/io/sentry/DummyTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -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-otlp/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console-otlp/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt deleted file mode 100644 index 1333fa50387..00000000000 --- a/sentry-samples/sentry-samples-console-otlp/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -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-otlp") - val process = - testHelper.launch( - jarFile, - mapOf( - "SENTRY_DSN" to testHelper.dsn, - "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 && testHelper.doesEventHaveFlag(event, "my-feature-flag", 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 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 received the message captured within the OTel span - testHelper.ensureErrorReceived { event -> - event.message?.formatted == "this message is connected to the outerSpan" - } - - // Verify we have breadcrumbs - testHelper.ensureErrorReceived { event -> - event.breadcrumbs?.any { breadcrumb -> - breadcrumb.message?.contains("Processed by") == true - } == true - } - - testHelper.ensureMetricsReceived { metricsEvents, sentryEnvelopeHeader -> - testHelper.doesContainMetric(metricsEvents, "countMetric", "counter", 1.0) && - testHelper.doesContainMetric(metricsEvents, "gaugeMetric", "gauge", 5.0) && - testHelper.doesContainMetric(metricsEvents, "distributionMetric", "distribution", 7.0) && - testHelper.doesContainMetric(metricsEvents, "invocations", "counter", 1.0) - } - - // Verify we received the log message captured within the OTel span - testHelper.ensureLogsReceived { logs, _ -> - testHelper.doesContainLogWithBody(logs, "Some error log message") - } - } -} diff --git a/test/system-test-runner.py b/test/system-test-runner.py index 3788194a025..91119338f00 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -826,7 +826,6 @@ def get_available_modules(self) -> List[ModuleConfig]: """Get list of all available test modules.""" return [ ModuleConfig("sentry-samples-spring-boot-4-otlp", "false", "true", "false"), - ModuleConfig("sentry-samples-console-otlp", "false", "true", "false"), ModuleConfig("sentry-samples-logback", "false", "true", "false"), ]