Skip to content

Commit 65b18dd

Browse files
committed
test(kafka): Add an end-to-end test of the raw Kafka client's tracing
SentryKafkaEndToEndTest publishes a message through a wrapped KafkaProducer to an in-JVM KRaft broker, consumes it with SentryKafkaConsumerTracing, and checks what reaches MockSentryServer: the queue.publish span in the producing transaction, and a queue.process transaction with the consumer origin and the Kafka system that continues the producer's trace. It ports the Kafka part of the console sample's system test, which needed a Redpanda container. The broker comes from spring-kafka-test 3, which needs Java 17, so the test classpaths resolve Java 17 variants. The tests run on it anyway. With its core, profiling and Kafka parts ported, the console sample has no system test left, so it comes off the system-test runner and the backend workflow, whose base row moves to the console OTLP sample. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 332c74b commit 65b18dd

7 files changed

Lines changed: 108 additions & 193 deletions

File tree

‎.github/workflows/system-tests-backend.yml‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,10 @@ jobs:
2424
strategy:
2525
fail-fast: false
2626
matrix:
27-
sample: [ "sentry-samples-console" ]
27+
sample: [ "sentry-samples-console-otlp" ]
2828
agent: [ "false" ]
2929
agent-auto-init: [ "true" ]
3030
include:
31-
- sample: "sentry-samples-console-otlp"
32-
agent: "false"
33-
agent-auto-init: "true"
3431
- sample: "sentry-samples-spring-boot-4-otlp"
3532
agent: "false"
3633
agent-auto-init: "true"

‎sentry-kafka/build.gradle.kts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@ dependencies {
2828
// tests
2929
testImplementation(projects.sentryTestSupport)
3030
testImplementation(kotlin(Config.kotlinStdLib))
31+
testImplementation(libs.google.truth)
3132
testImplementation(libs.kotlin.test.junit)
3233
testImplementation(libs.mockito.kotlin)
3334
testImplementation(libs.mockito.core)
3435
testImplementation(libs.kafka.clients)
36+
// For an in-JVM broker. It brings no Spring application into the tests.
37+
testImplementation(libs.spring.kafka3.test)
3538
}
3639

3740
tasks.withType<JavaCompile>().configureEach {
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package io.sentry.kafka
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import io.sentry.Sentry
5+
import io.sentry.test.MockSentryServer
6+
import io.sentry.test.initForTest
7+
import io.sentry.test.transaction
8+
import java.time.Duration
9+
import java.util.Properties
10+
import kotlin.test.AfterTest
11+
import kotlin.test.Test
12+
import org.apache.kafka.clients.consumer.ConsumerConfig
13+
import org.apache.kafka.clients.consumer.ConsumerRecord
14+
import org.apache.kafka.clients.consumer.KafkaConsumer
15+
import org.apache.kafka.clients.producer.KafkaProducer
16+
import org.apache.kafka.clients.producer.ProducerConfig
17+
import org.apache.kafka.clients.producer.ProducerRecord
18+
import org.apache.kafka.common.serialization.StringDeserializer
19+
import org.apache.kafka.common.serialization.StringSerializer
20+
import org.springframework.kafka.test.EmbeddedKafkaKraftBroker
21+
22+
/**
23+
* Sends a message through the plain Kafka client to an in-JVM KRaft broker, so no Docker is needed,
24+
* and checks that both sides reach Sentry over the real HTTP transport.
25+
*/
26+
class SentryKafkaEndToEndTest {
27+
private val server = MockSentryServer()
28+
private val kafka = EmbeddedKafkaKraftBroker(1, 1, TOPIC).apply { afterPropertiesSet() }
29+
30+
@AfterTest
31+
fun teardown() {
32+
Sentry.close()
33+
kafka.destroy()
34+
server.close()
35+
}
36+
37+
@Test
38+
fun `a message is traced where it's published and where it's processed`() {
39+
initForTest {
40+
it.dsn = server.dsn
41+
it.tracesSampleRate = 1.0
42+
it.isEnableQueueTracing = true
43+
}
44+
45+
val transaction = Sentry.startTransaction("kafka-demo", "demo")
46+
transaction.makeCurrent().use {
47+
SentryKafkaProducer.wrap(KafkaProducer<String, String>(producerProperties())).use { producer
48+
->
49+
producer.send(ProducerRecord(TOPIC, "sentry-kafka message")).get()
50+
}
51+
}
52+
transaction.finish()
53+
54+
KafkaConsumer<String, String>(consumerProperties()).use { consumer ->
55+
consumer.subscribe(listOf(TOPIC))
56+
val record = pollOne(consumer)
57+
SentryKafkaConsumerTracing.withTracing(record, Runnable {})
58+
}
59+
60+
val producer =
61+
server.awaitEnvelope { it.transaction()?.transaction == "kafka-demo" }.transaction()!!
62+
assertThat(producer.spans.map { it.op }).contains("queue.publish")
63+
64+
val consumer =
65+
server
66+
.awaitEnvelope { it.transaction()?.contexts?.trace?.operation == "queue.process" }
67+
.transaction()!!
68+
val trace = consumer.contexts.trace!!
69+
assertThat(trace.origin).isEqualTo(SentryKafkaConsumerTracing.TRACE_ORIGIN)
70+
assertThat(trace.data).containsEntry("messaging.system", "kafka")
71+
// The producer's span puts the trace in the message's headers, and the consumer continues it.
72+
assertThat(trace.traceId).isEqualTo(producer.contexts.trace!!.traceId)
73+
}
74+
75+
private fun pollOne(consumer: KafkaConsumer<String, String>): ConsumerRecord<String, String> {
76+
repeat(20) {
77+
consumer.poll(Duration.ofMillis(500)).firstOrNull()?.let {
78+
return it
79+
}
80+
}
81+
error("No message arrived")
82+
}
83+
84+
private fun producerProperties(): Properties =
85+
Properties().apply {
86+
put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.brokersAsString)
87+
put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer::class.java.name)
88+
put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer::class.java.name)
89+
}
90+
91+
private fun consumerProperties(): Properties =
92+
Properties().apply {
93+
put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka.brokersAsString)
94+
put(ConsumerConfig.GROUP_ID_CONFIG, "sentry-kafka-end-to-end-test")
95+
put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest")
96+
put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer::class.java.name)
97+
put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer::class.java.name)
98+
}
99+
100+
private companion object {
101+
const val TOPIC = "sentry-topic"
102+
}
103+
}
Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
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.sentry)
2212
implementation(projects.sentryAsyncProfiler)
@@ -25,13 +15,6 @@ dependencies {
2515
implementation(libs.jcache)
2616
implementation(libs.caffeine.jcache)
2717
implementation(libs.kafka.clients)
28-
29-
testImplementation(kotlin(Config.kotlinStdLib))
30-
testImplementation(projects.sentry)
31-
testImplementation(projects.sentrySystemTestSupport)
32-
testImplementation(libs.kotlin.test.junit)
33-
testImplementation(libs.slf4j.api)
34-
testImplementation(libs.slf4j.jdk14)
3518
}
3619

3720
// Configure the Shadow JAR (executable JAR with all dependencies)
@@ -49,22 +32,3 @@ tasks.jar {
4932

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

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

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

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

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

‎test/system-test-runner.py‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,7 @@
6868

6969
KAFKA_CONTAINER_NAME = "sentry-java-system-test-kafka"
7070
KAFKA_BOOTSTRAP_SERVERS = "localhost:9092"
71-
KAFKA_BROKER_REQUIRED_MODULES = {
72-
"sentry-samples-console",
73-
}
71+
KAFKA_BROKER_REQUIRED_MODULES = set()
7472
KAFKA_PROFILE_REQUIRED_MODULES = set()
7573

7674
class ServerType(Enum):
@@ -828,7 +826,6 @@ def get_available_modules(self) -> List[ModuleConfig]:
828826
"""Get list of all available test modules."""
829827
return [
830828
ModuleConfig("sentry-samples-spring-boot-4-otlp", "false", "true", "false"),
831-
ModuleConfig("sentry-samples-console", "false", "true", "false"),
832829
ModuleConfig("sentry-samples-console-otlp", "false", "true", "false"),
833830
ModuleConfig("sentry-samples-console-opentelemetry-noagent", "false", "true", "false"),
834831
]

0 commit comments

Comments
 (0)