Tracing¶
Your end-to-end test just failed. Now what?
You stare at a stack trace that says "expected message not found within timeout". You dig through application logs. You check Kafka topics. You wonder if the HTTP request even reached the controller. Was it a database error? A serialization issue? A Kafka consumer that silently died?
What if your test failure told you exactly what happened inside your application?
═══════════════════════════════════════════════════════════════════════════════
EXECUTION TRACE (Call Chain)
═══════════════════════════════════════════════════════════════════════════════
✓ POST (377ms)
✓ POST /api/product/create (361ms)
✓ ProductController.create (141ms)
✓ ProductCreator.create (0ms)
✓ KafkaProducer.send (137ms)
✓ orders.created publish (81ms)
✗ orders.created process (82ms) ← FAILURE POINT
That's Stove tracing. When a test fails, you see the entire call chain of your application, powered by OpenTelemetry: every controller method, every database query, every Kafka message, every HTTP call, with timing and the exact point of failure. It's a unique feature.
What You Get¶
When tracing is enabled, every test failure comes with the full story:
STOVE EXECUTION REPORT
═══════════════════════════════════════════════════════════════════════════════
TIMELINE
────────
14:45:38.439 ✓ PASSED [HTTP] POST /api/product/create
14:45:38.472 ✗ FAILED [Kafka] shouldBePublished<ProductCreatedEvent>
SYSTEM SNAPSHOTS
────────────────
KAFKA
Consumed: 0
Produced: 1
Failed: 1
[0] topic: orders.created
reason: Something went wrong
═══════════════════════════════════════════════════════════════════════════════
EXECUTION TRACE (Call Chain)
═══════════════════════════════════════════════════════════════════════════════
✓ POST (377ms)
✓ POST /api/product/create (361ms)
✓ ProductController.create (141ms)
✓ ProductCreator.create (0ms)
✓ KafkaProducer.send (137ms)
✓ orders.created publish (81ms)
✗ orders.created process (82ms) ← FAILURE POINT
Everything is automatic:
- Traces start and end with each test
- W3C
traceparentheaders are injected into HTTP requests - Trace headers are injected into Kafka messages
- Trace metadata is injected into gRPC calls
- All spans are correlated back to the originating test
- Failure reports are enriched with the execution trace
When failures include exceptions, you see those too:
✗ PaymentGateway.charge [80ms] ⚠ FAILURE POINT
├── Exception: PaymentDeclinedException
│ Message: Card declined
│ at PaymentGateway.charge(PaymentGateway.kt:42)
Successful traces render as clean hierarchical trees:
✓ OrderController.createOrder [100ms]
├── ✓ OrderService.processOrder [95ms]
│ ├── ✓ UserRepository.findById [10ms]
│ │ └── db.system: postgresql
│ └── ✓ PaymentClient.charge [65ms]
│ └── http.url: https://payment.api/charge
Summary: 4 spans, 0 failures, total: 100ms
Setup¶
Two steps. That's it.
Step 1: Enable tracing in your Stove config¶
Stove()
.with {
tracing {
enableSpanReceiver()
}
// ... your other systems (http, kafka, etc.)
}
.run()
Step 2: Attach the OpenTelemetry agent in your build¶
plugins {
id("com.trendyol.stove.tracing") version "<stove-version>"
}
stoveTracing {
serviceName.set("my-service")
}
The plugin is published to Maven Central. Add mavenCentral() to your pluginManagement repositories if not already present.
Copy StoveTracingConfiguration.kt to your project's buildSrc/src/main/kotlin/ directory, then add to your build.gradle.kts:
Both approaches handle everything: downloading the OpenTelemetry Java Agent, configuring JVM arguments, attaching the agent to your test tasks, and dynamically assigning ports so parallel test runs don't conflict.
That's all you need
Now write your tests as usual. When a test fails, you'll see the execution trace automatically. No code changes to your application required. The OpenTelemetry agent instruments 100+ libraries (Spring, JDBC, Kafka, gRPC, HTTP clients, Redis, MongoDB, and more) with zero code changes.
Dependencies¶
dependencies {
testImplementation("com.trendyol:stove-tracing:$stoveVersion")
testImplementation("com.trendyol:stove-extensions-kotest:$stoveVersion")
// or
testImplementation("com.trendyol:stove-extensions-junit:$stoveVersion")
}
Test Framework Extensions
StoveKotestExtension (stove-extensions-kotest) and StoveJUnitExtension (stove-extensions-junit) are separate packages that must be on your classpath. Kotest requires 6.1.3+; JUnit requires Jupiter 6.x if possible. For Kotest, add a kotest.properties file with kotest.framework.config.fqn=<your config class FQN>. See the Getting Started guide for details.
Zero-Effort Trace Propagation¶
You don't need to do anything special in your test code. Stove injects trace headers into every interaction automatically:
Every HTTP request gets a traceparent header. Every Kafka message gets trace headers. Every gRPC call gets trace metadata. Your application picks these up through the OpenTelemetry agent, and Stove collects the resulting spans, all without you writing a single line of tracing code.
Trace Validation DSL¶
Beyond automatic failure reports, you can actively query and assert on traces using the tracing { } DSL. This is useful when you want to verify how your application handled a request, not just that it did.
test("order processing should call payment service") {
stove {
http {
post<OrderResponse>("/orders", orderRequest) { response ->
response.status shouldBe "created"
}
}
tracing {
shouldContainSpan("OrderService.processOrder")
shouldContainSpan("PaymentClient.charge")
shouldNotHaveFailedSpans()
executionTimeShouldBeLessThan(500.milliseconds)
}
}
}
Span Assertions¶
Verify which operations happened (or didn't) during a test:
tracing {
shouldContainSpan("UserService.findById")
shouldContainSpanMatching { it.operationName.contains("Repository") }
shouldNotContainSpan("AdminService.delete")
shouldNotHaveFailedSpans()
shouldHaveFailedSpan("PaymentGateway.charge")
shouldHaveSpanWithAttribute("http.method", "GET")
shouldHaveSpanWithAttributeContaining("http.url", "/api/users")
}
Performance Assertions¶
Assert on execution timing and span counts:
tracing {
executionTimeShouldBeLessThan(500.milliseconds)
executionTimeShouldBeGreaterThan(10.milliseconds)
spanCountShouldBe(10)
spanCountShouldBeAtLeast(5)
spanCountShouldBeAtMost(20)
}
Debugging Helpers¶
When you need to understand what happened during a test, render the trace:
tracing {
println(renderTree()) // Hierarchical tree view
println(renderSummary()) // Compact summary
val failedSpans = getFailedSpans()
val totalDuration = getTotalDuration()
val span = findSpanByName("OrderService.process")
// Wait for spans to arrive before asserting (useful for async flows)
waitForSpans(expectedCount = 5, timeoutMs = 3000)
}
Real-World Example¶
Here's a realistic scenario: an HTTP request triggers order processing, which publishes a Kafka event, which is consumed and writes to the database.
test("should create order and notify downstream services") {
stove {
val orderId = UUID.randomUUID().toString()
// 1. Create order via HTTP
http {
post<OrderResponse>("/orders", CreateOrderRequest(orderId, amount = 99.99)) { response ->
response.status shouldBe "created"
}
}
// 2. Verify Kafka event was published
kafka {
shouldBePublished<OrderCreatedEvent>(atLeastIn = 10.seconds) {
actual.orderId == orderId
}
}
// 3. Verify database state
postgresql {
shouldQuery<Order>("SELECT * FROM orders WHERE id = '$orderId'") { orders ->
orders.size shouldBe 1
orders.first().status shouldBe "CREATED"
}
}
// 4. Verify the execution flow
tracing {
shouldContainSpan("OrderController.create")
shouldContainSpan("OrderService.processOrder")
shouldContainSpan("orders.created publish")
shouldNotHaveFailedSpans()
}
}
}
If any step fails, the trace tree shows you exactly where and why:
✓ POST (250ms)
✓ POST /orders (245ms)
✓ OrderController.create [120ms]
├── ✓ OrderService.processOrder [115ms]
│ ├── ✓ INSERT INTO orders [15ms]
│ │ └── db.system: postgresql
│ └── ✓ KafkaProducer.send [90ms]
│ └── ✓ orders.created publish [45ms]
│ └── ✓ orders.created process [40ms]
│ └── ✓ UPDATE orders SET status='CREATED' [8ms]
Summary: 8 spans, 0 failures, total: 250ms
Working example
For a complete working project with tracing, see the spring-showcase recipe.
Configuration Reference¶
Stove Test Config¶
Configure tracing behavior in your Stove setup:
tracing {
enableSpanReceiver() // Required: starts the span receiver
spanCollectionTimeout(10.seconds) // How long to wait for spans (default: 5s)
maxSpansPerTrace(2000) // Cap spans per trace (default: 1000)
spanFilter { span -> // Filter which spans are collected
!span.operationName.contains("health-check")
}
}
| Option | Default | Description |
|---|---|---|
enableSpanReceiver(port?) |
Port from STOVE_TRACING_PORT env or 4317 |
Starts the OTLP gRPC receiver |
spanCollectionTimeout |
5.seconds |
How long to wait for spans when building failure reports |
maxSpansPerTrace |
1000 |
Maximum spans stored per trace (prevents memory issues) |
spanFilter |
Accept all | Predicate to filter which spans are collected |
Gradle Plugin¶
The Stove Tracing Gradle plugin configures the OpenTelemetry Java Agent for your test tasks. It is published to Maven Central.
Add mavenCentral() to your pluginManagement repositories:
Then apply the plugin:
For snapshot versions, also add the Maven Central snapshot repository:
// settings.gradle.kts
pluginManagement {
repositories {
mavenCentral()
maven("https://central.sonatype.com/repository/maven-snapshots")
gradlePluginPortal()
}
}
Configure the plugin in your build.gradle.kts:
stoveTracing {
serviceName.set("my-service")
testTaskNames.set(listOf("integrationTest")) // Only apply to specific tasks
disabledInstrumentations.set(listOf("jdbc")) // Exclude noisy instrumentations
}
| Option | Default | Description |
|---|---|---|
serviceName |
"stove-traced-app" |
Service name shown in traces |
enabled |
true |
Toggle tracing on/off |
protocol |
"grpc" |
OTLP protocol (currently only grpc is supported) |
testTaskNames |
[] |
Apply only to specific test tasks (empty = all) |
otelAgentVersion |
"2.24.0" |
OpenTelemetry Java Agent version |
captureHttpHeaders |
true |
Include HTTP headers in spans |
captureExperimentalTelemetry |
true |
Enable experimental HTTP telemetry |
disabledInstrumentations |
[] |
Instrumentations to disable (e.g., jdbc, hibernate) |
additionalInstrumentations |
[] |
Extra instrumentations to enable |
customAnnotations |
[] |
Custom annotation classes to instrument |
bspScheduleDelay |
100 |
Batch span processor delay in ms (lower = faster export) |
bspMaxBatchSize |
1 |
Batch size for span export (1 = immediate) |
Alternative: buildSrc copy-paste approach
If you prefer not to use the plugin, copy StoveTracingConfiguration.kt to your project's buildSrc/src/main/kotlin/ directory and use stoveTracing { ... } in your build script.
Alternative: Manual OTel agent setup
If you prefer full control, you can configure the agent manually:
// build.gradle.kts
val otelAgent by configurations.creating { isTransitive = false }
dependencies {
otelAgent("io.opentelemetry.javaagent:opentelemetry-javaagent:2.24.0")
}
tasks.test {
doFirst {
jvmArgs(
"-javaagent:${otelAgent.singleFile.absolutePath}",
"-Dotel.traces.exporter=otlp",
"-Dotel.exporter.otlp.protocol=grpc",
"-Dotel.exporter.otlp.endpoint=http://localhost:4317",
"-Dotel.metrics.exporter=none",
"-Dotel.logs.exporter=none",
"-Dotel.service.name=my-service",
"-Dotel.propagators=tracecontext,baggage",
"-Dotel.traces.sampler=always_on",
"-Dotel.bsp.schedule.delay=100",
"-Dotel.bsp.max.export.batch.size=1",
"-Dotel.instrumentation.grpc.enabled=false"
)
}
}
Best Practices¶
- Just enable it. Tracing is automatic and low-overhead; there's no reason not to use it
- Use
tracing { }sparingly. The automatic failure reports cover most debugging needs; use the DSL only when you want to assert on the execution flow - Start with
shouldNotHaveFailedSpans(). The simplest assertion that catches unexpected errors - Filter noise. If you see too many spans, use
disabledInstrumentationsto exclude verbose libraries likejdbcorspring-scheduling - CI just works. Ports are dynamically assigned, so parallel test runs don't conflict
Works with Reporting
Tracing integrates seamlessly with Stove's Reporting system. When both are enabled, test failures include the execution report and the trace tree together, giving you the complete picture.
Troubleshooting¶
No trace in failure reports¶
- Ensure
stove-tracingis in your dependencies - Verify
enableSpanReceiver()is called in your Stove config - Verify the
com.trendyol.stove.tracingplugin is applied in yourbuild.gradle.kts - Look for "Stove tracing: Attached OTel agent" in test output
Too many spans¶
Use disabledInstrumentations to exclude noisy libraries:
stoveTracing {
serviceName.set("my-service")
disabledInstrumentations.set(listOf("jdbc", "hibernate", "spring-scheduling"))
}
Spans missing parent-child relationships¶
- Ensure trace context is propagated through async boundaries
- Check that the OTel agent version is compatible with your framework version