16
16
17
17
package org .springframework .kafka .support .micrometer ;
18
18
19
+ import java .nio .charset .StandardCharsets ;
19
20
import java .util .Arrays ;
20
21
import java .util .Deque ;
21
22
import java .util .List ;
26
27
import java .util .concurrent .TimeUnit ;
27
28
import java .util .concurrent .TimeoutException ;
28
29
import java .util .concurrent .atomic .AtomicReference ;
30
+ import java .util .stream .StreamSupport ;
29
31
30
32
import io .micrometer .common .KeyValues ;
31
33
import io .micrometer .core .instrument .MeterRegistry ;
56
58
import org .apache .kafka .common .errors .InvalidTopicException ;
57
59
import org .apache .kafka .common .header .Header ;
58
60
import org .apache .kafka .common .header .Headers ;
61
+ import org .apache .kafka .common .header .internals .RecordHeader ;
59
62
import org .junit .jupiter .api .Test ;
60
63
import reactor .core .publisher .Mono ;
61
64
77
80
import org .springframework .kafka .core .ProducerFactory ;
78
81
import org .springframework .kafka .listener .MessageListenerContainer ;
79
82
import org .springframework .kafka .listener .RecordInterceptor ;
83
+ import org .springframework .kafka .support .ProducerListener ;
80
84
import org .springframework .kafka .support .micrometer .KafkaListenerObservation .DefaultKafkaListenerObservationConvention ;
81
85
import org .springframework .kafka .support .micrometer .KafkaTemplateObservation .DefaultKafkaTemplateObservationConvention ;
82
86
import org .springframework .kafka .test .EmbeddedKafkaBroker ;
102
106
* @since 3.0
103
107
*/
104
108
@ SpringJUnitConfig
105
- @ EmbeddedKafka (topics = { ObservationTests .OBSERVATION_TEST_1 , ObservationTests .OBSERVATION_TEST_2 ,
109
+ @ EmbeddedKafka (topics = {ObservationTests .OBSERVATION_TEST_1 , ObservationTests .OBSERVATION_TEST_2 ,
106
110
ObservationTests .OBSERVATION_TEST_3 , ObservationTests .OBSERVATION_RUNTIME_EXCEPTION ,
107
- ObservationTests .OBSERVATION_ERROR }, partitions = 1 )
111
+ ObservationTests .OBSERVATION_ERROR , ObservationTests . OBSERVATION_TRACEPARENT_DUPLICATE }, partitions = 1 )
108
112
@ DirtiesContext
109
113
public class ObservationTests {
110
114
@@ -122,18 +126,21 @@ public class ObservationTests {
122
126
123
127
public final static String OBSERVATION_ERROR_MONO = "observation.error.mono" ;
124
128
129
+ public final static String OBSERVATION_TRACEPARENT_DUPLICATE = "observation.traceparent.duplicate" ;
130
+
125
131
@ Test
126
132
void endToEnd (@ Autowired Listener listener , @ Autowired KafkaTemplate <Integer , String > template ,
127
133
@ Autowired SimpleTracer tracer , @ Autowired KafkaListenerEndpointRegistry rler ,
128
134
@ Autowired MeterRegistry meterRegistry , @ Autowired EmbeddedKafkaBroker broker ,
129
135
@ Autowired KafkaListenerEndpointRegistry endpointRegistry , @ Autowired KafkaAdmin admin ,
130
136
@ Autowired @ Qualifier ("customTemplate" ) KafkaTemplate <Integer , String > customTemplate ,
131
137
@ Autowired Config config )
132
- throws InterruptedException , ExecutionException , TimeoutException {
138
+ throws InterruptedException , ExecutionException , TimeoutException {
133
139
134
140
AtomicReference <SimpleSpan > spanFromCallback = new AtomicReference <>();
135
141
136
142
template .setProducerInterceptor (new ProducerInterceptor <>() {
143
+
137
144
@ Override
138
145
public ProducerRecord <Integer , String > onSend (ProducerRecord <Integer , String > record ) {
139
146
tracer .currentSpanCustomizer ().tag ("key" , "value" );
@@ -321,10 +328,10 @@ private void assertThatTemplateHasTimerWithNameAndTags(MeterRegistryAssert meter
321
328
322
329
meterRegistryAssert .hasTimerWithNameAndTags ("spring.kafka.template" ,
323
330
KeyValues .of ("spring.kafka.template.name" , "template" ,
324
- "messaging.operation" , "publish" ,
325
- "messaging.system" , "kafka" ,
326
- "messaging.destination.kind" , "topic" ,
327
- "messaging.destination.name" , destName )
331
+ "messaging.operation" , "publish" ,
332
+ "messaging.system" , "kafka" ,
333
+ "messaging.destination.kind" , "topic" ,
334
+ "messaging.destination.name" , destName )
328
335
.and (keyValues ));
329
336
}
330
337
@@ -333,12 +340,12 @@ private void assertThatListenerHasTimerWithNameAndTags(MeterRegistryAssert meter
333
340
334
341
meterRegistryAssert .hasTimerWithNameAndTags ("spring.kafka.listener" ,
335
342
KeyValues .of (
336
- "messaging.kafka.consumer.group" , consumerGroup ,
337
- "messaging.operation" , "receive" ,
338
- "messaging.source.kind" , "topic" ,
339
- "messaging.source.name" , destName ,
340
- "messaging.system" , "kafka" ,
341
- "spring.kafka.listener.id" , listenerId )
343
+ "messaging.kafka.consumer.group" , consumerGroup ,
344
+ "messaging.operation" , "receive" ,
345
+ "messaging.source.kind" , "topic" ,
346
+ "messaging.source.name" , destName ,
347
+ "messaging.system" , "kafka" ,
348
+ "spring.kafka.listener.id" , listenerId )
342
349
.and (keyValues ));
343
350
}
344
351
@@ -388,7 +395,7 @@ void observationRuntimeException(@Autowired ExceptionListener listener, @Autowir
388
395
void observationErrorException (@ Autowired ExceptionListener listener , @ Autowired SimpleTracer tracer ,
389
396
@ Autowired @ Qualifier ("throwableTemplate" ) KafkaTemplate <Integer , String > errorTemplate ,
390
397
@ Autowired KafkaListenerEndpointRegistry endpointRegistry )
391
- throws ExecutionException , InterruptedException , TimeoutException {
398
+ throws ExecutionException , InterruptedException , TimeoutException {
392
399
393
400
errorTemplate .send (OBSERVATION_ERROR , "testError" ).get (10 , TimeUnit .SECONDS );
394
401
assertThat (listener .latch5 .await (10 , TimeUnit .SECONDS )).isTrue ();
@@ -449,6 +456,63 @@ void kafkaAdminNotRecreatedIfBootstrapServersSameInProducerAndAdminConfig(
449
456
assertThat (template .getKafkaAdmin ()).isSameAs (kafkaAdmin );
450
457
}
451
458
459
+ @ Test
460
+ void verifyKafkaRecordSenderContextTraceParentHandling () {
461
+ String initialTraceParent = "traceparent-from-previous" ;
462
+ String updatedTraceParent = "traceparent-current" ;
463
+ ProducerRecord <Integer , String > record = new ProducerRecord <>("test-topic" , "test-value" );
464
+ record .headers ().add ("traceparent" , initialTraceParent .getBytes (StandardCharsets .UTF_8 ));
465
+
466
+ // Create the context and update the traceparent
467
+ KafkaRecordSenderContext context = new KafkaRecordSenderContext (
468
+ record ,
469
+ "test-bean" ,
470
+ () -> "test-cluster"
471
+ );
472
+ context .getSetter ().set (record , "traceparent" , updatedTraceParent );
473
+
474
+ Iterable <Header > traceparentHeaders = record .headers ().headers ("traceparent" );
475
+
476
+ List <String > headerValues = StreamSupport .stream (traceparentHeaders .spliterator (), false )
477
+ .map (header -> new String (header .value (), StandardCharsets .UTF_8 ))
478
+ .toList ();
479
+
480
+ // Verify there's only one traceparent header and it contains the updated value
481
+ assertThat (headerValues ).containsExactly (updatedTraceParent );
482
+ }
483
+
484
+ @ Test
485
+ void verifyTraceParentHeader (@ Autowired KafkaTemplate <Integer , String > template ,
486
+ @ Autowired SimpleTracer tracer ) throws Exception {
487
+ CompletableFuture <ProducerRecord <Integer , String >> producerRecordFuture = new CompletableFuture <>();
488
+ template .setProducerListener (new ProducerListener <>() {
489
+
490
+ @ Override
491
+ public void onSuccess (ProducerRecord <Integer , String > producerRecord , RecordMetadata recordMetadata ) {
492
+ producerRecordFuture .complete (producerRecord );
493
+ }
494
+ });
495
+ String initialTraceParent = "traceparent-from-previous" ;
496
+ Header header = new RecordHeader ("traceparent" , initialTraceParent .getBytes (StandardCharsets .UTF_8 ));
497
+ ProducerRecord <Integer , String > producerRecord = new ProducerRecord <>(
498
+ OBSERVATION_TRACEPARENT_DUPLICATE ,
499
+ null , null , null ,
500
+ "test-value" ,
501
+ List .of (header )
502
+ );
503
+
504
+ template .send (producerRecord ).get (10 , TimeUnit .SECONDS );
505
+ ProducerRecord <Integer , String > recordResult = producerRecordFuture .get (10 , TimeUnit .SECONDS );
506
+
507
+ Iterable <Header > traceparentHeaders = recordResult .headers ().headers ("traceparent" );
508
+ assertThat (traceparentHeaders ).hasSize (1 );
509
+
510
+ String traceparentValue = new String (traceparentHeaders .iterator ().next ().value (), StandardCharsets .UTF_8 );
511
+ assertThat (traceparentValue ).isEqualTo ("traceparent-from-propagator" );
512
+
513
+ tracer .getSpans ().clear ();
514
+ }
515
+
452
516
@ Configuration
453
517
@ EnableKafka
454
518
public static class Config {
@@ -598,6 +662,9 @@ public List<String> fields() {
598
662
public <C > void inject (TraceContext context , @ Nullable C carrier , Setter <C > setter ) {
599
663
setter .set (carrier , "foo" , "some foo value" );
600
664
setter .set (carrier , "bar" , "some bar value" );
665
+
666
+ // Add a traceparent header to simulate W3C trace context
667
+ setter .set (carrier , "traceparent" , "traceparent-from-propagator" );
601
668
}
602
669
603
670
// This is called on the consumer side when the message is consumed
@@ -606,7 +673,9 @@ public <C> void inject(TraceContext context, @Nullable C carrier, Setter<C> sett
606
673
public <C > Span .Builder extract (C carrier , Getter <C > getter ) {
607
674
String foo = getter .get (carrier , "foo" );
608
675
String bar = getter .get (carrier , "bar" );
609
- return tracer .spanBuilder ().tag ("foo" , foo ).tag ("bar" , bar );
676
+ return tracer .spanBuilder ()
677
+ .tag ("foo" , foo )
678
+ .tag ("bar" , bar );
610
679
}
611
680
};
612
681
}
0 commit comments