Skip to content

Commit 182c580

Browse files
authored
GH-2198: Spring Observability Initial Commit (#2394)
* GH-2198: Spring Observability Initial Commit Resolves #2198 * Move getContextualContext to conventions. * Fix generics in test. * Add docs. * Fix doc link. * Remove unnecessary method overrides; make tag names more descriptive. * Async stop for send. * Fix checkstyle. * Fix async stop - don't stop on sync success; change order of spans for test. * Fix generics in test. * Fix checkstyle. * Fix race in test; with async send spans, finished spans order is indeterminate. * Move getName() from context to convention. * Fix Race in Test Fix Race in Test. Fix Race in Test. Fix Race in Test. Fix Race in Test.
1 parent 1b9716d commit 182c580

16 files changed

+1133
-49
lines changed

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,12 +335,14 @@ project ('spring-kafka') {
335335
optionalApi 'io.projectreactor:reactor-core'
336336
optionalApi 'io.projectreactor.kafka:reactor-kafka'
337337
optionalApi 'io.micrometer:micrometer-core'
338+
api 'io.micrometer:micrometer-observation'
338339
optionalApi 'io.micrometer:micrometer-tracing'
339340

340341
testImplementation project (':spring-kafka-test')
341342
testImplementation 'io.projectreactor:reactor-test'
342343
testImplementation "org.mockito:mockito-junit-jupiter:$mockitoVersion"
343344
testImplementation "org.hibernate.validator:hibernate-validator:$hibernateValidationVersion"
345+
testImplementation 'io.micrometer:micrometer-observation-test'
344346
testImplementation 'io.micrometer:micrometer-tracing-bridge-brave'
345347
testImplementation 'io.micrometer:micrometer-tracing-test'
346348
testImplementation 'io.micrometer:micrometer-tracing-integration-test'

spring-kafka-docs/src/main/asciidoc/kafka.adoc

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3310,7 +3310,6 @@ IMPORTANT: By default, the application context's event multicaster invokes event
33103310
If you change the multicaster to use an async executor, thread cleanup is not effective.
33113311

33123312
[[micrometer]]
3313-
33143313
==== Monitoring
33153314

33163315
===== Monitoring Listener Performance
@@ -3398,6 +3397,21 @@ double count = this.meterRegistry.get("kafka.producer.node.incoming.byte.total")
33983397

33993398
A similar listener is provided for the `StreamsBuilderFactoryBean` - see <<streams-micrometer>>.
34003399

3400+
[[observation]]
3401+
===== Micrometer Observation
3402+
3403+
Using Micrometer for observation is now supported, since version 3.0, for the `KafkaTemplate` and listener containers.
3404+
3405+
Set `observationEnabled` on each component to enable observation; this will disable <<micrometer,Micrometer Timers>> because the timers will now be managed with each observation.
3406+
3407+
Refer to https://micrometer.io/docs/tracing[Micrometer Tracing] for more information.
3408+
3409+
To add tags to timers/traces, configure a custom `KafkaTemplateObservationConvention` or `KafkaListenerObservationConvention` to the template or listener container, respectively.
3410+
3411+
The default implementations add the `bean.name` tag for template observations and `listener.id` tag for containers.
3412+
3413+
You can either subclass `DefaultKafkaTemplateObservationConvention` or `DefaultKafkaListenerObservationConvention` or provide completely new implementations.
3414+
34013415
[[transactions]]
34023416
==== Transactions
34033417

spring-kafka-docs/src/main/asciidoc/whats-new.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ IMPORTANT: When using transactions, the minimum broker version is 2.5.
1717

1818
See <<exactly-once>> and https://cwiki.apache.org/confluence/display/KAFKA/KIP-447%3A+Producer+scalability+for+exactly+once+semantics[KIP-447] for more information.
1919

20+
[[x30-obs]]
21+
==== Observation
22+
23+
Enabling observation for timers and tracing using Micrometer is now supported.
24+
See <<observation>> for more information.
25+
2026
[[x30-global-embedded-kafka]]
2127
==== Global Single Embedded Kafka
2228

spring-kafka/src/main/java/org/springframework/kafka/core/KafkaTemplate.java

Lines changed: 85 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848

4949
import org.springframework.beans.factory.BeanNameAware;
5050
import org.springframework.beans.factory.DisposableBean;
51+
import org.springframework.beans.factory.ObjectProvider;
52+
import org.springframework.beans.factory.SmartInitializingSingleton;
5153
import org.springframework.context.ApplicationContext;
5254
import org.springframework.context.ApplicationContextAware;
5355
import org.springframework.context.ApplicationListener;
@@ -62,13 +64,20 @@
6264
import org.springframework.kafka.support.TopicPartitionOffset;
6365
import org.springframework.kafka.support.converter.MessagingMessageConverter;
6466
import org.springframework.kafka.support.converter.RecordMessageConverter;
67+
import org.springframework.kafka.support.micrometer.DefaultKafkaTemplateObservationConvention;
68+
import org.springframework.kafka.support.micrometer.KafkaRecordSenderContext;
69+
import org.springframework.kafka.support.micrometer.KafkaTemplateObservation;
70+
import org.springframework.kafka.support.micrometer.KafkaTemplateObservationConvention;
6571
import org.springframework.kafka.support.micrometer.MicrometerHolder;
6672
import org.springframework.lang.Nullable;
6773
import org.springframework.messaging.Message;
6874
import org.springframework.messaging.converter.SmartMessageConverter;
6975
import org.springframework.transaction.support.TransactionSynchronizationManager;
7076
import org.springframework.util.Assert;
7177

78+
import io.micrometer.observation.Observation;
79+
import io.micrometer.observation.ObservationRegistry;
80+
7281
/**
7382
* A template for executing high-level operations. When used with a
7483
* {@link DefaultKafkaProducerFactory}, the template is thread-safe. The producer factory
@@ -90,7 +99,7 @@
9099
*/
91100
@SuppressWarnings("deprecation")
92101
public class KafkaTemplate<K, V> implements KafkaOperations<K, V>, ApplicationContextAware, BeanNameAware,
93-
ApplicationListener<ContextStoppedEvent>, DisposableBean {
102+
ApplicationListener<ContextStoppedEvent>, DisposableBean, SmartInitializingSingleton {
94103

95104
protected final LogAccessor logger = new LogAccessor(LogFactory.getLog(this.getClass())); //NOSONAR
96105

@@ -126,11 +135,17 @@ public class KafkaTemplate<K, V> implements KafkaOperations<K, V>, ApplicationCo
126135

127136
private ConsumerFactory<K, V> consumerFactory;
128137

129-
private volatile boolean micrometerEnabled = true;
138+
private ProducerInterceptor<K, V> producerInterceptor;
139+
140+
private boolean micrometerEnabled = true;
130141

131-
private volatile MicrometerHolder micrometerHolder;
142+
private MicrometerHolder micrometerHolder;
132143

133-
private ProducerInterceptor<K, V> producerInterceptor;
144+
private boolean observationEnabled;
145+
146+
private KafkaTemplateObservationConvention observationConvention;
147+
148+
private ObservationRegistry observationRegistry;
134149

135150
/**
136151
* Create an instance using the supplied producer factory and autoFlush false.
@@ -382,6 +397,37 @@ public void setProducerInterceptor(ProducerInterceptor<K, V> producerInterceptor
382397
this.producerInterceptor = producerInterceptor;
383398
}
384399

400+
/**
401+
* Set to true to enable observation via Micrometer.
402+
* @param observationEnabled true to enable.
403+
* @since 3.0
404+
* @see #setMicrometerEnabled(boolean)
405+
*/
406+
public void setObservationEnabled(boolean observationEnabled) {
407+
this.observationEnabled = observationEnabled;
408+
}
409+
410+
/**
411+
* Set a custom {@link KafkaTemplateObservationConvention}.
412+
* @param observationConvention the convention.
413+
* @since 3.0
414+
*/
415+
public void setObservationConvention(KafkaTemplateObservationConvention observationConvention) {
416+
this.observationConvention = observationConvention;
417+
}
418+
419+
@Override
420+
public void afterSingletonsInstantiated() {
421+
if (this.observationEnabled && this.observationRegistry == null && this.applicationContext != null) {
422+
ObjectProvider<ObservationRegistry> registry =
423+
this.applicationContext.getBeanProvider(ObservationRegistry.class);
424+
this.observationRegistry = registry.getIfUnique();
425+
}
426+
else if (this.micrometerEnabled) {
427+
this.micrometerHolder = obtainMicrometerHolder();
428+
}
429+
}
430+
385431
@Override
386432
public void onApplicationEvent(ContextStoppedEvent event) {
387433
if (this.customProducerFactory) {
@@ -412,33 +458,33 @@ public CompletableFuture<SendResult<K, V>> sendDefault(Integer partition, Long t
412458
@Override
413459
public CompletableFuture<SendResult<K, V>> send(String topic, @Nullable V data) {
414460
ProducerRecord<K, V> producerRecord = new ProducerRecord<>(topic, data);
415-
return doSend(producerRecord);
461+
return observeSend(producerRecord);
416462
}
417463

418464
@Override
419465
public CompletableFuture<SendResult<K, V>> send(String topic, K key, @Nullable V data) {
420466
ProducerRecord<K, V> producerRecord = new ProducerRecord<>(topic, key, data);
421-
return doSend(producerRecord);
467+
return observeSend(producerRecord);
422468
}
423469

424470
@Override
425471
public CompletableFuture<SendResult<K, V>> send(String topic, Integer partition, K key, @Nullable V data) {
426472
ProducerRecord<K, V> producerRecord = new ProducerRecord<>(topic, partition, key, data);
427-
return doSend(producerRecord);
473+
return observeSend(producerRecord);
428474
}
429475

430476
@Override
431477
public CompletableFuture<SendResult<K, V>> send(String topic, Integer partition, Long timestamp, K key,
432478
@Nullable V data) {
433479

434480
ProducerRecord<K, V> producerRecord = new ProducerRecord<>(topic, partition, timestamp, key, data);
435-
return doSend(producerRecord);
481+
return observeSend(producerRecord);
436482
}
437483

438484
@Override
439485
public CompletableFuture<SendResult<K, V>> send(ProducerRecord<K, V> record) {
440486
Assert.notNull(record, "'record' cannot be null");
441-
return doSend(record);
487+
return observeSend(record);
442488
}
443489

444490
@SuppressWarnings("unchecked")
@@ -451,7 +497,7 @@ public CompletableFuture<SendResult<K, V>> send(Message<?> message) {
451497
producerRecord.headers().add(KafkaHeaders.CORRELATION_ID, correlationId);
452498
}
453499
}
454-
return doSend((ProducerRecord<K, V>) producerRecord);
500+
return observeSend((ProducerRecord<K, V>) producerRecord);
455501
}
456502

457503

@@ -621,28 +667,48 @@ protected void closeProducer(Producer<K, V> producer, boolean inTx) {
621667
}
622668
}
623669

670+
private CompletableFuture<SendResult<K, V>> observeSend(final ProducerRecord<K, V> producerRecord) {
671+
Observation observation;
672+
if (!this.observationEnabled || this.observationRegistry == null) {
673+
observation = Observation.NOOP;
674+
}
675+
else {
676+
observation = KafkaTemplateObservation.TEMPLATE_OBSERVATION.observation(
677+
this.observationConvention, DefaultKafkaTemplateObservationConvention.INSTANCE,
678+
new KafkaRecordSenderContext(producerRecord, this.beanName), this.observationRegistry);
679+
}
680+
try {
681+
observation.start();
682+
return doSend(producerRecord, observation);
683+
}
684+
catch (RuntimeException ex) {
685+
observation.error(ex);
686+
observation.stop();
687+
throw ex;
688+
}
689+
}
624690
/**
625691
* Send the producer record.
626692
* @param producerRecord the producer record.
693+
* @param observation the observation.
627694
* @return a Future for the {@link org.apache.kafka.clients.producer.RecordMetadata
628695
* RecordMetadata}.
629696
*/
630-
protected CompletableFuture<SendResult<K, V>> doSend(final ProducerRecord<K, V> producerRecord) {
697+
protected CompletableFuture<SendResult<K, V>> doSend(final ProducerRecord<K, V> producerRecord,
698+
@Nullable Observation observation) {
699+
631700
final Producer<K, V> producer = getTheProducer(producerRecord.topic());
632701
this.logger.trace(() -> "Sending: " + KafkaUtils.format(producerRecord));
633702
final CompletableFuture<SendResult<K, V>> future = new CompletableFuture<>();
634703
Object sample = null;
635-
if (this.micrometerEnabled && this.micrometerHolder == null) {
636-
this.micrometerHolder = obtainMicrometerHolder();
637-
}
638704
if (this.micrometerHolder != null) {
639705
sample = this.micrometerHolder.start();
640706
}
641707
if (this.producerInterceptor != null) {
642708
this.producerInterceptor.onSend(producerRecord);
643709
}
644710
Future<RecordMetadata> sendFuture =
645-
producer.send(producerRecord, buildCallback(producerRecord, producer, future, sample));
711+
producer.send(producerRecord, buildCallback(producerRecord, producer, future, sample, observation));
646712
// May be an immediate failure
647713
if (sendFuture.isDone()) {
648714
try {
@@ -664,7 +730,7 @@ protected CompletableFuture<SendResult<K, V>> doSend(final ProducerRecord<K, V>
664730
}
665731

666732
private Callback buildCallback(final ProducerRecord<K, V> producerRecord, final Producer<K, V> producer,
667-
final CompletableFuture<SendResult<K, V>> future, @Nullable Object sample) {
733+
final CompletableFuture<SendResult<K, V>> future, @Nullable Object sample, Observation observation) {
668734

669735
return (metadata, exception) -> {
670736
try {
@@ -680,6 +746,7 @@ private Callback buildCallback(final ProducerRecord<K, V> producerRecord, final
680746
if (sample != null) {
681747
this.micrometerHolder.success(sample);
682748
}
749+
observation.stop();
683750
future.complete(new SendResult<>(producerRecord, metadata));
684751
if (KafkaTemplate.this.producerListener != null) {
685752
KafkaTemplate.this.producerListener.onSuccess(producerRecord, metadata);
@@ -691,6 +758,8 @@ private Callback buildCallback(final ProducerRecord<K, V> producerRecord, final
691758
if (sample != null) {
692759
this.micrometerHolder.failure(sample, exception.getClass().getSimpleName());
693760
}
761+
observation.error(exception);
762+
observation.stop();
694763
future.completeExceptionally(
695764
new KafkaProducerException(producerRecord, "Failed to send", exception));
696765
if (KafkaTemplate.this.producerListener != null) {

spring-kafka/src/main/java/org/springframework/kafka/listener/ContainerProperties.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.springframework.aop.support.AopUtils;
3333
import org.springframework.core.task.AsyncTaskExecutor;
3434
import org.springframework.kafka.support.TopicPartitionOffset;
35+
import org.springframework.kafka.support.micrometer.KafkaListenerObservationConvention;
3536
import org.springframework.lang.Nullable;
3637
import org.springframework.scheduling.TaskScheduler;
3738
import org.springframework.transaction.PlatformTransactionManager;
@@ -262,6 +263,8 @@ public enum EOSMode {
262263

263264
private boolean micrometerEnabled = true;
264265

266+
private boolean observationEnabled;
267+
265268
private Duration consumerStartTimeout = DEFAULT_CONSUMER_START_TIMEOUT;
266269

267270
private Boolean subBatchPerPartition;
@@ -282,6 +285,8 @@ public enum EOSMode {
282285

283286
private boolean pauseImmediate;
284287

288+
private KafkaListenerObservationConvention observationConvention;
289+
285290
/**
286291
* Create properties for a container that will subscribe to the specified topics.
287292
* @param topics the topics.
@@ -635,13 +640,28 @@ public boolean isMicrometerEnabled() {
635640

636641
/**
637642
* Set to false to disable the Micrometer listener timers. Default true.
643+
* Disabled when {@link #setObservationEnabled(boolean)} is true.
638644
* @param micrometerEnabled false to disable.
639645
* @since 2.3
640646
*/
641647
public void setMicrometerEnabled(boolean micrometerEnabled) {
642648
this.micrometerEnabled = micrometerEnabled;
643649
}
644650

651+
public boolean isObservationEnabled() {
652+
return this.observationEnabled;
653+
}
654+
655+
/**
656+
* Set to true to enable observation via Micrometer.
657+
* @param observationEnabled true to enable.
658+
* @since 3.0
659+
* @see #setMicrometerEnabled(boolean)
660+
*/
661+
public void setObservationEnabled(boolean observationEnabled) {
662+
this.observationEnabled = observationEnabled;
663+
}
664+
645665
/**
646666
* Set additional tags for the Micrometer listener timers.
647667
* @param tags the tags.
@@ -912,6 +932,19 @@ private void adviseListenerIfNeeded() {
912932
}
913933
}
914934

935+
public KafkaListenerObservationConvention getObservationConvention() {
936+
return this.observationConvention;
937+
}
938+
939+
/**
940+
* Set a custom {@link KafkaListenerObservationConvention}.
941+
* @param observationConvention the convention.
942+
* @since 3.0
943+
*/
944+
public void setObservationConvention(KafkaListenerObservationConvention observationConvention) {
945+
this.observationConvention = observationConvention;
946+
}
947+
915948
@Override
916949
public String toString() {
917950
return "ContainerProperties ["
@@ -942,7 +975,12 @@ public String toString() {
942975
+ "\n stopContainerWhenFenced=" + this.stopContainerWhenFenced
943976
+ "\n stopImmediate=" + this.stopImmediate
944977
+ "\n asyncAcks=" + this.asyncAcks
945-
+ "\n idleBeforeDataMultiplier" + this.idleBeforeDataMultiplier
978+
+ "\n idleBeforeDataMultiplier=" + this.idleBeforeDataMultiplier
979+
+ "\n micrometerEnabled=" + this.micrometerEnabled
980+
+ "\n observationEnabled=" + this.observationEnabled
981+
+ (this.observationConvention != null
982+
? "\n observationConvention=" + this.observationConvention
983+
: "")
946984
+ "\n]";
947985
}
948986

0 commit comments

Comments
 (0)