Skip to content

Commit 5a245f0

Browse files
authored
Merge pull request #5352 from getsentry/feat/queue-instrumentation-spring-boot-2
feat(spring): [Queue Instrumentation 41] Add Spring Boot 2 Kafka tracing
2 parents ce746d1 + a4b2016 commit 5a245f0

31 files changed

Lines changed: 1822 additions & 0 deletions

File tree

.cursor/rules/overview_dev.mdc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ Use the `fetch_rules` tool to include these rules when working on specific areas
6666
- `SentryMetricsEvent`, `SentryMetricsEvents`
6767
- `SentryOptions.getMetrics()`, `beforeSend` callback
6868

69+
- **`queues`**: Use when working with:
70+
- Sentry Queues product data or messaging span conventions
71+
- Queue tracing spans/transactions (`queue.publish`, `queue.process`)
72+
- `enableQueueTracing` option and `sentry.enable-queue-tracing`
73+
- Kafka instrumentation (`sentry-kafka`, `SentryKafkaProducer`, `SentryKafkaConsumerTracing`)
74+
- Spring Kafka queue auto-instrumentation and `SentryKafkaRecordInterceptor`
75+
- Messaging span data (`messaging.system`, `messaging.destination.name`, receive latency, retry count)
76+
- `sentry-task-enqueued-time` header and distributed trace propagation through queues
77+
6978
- **`continuous_profiling_jvm`**: Use when working with:
7079
- JVM continuous profiling (`sentry-async-profiler` module)
7180
- `IContinuousProfiler`, `JavaContinuousProfiler`
@@ -118,6 +127,7 @@ Use the `fetch_rules` tool to include these rules when working on specific areas
118127
- System test/e2e/sample → `e2e_tests`
119128
- Feature flag/addFeatureFlag/flag evaluation → `feature_flags`
120129
- Metrics/count/distribution/gauge → `metrics`
130+
- Queues/queue tracing/Kafka/Spring Kafka/queue.publish/queue.process/enableQueueTracing/messaging spans → `queues`
121131
- PR/pull request/stacked PR/stack → `pr`
122132
- JVM continuous profiling/async-profiler/JFR/ProfileChunk → `continuous_profiling_jvm`
123133
- Android continuous profiling/AndroidProfiler/frame metrics/method tracing → no dedicated rule yet; inspect the code directly

.cursor/rules/queues.mdc

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
alwaysApply: false
3+
description: Sentry Queues module and Java SDK queue tracing
4+
---
5+
# Sentry Queues and Java SDK Queue Tracing
6+
7+
## Product model
8+
9+
Sentry Queues is built from tracing data. SDKs mark queue work with queue-specific span operations and messaging span data so Sentry can identify producers, consumers, destinations, latency, and failures.
10+
11+
The important concepts are:
12+
- `queue.publish`: a span for enqueueing/publishing a message to a queue or topic.
13+
- `queue.process`: a transaction for processing a dequeued message.
14+
- Messaging span data, especially:
15+
- `messaging.system` (for example `kafka`)
16+
- `messaging.destination.name` (queue/topic name)
17+
- `messaging.message.id`
18+
- `messaging.message.retry.count`
19+
- `messaging.message.body.size`
20+
- `messaging.message.envelope.size`
21+
- `messaging.message.receive.latency`
22+
- Distributed tracing headers (`sentry-trace` and `baggage`) link producer-side work to consumer-side processing.
23+
- Queue receive latency is the time a message spent waiting between publish/enqueue and processing. For Java Kafka, this comes from the `sentry-task-enqueued-time` header that the producer writes and the consumer reads.
24+
25+
The Queues UI is not backed by a separate Java event type. The Java SDK contributes data through spans/transactions with the expected operations, trace context, statuses, and messaging attributes.
26+
27+
## Java SDK implementation
28+
29+
Queue tracing is opt-in. `SentryOptions.isEnableQueueTracing()` defaults to `false` and can be enabled with `setEnableQueueTracing(true)` or external config key `enable-queue-tracing` (`sentry.enable-queue-tracing` in Spring Boot). Captured queue spans/transactions still depend on tracing being enabled and sampled.
30+
31+
Kafka support lives in `sentry-kafka`:
32+
- `SentryKafkaProducer.wrap(Producer)` wraps Kafka `Producer.send(...)` calls.
33+
- Creates a `queue.publish` child span when there is an active span.
34+
- Sets `messaging.system=kafka` and `messaging.destination.name=<topic>`.
35+
- Injects `sentry-trace`, `baggage`, and `sentry-task-enqueued-time` headers.
36+
- Still injects tracing/enqueued-time headers when queue tracing is enabled but there is no active span, so background producers can link to consumers.
37+
- Finishes the span from the Kafka callback with `OK` or `INTERNAL_ERROR`.
38+
- `SentryKafkaConsumerTracing.withTracing(record, callback)` is the manual raw-Kafka consumer helper.
39+
- Forks root scopes for the processing lifecycle and makes them current.
40+
- Continues the trace from Kafka headers.
41+
- Starts a `queue.process` transaction bound to scope when tracing is enabled.
42+
- Sets Kafka messaging data, body size, retry count, and receive latency when available.
43+
- Finishes with `OK` or `INTERNAL_ERROR` and never lets instrumentation failures break customer processing.
44+
45+
Spring Kafka support lives in `sentry-spring`, `sentry-spring-jakarta`, and `sentry-spring-7`:
46+
- `SentryKafkaProducerBeanPostProcessor` installs a producer post-processor on `DefaultKafkaProducerFactory` and wraps created producers with `SentryKafkaProducer.wrap(...)`.
47+
- `SentryKafkaConsumerBeanPostProcessor` installs `SentryKafkaRecordInterceptor` on listener container factories.
48+
- `SentryKafkaRecordInterceptor` starts/finishes `queue.process` transactions around listener processing, continues traces from headers, forks scopes for the record lifecycle, and preserves any existing delegate interceptor.
49+
- Spring Boot auto-configuration registers both post-processors only when Spring Kafka and `sentry-kafka` are present and `sentry.enable-queue-tracing=true`.
50+
- Spring Boot queue auto-configuration is disabled when Sentry OpenTelemetry integration classes are present to avoid duplicate Kafka instrumentation.
51+
52+
## Trace origins and suppression
53+
54+
Queue instrumentation sets span origins so it can be identified and suppressed with `ignoredSpanOrigins`:
55+
- Raw Kafka producer: `auto.queue.kafka.producer`
56+
- Raw Kafka consumer helper: `manual.queue.kafka.consumer`
57+
- Spring Kafka producer: `auto.queue.spring.kafka.producer`, `auto.queue.spring_jakarta.kafka.producer`, `auto.queue.spring7.kafka.producer`
58+
- Spring Kafka consumer: `auto.queue.spring.kafka.consumer`, `auto.queue.spring_jakarta.kafka.consumer`, `auto.queue.spring7.kafka.consumer`
59+
60+
## Files to inspect when changing queue tracing
61+
62+
- Core option and conventions:
63+
- `sentry/src/main/java/io/sentry/SentryOptions.java`
64+
- `sentry/src/main/java/io/sentry/ExternalOptions.java`
65+
- `sentry/src/main/java/io/sentry/SpanDataConvention.java`
66+
- Raw Kafka:
67+
- `sentry-kafka/src/main/java/io/sentry/kafka/SentryKafkaProducer.java`
68+
- `sentry-kafka/src/main/java/io/sentry/kafka/SentryKafkaConsumerTracing.java`
69+
- `sentry-kafka/src/test/kotlin/io/sentry/kafka/*Test.kt`
70+
- Spring Kafka:
71+
- `sentry-spring*/src/main/java/io/sentry/**/kafka/*`
72+
- `sentry-spring*/src/test/kotlin/io/sentry/**/kafka/*Test.kt`
73+
- `sentry-spring-boot*/src/main/java/io/sentry/**/SentryAutoConfiguration.java`
74+
- `sentry-spring-boot*/src/test/kotlin/io/sentry/**/SentryKafkaAutoConfigurationTest.kt`
75+
76+
## Related rules
77+
78+
Also fetch:
79+
- `options` when changing `enableQueueTracing` or configuration surfaces.
80+
- `scopes` when changing consumer scope forking/lifecycle.
81+
- `opentelemetry` when changing coexistence with OTel auto-instrumentation.
82+
- `api` when changing public Kafka APIs or option methods.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Features
66

7+
- Add Kafka queue tracing for Spring Boot 2 ([#5352](https://github.com/getsentry/sentry-java/pull/5352))
78
- Add Kafka queue tracing for Spring Boot 4 ([#5348](https://github.com/getsentry/sentry-java/pull/5348))
89
- Add `sentry-kafka` module for Kafka queue instrumentation without Spring ([#5288](https://github.com/getsentry/sentry-java/pull/5288))
910
- Add Kafka queue tracing for Spring Boot 3 ([#5254](https://github.com/getsentry/sentry-java/pull/5254)), ([#5255](https://github.com/getsentry/sentry-java/pull/5255)), ([#5256](https://github.com/getsentry/sentry-java/pull/5256))

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ springboot3-starter-security = { module = "org.springframework.boot:spring-boot-
183183
springboot3-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-jdbc", version.ref = "springboot3" }
184184
springboot3-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springboot3" }
185185
springboot3-starter-cache = { module = "org.springframework.boot:spring-boot-starter-cache", version.ref = "springboot3" }
186+
spring-kafka2 = { module = "org.springframework.kafka:spring-kafka", version = "2.8.11" }
186187
spring-kafka3 = { module = "org.springframework.kafka:spring-kafka", version = "3.3.5" }
187188
spring-kafka4 = { module = "org.springframework.kafka:spring-kafka" }
188189
kafka-clients = { module = "org.apache.kafka:kafka-clients", version = "3.8.1" }

sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ dependencies {
5555
implementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentlessSpring)
5656
implementation(projects.sentryAsyncProfiler)
5757

58+
// kafka
59+
implementation(libs.spring.kafka2)
60+
implementation(projects.sentryKafka)
61+
5862
// database query tracing
5963
implementation(projects.sentryJdbc)
6064
runtimeOnly(libs.hsqldb)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.sentry.samples.spring.boot.queues.kafka;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
import org.springframework.context.annotation.Profile;
6+
import org.springframework.kafka.annotation.KafkaListener;
7+
import org.springframework.stereotype.Component;
8+
9+
@Component
10+
@Profile("kafka")
11+
public class KafkaConsumer {
12+
13+
private static final Logger logger = LoggerFactory.getLogger(KafkaConsumer.class);
14+
15+
@KafkaListener(topics = "sentry-topic", groupId = "sentry-sample-group")
16+
public void listen(String message) {
17+
logger.info("Received message: {}", message);
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.sentry.samples.spring.boot.queues.kafka;
2+
3+
import org.springframework.context.annotation.Profile;
4+
import org.springframework.kafka.core.KafkaTemplate;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.RequestMapping;
7+
import org.springframework.web.bind.annotation.RequestParam;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
@RestController
11+
@Profile("kafka")
12+
@RequestMapping("/kafka")
13+
public class KafkaController {
14+
15+
private final KafkaTemplate<String, String> kafkaTemplate;
16+
17+
public KafkaController(KafkaTemplate<String, String> kafkaTemplate) {
18+
this.kafkaTemplate = kafkaTemplate;
19+
}
20+
21+
@GetMapping("/produce")
22+
String produce(@RequestParam(defaultValue = "hello from sentry!") String message) {
23+
kafkaTemplate.send("sentry-topic", message);
24+
return "Message sent: " + message;
25+
}
26+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Kafka — activate with: --spring.profiles.active=kafka
2+
sentry.enable-queue-tracing=true
3+
4+
spring.kafka.bootstrap-servers=localhost:9092
5+
spring.kafka.consumer.group-id=sentry-sample-group
6+
spring.kafka.consumer.auto-offset-reset=earliest
7+
spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer
8+
spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer
9+
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
10+
spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer
11+
12+
logging.level.org.apache.kafka=warn
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.sentry.systemtest
2+
3+
import io.sentry.systemtest.util.TestHelper
4+
import kotlin.test.Test
5+
import kotlin.test.assertEquals
6+
import org.junit.Before
7+
8+
class KafkaOtelCoexistenceSystemTest {
9+
lateinit var testHelper: TestHelper
10+
11+
@Before
12+
fun setup() {
13+
testHelper = TestHelper("http://localhost:8080")
14+
testHelper.reset()
15+
}
16+
17+
@Test
18+
fun `Sentry Kafka integration is suppressed when OTel is active`() {
19+
val restClient = testHelper.restClient
20+
21+
restClient.produceKafkaMessage("otel-coexistence-test")
22+
assertEquals(200, restClient.lastKnownStatusCode)
23+
24+
testHelper.ensureTransactionReceived { transaction, _ ->
25+
transaction.transaction == "GET /kafka/produce" &&
26+
transaction.sdk?.integrationSet?.contains("SpringKafka") != true &&
27+
transaction.spans.any { span ->
28+
span.op == "queue.publish" &&
29+
span.origin == "auto.opentelemetry" &&
30+
span.data?.get("messaging.system") == "kafka"
31+
}
32+
}
33+
34+
testHelper.ensureTransactionReceived { transaction, _ ->
35+
transaction.contexts.trace?.operation == "queue.process" &&
36+
transaction.contexts.trace?.origin == "auto.opentelemetry" &&
37+
transaction.contexts.trace?.data?.get("messaging.system") == "kafka" &&
38+
transaction.sdk?.integrationSet?.contains("SpringKafka") != true
39+
}
40+
}
41+
}

sentry-samples/sentry-samples-spring-boot-opentelemetry/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ dependencies {
5353
implementation(projects.sentryAsyncProfiler)
5454
implementation(libs.otel)
5555

56+
// kafka
57+
implementation(libs.spring.kafka2)
58+
implementation(projects.sentryKafka)
59+
5660
// database query tracing
5761
implementation(projects.sentryJdbc)
5862
runtimeOnly(libs.hsqldb)

0 commit comments

Comments
 (0)