Skip to content

Commit ad49908

Browse files
Prove the restored trace context parents the outbox delivery
1 parent d167763 commit ad49908

3 files changed

Lines changed: 116 additions & 8 deletions

File tree

notify-spring-boot-outbox/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,16 @@
6868
<artifactId>micrometer-tracing-test</artifactId>
6969
<scope>test</scope>
7070
</dependency>
71+
<dependency>
72+
<groupId>io.micrometer</groupId>
73+
<artifactId>micrometer-tracing-bridge-otel</artifactId>
74+
<scope>test</scope>
75+
</dependency>
76+
<dependency>
77+
<groupId>io.opentelemetry</groupId>
78+
<artifactId>opentelemetry-sdk-testing</artifactId>
79+
<scope>test</scope>
80+
</dependency>
7181
<dependency>
7282
<groupId>org.springframework.boot</groupId>
7383
<artifactId>spring-boot-starter-test</artifactId>

notify-spring-boot-outbox/src/test/java/sk/solodev/notify/outbox/MicrometerOutboxTracePropagatorTest.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
class MicrometerOutboxTracePropagatorTest {
1818

1919
@Test
20-
void should_return_empty_when_no_span_is_active() {
20+
void returnsEmptyWhenNoSpanIsActive() {
2121
var tracer = new SimpleTracer();
2222
var propagator = new TestPropagator();
2323
var sut = new MicrometerOutboxTracePropagator(tracer, propagator);
@@ -28,7 +28,7 @@ void should_return_empty_when_no_span_is_active() {
2828
}
2929

3030
@Test
31-
void should_capture_trace_context_when_span_is_active() {
31+
void capturesTheTraceContextWhenASpanIsActive() {
3232
var tracer = new SimpleTracer();
3333
var propagator = new TestPropagator();
3434
var sut = new MicrometerOutboxTracePropagator(tracer, propagator);
@@ -47,7 +47,7 @@ void should_capture_trace_context_when_span_is_active() {
4747
}
4848

4949
@Test
50-
void should_run_delivery_and_return_its_value_with_restored_context() {
50+
void runsTheDeliveryAndReturnsItsValueWithARestoredContext() {
5151
var tracer = new SimpleTracer();
5252
var propagator = new TestPropagator();
5353
var sut = new MicrometerOutboxTracePropagator(tracer, propagator);
@@ -66,7 +66,7 @@ void should_run_delivery_and_return_its_value_with_restored_context() {
6666
}
6767

6868
@Test
69-
void should_run_delivery_when_trace_context_is_null() {
69+
void runsTheDeliveryWhenThereIsNoStoredContext() {
7070
var tracer = new SimpleTracer();
7171
var propagator = new TestPropagator();
7272
var sut = new MicrometerOutboxTracePropagator(tracer, propagator);
@@ -77,7 +77,7 @@ void should_run_delivery_when_trace_context_is_null() {
7777
}
7878

7979
@Test
80-
void should_run_delivery_even_with_malformed_context() {
80+
void runsTheDeliveryEvenWhenTheStoredContextIsMalformed() {
8181
var tracer = new SimpleTracer();
8282
var propagator = new TestPropagator();
8383
var sut = new MicrometerOutboxTracePropagator(tracer, propagator);
@@ -88,7 +88,7 @@ void should_run_delivery_even_with_malformed_context() {
8888
}
8989

9090
@Test
91-
void should_propagate_trace_id_through_round_trip() {
91+
void carriesTheTraceIdThroughACaptureAndRestoreRoundTrip() {
9292
var tracer = new SimpleTracer();
9393
var propagator = new TestPropagator();
9494
var sut = new MicrometerOutboxTracePropagator(tracer, propagator);
@@ -107,7 +107,7 @@ void should_propagate_trace_id_through_round_trip() {
107107

108108
// Verify delivery runs (the TestSpan created by our propagator isn't recognized by
109109
// SimpleTracer as "current", which is a limitation of the test infrastructure.
110-
// The actual parent-child link verification happens in the end-to-end test in a later task.)
110+
// The actual parent-child link verification happens in OutboxTracePropagationOtelTest.)
111111
var deliveryRan = new AtomicBoolean(false);
112112
sut.withRestoredContext(captured, () -> {
113113
deliveryRan.set(true);
@@ -120,7 +120,7 @@ void should_propagate_trace_id_through_round_trip() {
120120
}
121121

122122
@Test
123-
void should_restore_context_before_running_delivery() {
123+
void restoresTheContextBeforeRunningTheDelivery() {
124124
var tracer = new SimpleTracer();
125125
var propagator = new TestPropagator();
126126
var sut = new MicrometerOutboxTracePropagator(tracer, propagator);
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package sk.solodev.notify.outbox;
2+
3+
import io.micrometer.tracing.otel.bridge.OtelCurrentTraceContext;
4+
import io.micrometer.tracing.otel.bridge.OtelPropagator;
5+
import io.micrometer.tracing.otel.bridge.OtelTracer;
6+
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
7+
import io.opentelemetry.context.propagation.ContextPropagators;
8+
import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporter;
9+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
10+
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.Test;
13+
import sk.solodev.notify.NotificationRequest;
14+
import sk.solodev.notify.test.RecordingNotifier;
15+
import tools.jackson.databind.json.JsonMapper;
16+
17+
import java.time.Duration;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
21+
/**
22+
* Proves trace context propagation across the outbox boundary using a real OpenTelemetry SDK: the
23+
* delivery span created by {@link OutboxRelay} becomes a child of the request span that enqueued,
24+
* even though they run in different transactions and possibly different threads.
25+
*/
26+
class OutboxTracePropagationOtelTest {
27+
28+
record SampleRequest(String to, String body) implements NotificationRequest { }
29+
30+
private static final OutboxProperties PROPERTIES = new OutboxProperties(
31+
Duration.ofSeconds(5), 100, 7, Duration.ofSeconds(10), Duration.ofMinutes(10),
32+
"notification_outbox");
33+
34+
private InMemorySpanExporter spanExporter;
35+
private OtelTracer tracer;
36+
private MicrometerOutboxTracePropagator propagator;
37+
38+
@BeforeEach
39+
void setUp() {
40+
spanExporter = InMemorySpanExporter.create();
41+
var tracerProvider = SdkTracerProvider.builder()
42+
.addSpanProcessor(SimpleSpanProcessor.create(spanExporter))
43+
.build();
44+
var otelTracer = tracerProvider.get("test");
45+
var propagators = ContextPropagators.create(W3CTraceContextPropagator.getInstance());
46+
47+
tracer = new OtelTracer(otelTracer, new OtelCurrentTraceContext(), null);
48+
propagator = new MicrometerOutboxTracePropagator(tracer,
49+
new OtelPropagator(propagators, otelTracer));
50+
}
51+
52+
@Test
53+
void theDeliverySpanInheritsTheTraceIdFromTheEnqueuingRequest() {
54+
// Start a span representing the incoming request
55+
var requestSpan = tracer.nextSpan().name("incoming-request").start();
56+
String originalTraceId = requestSpan.context().traceId();
57+
58+
// Enqueue within the request span's scope
59+
OutboxEntry entry;
60+
var store = new RecordingOutboxStore();
61+
var enqueuer = new DefaultOutboxNotifier(store, JsonMapper.builder().build(),
62+
PROPERTIES, propagator);
63+
try (var _ = tracer.withSpan(requestSpan)) {
64+
enqueuer.enqueue(new SampleRequest("+421900123456", "hello"));
65+
} finally {
66+
requestSpan.end();
67+
}
68+
69+
// The entry should have captured the trace context
70+
entry = store.inserted.getFirst();
71+
assertThat(entry.traceContext()).isNotNull();
72+
73+
// Now deliver it through the relay
74+
var notifier = new RecordingNotifier();
75+
var relay = new OutboxRelay(notifier, new RecordingOutboxStore(entry),
76+
JsonMapper.builder().build(), PROPERTIES, propagator);
77+
78+
relay.poll();
79+
80+
// Export spans and find the delivery span
81+
var exportedSpans = spanExporter.getFinishedSpanItems();
82+
83+
// The request span and the delivery span should both be present
84+
assertThat(exportedSpans).hasSizeGreaterThanOrEqualTo(2);
85+
86+
// Find the delivery span (it's the one created after the request span ended)
87+
var deliverySpan = exportedSpans.stream()
88+
.filter(span -> !span.getSpanId().equals(requestSpan.context().spanId()))
89+
.findFirst()
90+
.orElseThrow(() -> new AssertionError("Delivery span not found"));
91+
92+
// The critical assertion: the delivery span must have the same trace ID as the request
93+
assertThat(deliverySpan.getTraceId())
94+
.as("Delivery span should inherit the request's trace ID")
95+
.isEqualTo(originalTraceId);
96+
}
97+
98+
}

0 commit comments

Comments
 (0)