diff --git a/build.gradle b/build.gradle index 6d1d3c5466..bb702cfe7a 100644 --- a/build.gradle +++ b/build.gradle @@ -387,6 +387,7 @@ project('spring-rabbit') { optionalApi "ch.qos.logback:logback-classic:$logbackVersion" optionalApi 'org.apache.logging.log4j:log4j-core' optionalApi 'io.micrometer:micrometer-core' + api 'io.micrometer:micrometer-observation' optionalApi 'io.micrometer:micrometer-tracing' // Spring Data projection message binding support optionalApi ("org.springframework.data:spring-data-commons") { @@ -398,6 +399,7 @@ project('spring-rabbit') { testApi project(':spring-rabbit-junit') testImplementation("com.willowtreeapps.assertk:assertk-jvm:$assertkVersion") testImplementation "org.hibernate.validator:hibernate-validator:$hibernateValidationVersion" + testImplementation 'io.micrometer:micrometer-observation-test' testImplementation 'io.micrometer:micrometer-tracing-bridge-brave' testImplementation 'io.micrometer:micrometer-tracing-test' testImplementation 'io.micrometer:micrometer-tracing-integration-test' diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java index afebee61bc..54c23bf687 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/connection/RabbitAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,13 @@ import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.ApplicationContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.rabbitmq.client.Channel; +import io.micrometer.observation.ObservationRegistry; /** * @author Mark Fisher @@ -40,6 +43,8 @@ public abstract class RabbitAccessor implements InitializingBean { private volatile boolean transactional; + private ObservationRegistry observationRegistry; + public boolean isChannelTransacted() { return this.transactional; } @@ -113,4 +118,17 @@ protected RuntimeException convertRabbitAccessException(Exception ex) { return RabbitExceptionTranslator.convertRabbitAccessException(ex); } + protected void obtainObservationRegistry(@Nullable ApplicationContext appContext) { + if (this.observationRegistry == null && appContext != null) { + ObjectProvider registry = + appContext.getBeanProvider(ObservationRegistry.class); + this.observationRegistry = registry.getIfUnique(); + } + } + + @Nullable + protected ObservationRegistry getObservationRegistry() { + return this.observationRegistry; + } + } diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java index aecc38de30..2bbd7b98c5 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/core/RabbitTemplate.java @@ -74,6 +74,10 @@ import org.springframework.amqp.rabbit.support.MessagePropertiesConverter; import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator; import org.springframework.amqp.rabbit.support.ValueExpression; +import org.springframework.amqp.rabbit.support.micrometer.DefaultRabbitTemplateObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitMessageSenderContext; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservation; +import org.springframework.amqp.rabbit.support.micrometer.RabbitTemplateObservationConvention; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.amqp.support.converter.SimpleMessageConverter; import org.springframework.amqp.support.converter.SmartMessageConverter; @@ -83,6 +87,8 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.context.expression.MapAccessor; import org.springframework.core.ParameterizedTypeReference; @@ -108,6 +114,8 @@ import com.rabbitmq.client.Return; import com.rabbitmq.client.ShutdownListener; import com.rabbitmq.client.ShutdownSignalException; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; /** *

@@ -152,7 +160,7 @@ * @since 1.0 */ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count - implements BeanFactoryAware, RabbitOperations, ChannelAwareMessageListener, + implements BeanFactoryAware, RabbitOperations, ChannelAwareMessageListener, ApplicationContextAware, ListenerContainerAware, PublisherCallbackChannel.Listener, BeanNameAware, DisposableBean { private static final String UNCHECKED = "unchecked"; @@ -198,6 +206,8 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private final AtomicInteger containerInstance = new AtomicInteger(); + private ApplicationContext applicationContext; + private String exchange = DEFAULT_EXCHANGE; private String routingKey = DEFAULT_ROUTING_KEY; @@ -258,13 +268,20 @@ public class RabbitTemplate extends RabbitAccessor // NOSONAR type line count private ErrorHandler replyErrorHandler; + private boolean useChannelForCorrelation; + + private boolean observationEnabled; + + @Nullable + private RabbitTemplateObservationConvention observationConvention; + private volatile boolean usingFastReplyTo; private volatile boolean evaluatedFastReplyTo; private volatile boolean isListener; - private boolean useChannelForCorrelation; + private volatile boolean observationRegistryObtained; /** * Convenient constructor for use with setter injection. Don't forget to set the connection factory. @@ -297,6 +314,29 @@ public final void setConnectionFactory(ConnectionFactory connectionFactory) { } } + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + /** + * Enable observation via micrometer. + * @param observationEnabled true to enable. + * @since 3.0 + */ + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + /** + * Set an observation convention; used to add additional key/values to observations. + * @param observationConvention the convention. + * @since 3.0 + */ + public void setObservationConvention(RabbitTemplateObservationConvention observationConvention) { + this.observationConvention = observationConvention; + } + /** * The name of the default exchange to use for send operations when none is specified. Defaults to "" * which is the default exchange in the broker (per the AMQP specification). @@ -2348,7 +2388,7 @@ private boolean isPublisherConfirmsOrReturns(ConnectionFactory connectionFactory * @throws IOException If thrown by RabbitMQ API methods. */ public void doSend(Channel channel, String exchangeArg, String routingKeyArg, Message message, - boolean mandatory, @Nullable CorrelationData correlationData) throws IOException { + boolean mandatory, @Nullable CorrelationData correlationData) { String exch = nullSafeExchange(exchangeArg); String rKey = nullSafeRoutingKey(routingKeyArg); @@ -2378,7 +2418,7 @@ public void doSend(Channel channel, String exchangeArg, String routingKeyArg, Me logger.debug("Publishing message [" + messageToUse + "] on exchange [" + exch + "], routingKey = [" + rKey + "]"); } - sendToRabbit(channel, exch, rKey, mandatory, messageToUse); + observeTheSend(channel, messageToUse, mandatory, exch, rKey); // Check if commit needed if (isChannelLocallyTransacted(channel)) { // Transacted channel created by this template -> commit. @@ -2386,6 +2426,26 @@ public void doSend(Channel channel, String exchangeArg, String routingKeyArg, Me } } + protected void observeTheSend(Channel channel, Message message, boolean mandatory, String exch, String rKey) { + + if (!this.observationRegistryObtained) { + obtainObservationRegistry(this.applicationContext); + this.observationRegistryObtained = true; + } + Observation observation; + ObservationRegistry registry = getObservationRegistry(); + if (!this.observationEnabled || registry == null) { + observation = Observation.NOOP; + } + else { + observation = RabbitTemplateObservation.TEMPLATE_OBSERVATION.observation(this.observationConvention, + DefaultRabbitTemplateObservationConvention.INSTANCE, + new RabbitMessageSenderContext(message, this.beanName, exch + "/" + rKey), registry); + + } + observation.observe(() -> sendToRabbit(channel, exch, rKey, mandatory, message)); + } + /** * Return the exchange or the default exchange if null. * @param exchange the exchange. @@ -2407,10 +2467,16 @@ public String nullSafeRoutingKey(String rk) { } protected void sendToRabbit(Channel channel, String exchange, String routingKey, boolean mandatory, - Message message) throws IOException { + Message message) { + BasicProperties convertedMessageProperties = this.messagePropertiesConverter .fromMessageProperties(message.getMessageProperties(), this.encoding); - channel.basicPublish(exchange, routingKey, mandatory, convertedMessageProperties, message.getBody()); + try { + channel.basicPublish(exchange, routingKey, mandatory, convertedMessageProperties, message.getBody()); + } + catch (IOException ex) { + throw RabbitExceptionTranslator.convertRabbitAccessException(ex); + } } private void setupConfirm(Channel channel, Message message, @Nullable CorrelationData correlationDataArg) { diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java index 4e276dd05c..e96633e56c 100644 --- a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/listener/AbstractMessageListenerContainer.java @@ -63,6 +63,10 @@ import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter; import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.amqp.rabbit.support.MessagePropertiesConverter; +import org.springframework.amqp.rabbit.support.micrometer.DefaultRabbitListenerObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservation; +import org.springframework.amqp.rabbit.support.micrometer.RabbitListenerObservationConvention; +import org.springframework.amqp.rabbit.support.micrometer.RabbitMessageReceiverContext; import org.springframework.amqp.support.ConditionalExceptionLogger; import org.springframework.amqp.support.ConsumerTagStrategy; import org.springframework.amqp.support.postprocessor.MessagePostProcessorUtils; @@ -91,6 +95,8 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.ShutdownSignalException; +import io.micrometer.observation.Observation; +import io.micrometer.observation.ObservationRegistry; /** * @author Mark Pollack @@ -240,6 +246,8 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private boolean micrometerEnabled = true; + private boolean observationEnabled = false; + private boolean isBatchListener; private long consumeDelay; @@ -254,6 +262,9 @@ public abstract class AbstractMessageListenerContainer extends RabbitAccessor private MessageAckListener messageAckListener = (success, deliveryTag, cause) -> { }; + @Nullable + private RabbitListenerObservationConvention observationConvention; + @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.applicationEventPublisher = applicationEventPublisher; @@ -1151,14 +1162,36 @@ public void setMicrometerTags(Map tags) { } /** - * Set to false to disable micrometer listener timers. + * Set to false to disable micrometer listener timers. When true, ignored + * if {@link #setObservationEnabled(boolean)} is set to true. * @param micrometerEnabled false to disable. * @since 2.2 + * @see #setObservationEnabled(boolean) */ public void setMicrometerEnabled(boolean micrometerEnabled) { this.micrometerEnabled = micrometerEnabled; } + /** + * Enable observation via micrometer; disables basic Micrometer timers enabled + * by {@link #setMicrometerEnabled(boolean)}. + * @param observationEnabled true to enable. + * @since 3.0 + * @see #setMicrometerEnabled(boolean) + */ + public void setObservationEnabled(boolean observationEnabled) { + this.observationEnabled = observationEnabled; + } + + /** + * Set an observation convention; used to add additional key/values to observations. + * @param observationConvention the convention. + * @since 3.0 + */ + public void setObservationConvention(RabbitListenerObservationConvention observationConvention) { + this.observationConvention = observationConvention; + } + /** * Get the consumeDelay - a time to wait before consuming in ms. * @return the consume delay. @@ -1230,7 +1263,7 @@ public void afterPropertiesSet() { validateConfiguration(); initialize(); try { - if (this.micrometerHolder == null && MICROMETER_PRESENT && this.micrometerEnabled + if (this.micrometerHolder == null && MICROMETER_PRESENT && this.micrometerEnabled && !this.observationEnabled && this.applicationContext != null) { String id = getListenerId(); if (id == null) { @@ -1402,6 +1435,7 @@ public void start() { } } } + obtainObservationRegistry(this.applicationContext); try { logger.debug("Starting Rabbit listener container."); configureAdminIfNeeded(); @@ -1499,8 +1533,23 @@ protected void invokeErrorHandler(Throwable ex) { * @see #invokeListener * @see #handleListenerException */ - @SuppressWarnings(UNCHECKED) protected void executeListener(Channel channel, Object data) { + Observation observation; + ObservationRegistry registry = getObservationRegistry(); + if (!this.observationEnabled || data instanceof List || registry == null) { + observation = Observation.NOOP; + } + else { + Message message = (Message) data; + observation = RabbitListenerObservation.LISTENER_OBSERVATION.observation(this.observationConvention, + DefaultRabbitListenerObservationConvention.INSTANCE, + new RabbitMessageReceiverContext(message, getListenerId()), registry); + } + observation.observe(() -> executeListenerAndHandleException(channel, data)); + } + + @SuppressWarnings(UNCHECKED) + protected void executeListenerAndHandleException(Channel channel, Object data) { if (!isRunning()) { if (logger.isWarnEnabled()) { logger.warn( diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitListenerObservationConvention.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitListenerObservationConvention.java new file mode 100644 index 0000000000..ae6f4488b9 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitListenerObservationConvention.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import io.micrometer.common.KeyValues; + +/** + * Default {@link RabbitListenerObservationConvention} for Rabbit listener key values. + * + * @author Gary Russell + * @since 3.0 + * + */ +public class DefaultRabbitListenerObservationConvention implements RabbitListenerObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitListenerObservationConvention INSTANCE = + new DefaultRabbitListenerObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context) { + return KeyValues.of(RabbitListenerObservation.ListenerLowCardinalityTags.LISTENER_ID.asString(), + context.getListenerId()); + } + + @Override + public String getContextualName(RabbitMessageReceiverContext context) { + return context.getSource() + " receive"; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitTemplateObservationConvention.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitTemplateObservationConvention.java new file mode 100644 index 0000000000..285d52b835 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/DefaultRabbitTemplateObservationConvention.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import io.micrometer.common.KeyValues; + +/** + * Default {@link RabbitTemplateObservationConvention} for Rabbit template key values. + * + * @author Gary Russell + * @since 3.0 + * + */ +public class DefaultRabbitTemplateObservationConvention implements RabbitTemplateObservationConvention { + + /** + * A singleton instance of the convention. + */ + public static final DefaultRabbitTemplateObservationConvention INSTANCE = + new DefaultRabbitTemplateObservationConvention(); + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageSenderContext context) { + return KeyValues.of(RabbitTemplateObservation.TemplateLowCardinalityTags.BEAN_NAME.asString(), + context.getBeanName()); + } + + @Override + public String getContextualName(RabbitMessageSenderContext context) { + return context.getDestination() + " send"; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java new file mode 100644 index 0000000000..8454a987a3 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservation.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.DocumentedObservation; + +/** + * Spring Rabbit Observation for listeners. + * + * @author Gary Russell + * @since 3.0 + * + */ +public enum RabbitListenerObservation implements DocumentedObservation { + + /** + * Observation for Rabbit listeners. + */ + LISTENER_OBSERVATION { + + + @Override + public Class> getDefaultConvention() { + return DefaultRabbitListenerObservationConvention.class; + } + + @Override + public String getPrefix() { + return "spring.rabbit.listener"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return ListenerLowCardinalityTags.values(); + } + + }; + + /** + * Low cardinality tags. + */ + public enum ListenerLowCardinalityTags implements KeyName { + + /** + * Listener id. + */ + LISTENER_ID { + + @Override + public String asString() { + return "spring.rabbit.listener.id"; + } + + } + + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservationConvention.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservationConvention.java new file mode 100644 index 0000000000..bbf1f27df5 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitListenerObservationConvention.java @@ -0,0 +1,41 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} for Rabbit listener key values. + * + * @author Gary Russell + * @since 3.0 + * + */ +public interface RabbitListenerObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Context context) { + return context instanceof RabbitMessageReceiverContext; + } + + @Override + default String getName() { + return "spring.rabbit.listener"; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java new file mode 100644 index 0000000000..07ffebd732 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageReceiverContext.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import org.springframework.amqp.core.Message; + +import io.micrometer.observation.transport.ReceiverContext; + +/** + * {@link ReceiverContext} for {@link Message}s. + * + * @author Gary Russell + * @since 3.0 + * + */ +public class RabbitMessageReceiverContext extends ReceiverContext { + + private final String listenerId; + + private final Message message; + + public RabbitMessageReceiverContext(Message message, String listenerId) { + super((carrier, key) -> carrier.getMessageProperties().getHeader(key)); + setCarrier(message); + this.message = message; + this.listenerId = listenerId; + } + + public String getListenerId() { + return this.listenerId; + } + + /** + * Return the source (queue) for this message. + * @return the source. + */ + public String getSource() { + return this.message.getMessageProperties().getConsumerQueue(); + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java new file mode 100644 index 0000000000..b1b25755d4 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitMessageSenderContext.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import org.springframework.amqp.core.Message; + +import io.micrometer.observation.transport.SenderContext; + +/** + * {@link SenderContext} for {@link Message}s. + * + * @author Gary Russell + * @since 3.0 + * + */ +public class RabbitMessageSenderContext extends SenderContext { + + private final String beanName; + + private final String destination; + + public RabbitMessageSenderContext(Message message, String beanName, String destination) { + super((carrier, key, value) -> message.getMessageProperties().setHeader(key, value)); + setCarrier(message); + this.beanName = beanName; + this.destination = destination; + } + + public String getBeanName() { + return this.beanName; + } + + /** + * Return the destination - {@code exchange/routingKey}. + * @return the destination. + */ + public String getDestination() { + return this.destination; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java new file mode 100644 index 0000000000..01fb63f6c2 --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservation.java @@ -0,0 +1,74 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import io.micrometer.common.docs.KeyName; +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; +import io.micrometer.observation.docs.DocumentedObservation; + +/** + * Spring RabbitMQ Observation for {@link org.springframework.amqp.rabbit.core.RabbitTemplate}. + * + * @author Gary Russell + * @since 3.0 + * + */ +public enum RabbitTemplateObservation implements DocumentedObservation { + + /** + * {@link org.springframework.kafka.core.KafkaTemplate} observation. + */ + TEMPLATE_OBSERVATION { + + @Override + public Class> getDefaultConvention() { + return DefaultRabbitTemplateObservationConvention.class; + } + + @Override + public String getPrefix() { + return "spring.rabbit.template"; + } + + @Override + public KeyName[] getLowCardinalityKeyNames() { + return TemplateLowCardinalityTags.values(); + } + + }; + + /** + * Low cardinality tags. + */ + public enum TemplateLowCardinalityTags implements KeyName { + + /** + * Bean name of the template. + */ + BEAN_NAME { + + @Override + public String asString() { + return "spring.rabbit.template.name"; + } + + } + + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservationConvention.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservationConvention.java new file mode 100644 index 0000000000..2128d3dd9b --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/RabbitTemplateObservationConvention.java @@ -0,0 +1,41 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import io.micrometer.observation.Observation.Context; +import io.micrometer.observation.ObservationConvention; + +/** + * {@link ObservationConvention} for Rabbit template key values. + * + * @author Gary Russell + * @since 3.0 + * + */ +public interface RabbitTemplateObservationConvention extends ObservationConvention { + + @Override + default boolean supportsContext(Context context) { + return context instanceof RabbitMessageSenderContext; + } + + @Override + default String getName() { + return "spring.rabbit.template"; + } + +} diff --git a/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java new file mode 100644 index 0000000000..8131427dac --- /dev/null +++ b/spring-rabbit/src/main/java/org/springframework/amqp/rabbit/support/micrometer/package-info.java @@ -0,0 +1,6 @@ +/** + * Provides classes for Micrometer support. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.amqp.rabbit.support.micrometer; diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java new file mode 100644 index 0000000000..2611785c8f --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationIntegrationTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.Message; + +import io.micrometer.common.KeyValues; +import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.Span.Kind; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.test.SampleTestRunner; +import io.micrometer.tracing.test.simple.SpanAssert; +import io.micrometer.tracing.test.simple.SpansAssert; + +/** + * @author Artem Bilan + * @author Gary Russell + * + * @since 3.0 + */ +@RabbitAvailable(queues = { "int.observation.testQ1", "int.observation.testQ2" }) +public class ObservationIntegrationTests extends SampleTestRunner { + + @Override + public SampleTestRunnerConsumer yourCode() { + // template -> listener -> template -> listener + return (bb, meterRegistry) -> { + ObservationRegistry observationRegistry = getObservationRegistry(); + try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) { + applicationContext.registerBean(ObservationRegistry.class, () -> observationRegistry); + applicationContext.register(Config.class); + applicationContext.refresh(); + applicationContext.getBean(RabbitTemplate.class).convertAndSend("int.observation.testQ1", "test"); + assertThat(applicationContext.getBean(Listener.class).latch1.await(10, TimeUnit.SECONDS)).isTrue(); + } + + List finishedSpans = bb.getFinishedSpans(); + SpansAssert.assertThat(finishedSpans) + .haveSameTraceId() + .hasSize(4); + SpanAssert.assertThat(finishedSpans.get(0)) + .hasKindEqualTo(Kind.PRODUCER) + .hasTag("spring.rabbit.template.name", "template"); + SpanAssert.assertThat(finishedSpans.get(1)) + .hasKindEqualTo(Kind.PRODUCER) + .hasTag("spring.rabbit.template.name", "template"); + SpanAssert.assertThat(finishedSpans.get(2)) + .hasKindEqualTo(Kind.CONSUMER) + .hasTag("spring.rabbit.listener.id", "obs1"); + SpanAssert.assertThat(finishedSpans.get(3)) + .hasKindEqualTo(Kind.CONSUMER) + .hasTag("spring.rabbit.listener.id", "obs2"); + + MeterRegistryAssert.assertThat(getMeterRegistry()) + .hasTimerWithNameAndTags("spring.rabbit.template", + KeyValues.of("spring.rabbit.template.name", "template")) + .hasTimerWithNameAndTags("spring.rabbit.template", + KeyValues.of("spring.rabbit.template.name", "template")) + .hasTimerWithNameAndTags("spring.rabbit.listener", + KeyValues.of("spring.rabbit.listener.id", "obs1")) + .hasTimerWithNameAndTags("spring.rabbit.listener", + KeyValues.of("spring.rabbit.listener.id", "obs2")); + }; + } + + + @Configuration + @EnableRabbit + public static class Config { + + @Bean + CachingConnectionFactory ccf() { + return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + } + + @Bean + RabbitTemplate template(CachingConnectionFactory ccf) { + RabbitTemplate template = new RabbitTemplate(ccf); + template.setObservationEnabled(true); + return template; + } + + @Bean + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(CachingConnectionFactory ccf) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(ccf); + factory.setContainerCustomizer(container -> container.setObservationEnabled(true)); + return factory; + } + + @Bean + Listener listener(RabbitTemplate template) { + return new Listener(template); + } + + } + + public static class Listener { + + private final RabbitTemplate template; + + final CountDownLatch latch1 = new CountDownLatch(1); + + volatile Message message; + + public Listener(RabbitTemplate template) { + this.template = template; + } + + @RabbitListener(id = "obs1", queues = "int.observation.testQ1") + void listen1(Message in) { + this.template.convertAndSend("int.observation.testQ2", in); + } + + @RabbitListener(id = "obs2", queues = "int.observation.testQ2") + void listen2(Message in) { + this.message = in; + this.latch1.countDown(); + } + + } + + +} diff --git a/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java new file mode 100644 index 0000000000..58e3d911b3 --- /dev/null +++ b/spring-rabbit/src/test/java/org/springframework/amqp/rabbit/support/micrometer/ObservationTests.java @@ -0,0 +1,280 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.amqp.rabbit.support.micrometer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.util.Arrays; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.rabbit.junit.RabbitAvailable; +import org.springframework.amqp.rabbit.junit.RabbitAvailableCondition; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.micrometer.common.KeyValues; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.core.tck.MeterRegistryAssert; +import io.micrometer.observation.ObservationHandler; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.observation.tck.TestObservationRegistry; +import io.micrometer.tracing.Span; +import io.micrometer.tracing.TraceContext; +import io.micrometer.tracing.Tracer; +import io.micrometer.tracing.handler.DefaultTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingReceiverTracingObservationHandler; +import io.micrometer.tracing.handler.PropagatingSenderTracingObservationHandler; +import io.micrometer.tracing.propagation.Propagator; +import io.micrometer.tracing.test.simple.SimpleSpan; +import io.micrometer.tracing.test.simple.SimpleTracer; + +/** + * @author Gary Russell + * @since 3.0 + * + */ +@SpringJUnitConfig +@RabbitAvailable(queues = { "observation.testQ1", "observation.testQ2" }) +public class ObservationTests { + + @Test + void endToEnd(@Autowired Listener listener, @Autowired RabbitTemplate template, + @Autowired SimpleTracer tracer, @Autowired RabbitListenerEndpointRegistry rler, + @Autowired MeterRegistry meterRegistry) + throws InterruptedException { + + template.convertAndSend("observation.testQ1", "test"); + assertThat(listener.latch1.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(listener.message) + .extracting(msg -> msg.getMessageProperties().getHeaders()) + .hasFieldOrPropertyWithValue("foo", "some foo value") + .hasFieldOrPropertyWithValue("bar", "some bar value"); + Deque spans = tracer.getSpans(); + assertThat(spans).hasSize(4); + SimpleSpan span = spans.poll(); + assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); + assertThat(span.getName()).isEqualTo("/observation.testQ1 send"); + await().until(() -> spans.peekFirst().getTags().size() == 3); + span = spans.poll(); + assertThat(span.getTags()) + .containsAllEntriesOf( + Map.of("spring.rabbit.listener.id", "obs1", "foo", "some foo value", "bar", "some bar value")); + assertThat(span.getName()).isEqualTo("observation.testQ1 receive"); + await().until(() -> spans.peekFirst().getTags().size() == 1); + span = spans.poll(); + assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); + assertThat(span.getName()).isEqualTo("/observation.testQ2 send"); + await().until(() -> spans.peekFirst().getTags().size() == 3); + span = spans.poll(); + assertThat(span.getTags()) + .containsAllEntriesOf( + Map.of("spring.rabbit.listener.id", "obs2", "foo", "some foo value", "bar", "some bar value")); + assertThat(span.getName()).isEqualTo("observation.testQ2 receive"); + template.setObservationConvention(new DefaultRabbitTemplateObservationConvention() { + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageSenderContext context) { + return super.getLowCardinalityKeyValues(context).and("foo", "bar"); + } + + }); + ((AbstractMessageListenerContainer) rler.getListenerContainer("obs1")).setObservationConvention( + new DefaultRabbitListenerObservationConvention() { + + @Override + public KeyValues getLowCardinalityKeyValues(RabbitMessageReceiverContext context) { + return super.getLowCardinalityKeyValues(context).and("baz", "qux"); + } + + }); + rler.getListenerContainer("obs1").stop(); + rler.getListenerContainer("obs1").start(); + template.convertAndSend("observation.testQ1", "test"); + assertThat(listener.latch2.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(listener.message) + .extracting(msg -> msg.getMessageProperties().getHeaders()) + .hasFieldOrPropertyWithValue("foo", "some foo value") + .hasFieldOrPropertyWithValue("bar", "some bar value"); + assertThat(spans).hasSize(4); + span = spans.poll(); + assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); + assertThat(span.getTags()).containsEntry("foo", "bar"); + assertThat(span.getName()).isEqualTo("/observation.testQ1 send"); + await().until(() -> spans.peekFirst().getTags().size() == 4); + span = spans.poll(); + assertThat(span.getTags()) + .containsAllEntriesOf(Map.of("spring.rabbit.listener.id", "obs1", "foo", "some foo value", "bar", + "some bar value", "baz", "qux")); + assertThat(span.getName()).isEqualTo("observation.testQ1 receive"); + await().until(() -> spans.peekFirst().getTags().size() == 2); + span = spans.poll(); + assertThat(span.getTags()).containsEntry("spring.rabbit.template.name", "template"); + assertThat(span.getTags()).containsEntry("foo", "bar"); + assertThat(span.getName()).isEqualTo("/observation.testQ2 send"); + await().until(() -> spans.peekFirst().getTags().size() == 3); + span = spans.poll(); + assertThat(span.getTags()) + .containsAllEntriesOf( + Map.of("spring.rabbit.listener.id", "obs2", "foo", "some foo value", "bar", "some bar value")); + assertThat(span.getTags()).doesNotContainEntry("baz", "qux"); + assertThat(span.getName()).isEqualTo("observation.testQ2 receive"); + MeterRegistryAssert.assertThat(meterRegistry) + .hasTimerWithNameAndTags("spring.rabbit.template", + KeyValues.of("spring.rabbit.template.name", "template")) + .hasTimerWithNameAndTags("spring.rabbit.template", + KeyValues.of("spring.rabbit.template.name", "template", "foo", "bar")) + .hasTimerWithNameAndTags("spring.rabbit.listener", KeyValues.of("spring.rabbit.listener.id", "obs1")) + .hasTimerWithNameAndTags("spring.rabbit.listener", + KeyValues.of("spring.rabbit.listener.id", "obs1", "baz", "qux")) + .hasTimerWithNameAndTags("spring.rabbit.listener", KeyValues.of("spring.rabbit.listener.id", "obs2")); + } + + @Configuration + @EnableRabbit + public static class Config { + + @Bean + CachingConnectionFactory ccf() { + return new CachingConnectionFactory(RabbitAvailableCondition.getBrokerRunning().getConnectionFactory()); + } + + @Bean + RabbitTemplate template(CachingConnectionFactory ccf) { + RabbitTemplate template = new RabbitTemplate(ccf); + template.setObservationEnabled(true); + return template; + } + + @Bean + SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(CachingConnectionFactory ccf) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(ccf); + factory.setContainerCustomizer(container -> container.setObservationEnabled(true)); + return factory; + } + + @Bean + SimpleTracer simpleTracer() { + return new SimpleTracer(); + } + + @Bean + MeterRegistry meterRegistry() { + return new SimpleMeterRegistry(); + } + + @Bean + ObservationRegistry observationRegistry(Tracer tracer, Propagator propagator, MeterRegistry meterRegistry) { + TestObservationRegistry observationRegistry = TestObservationRegistry.create(); + observationRegistry.observationConfig().observationHandler( + // Composite will pick the first matching handler + new ObservationHandler.FirstMatchingCompositeObservationHandler( + // This is responsible for creating a child span on the sender side + new PropagatingSenderTracingObservationHandler<>(tracer, propagator), + // This is responsible for creating a span on the receiver side + new PropagatingReceiverTracingObservationHandler<>(tracer, propagator), + // This is responsible for creating a default span + new DefaultTracingObservationHandler(tracer))) + .observationHandler(new DefaultMeterObservationHandler(meterRegistry)); + return observationRegistry; + } + + @Bean + Propagator propagator(Tracer tracer) { + return new Propagator() { + + // List of headers required for tracing propagation + @Override + public List fields() { + return Arrays.asList("foo", "bar"); + } + + // This is called on the producer side when the message is being sent + // Normally we would pass information from tracing context - for tests we don't need to + @Override + public void inject(TraceContext context, @Nullable C carrier, Setter setter) { + setter.set(carrier, "foo", "some foo value"); + setter.set(carrier, "bar", "some bar value"); + } + + // This is called on the consumer side when the message is consumed + // Normally we would use tools like Extractor from tracing but for tests we are just manually creating a span + @Override + public Span.Builder extract(C carrier, Getter getter) { + String foo = getter.get(carrier, "foo"); + String bar = getter.get(carrier, "bar"); + return tracer.spanBuilder().tag("foo", foo).tag("bar", bar); + } + }; + } + + @Bean + Listener listener(RabbitTemplate template) { + return new Listener(template); + } + + } + + public static class Listener { + + private final RabbitTemplate template; + + final CountDownLatch latch1 = new CountDownLatch(1); + + final CountDownLatch latch2 = new CountDownLatch(2); + + volatile Message message; + + public Listener(RabbitTemplate template) { + this.template = template; + } + + @RabbitListener(id = "obs1", queues = "observation.testQ1") + void listen1(Message in) { + this.template.send("observation.testQ2", in); + } + + @RabbitListener(id = "obs2", queues = "observation.testQ2") + void listen2(Message in) { + this.message = in; + this.latch1.countDown(); + this.latch2.countDown(); + } + + } + +} diff --git a/src/reference/asciidoc/amqp.adoc b/src/reference/asciidoc/amqp.adoc index dc011b1666..b817785496 100644 --- a/src/reference/asciidoc/amqp.adoc +++ b/src/reference/asciidoc/amqp.adoc @@ -3764,6 +3764,23 @@ The timers are named `spring.rabbitmq.listener` and have the following tags: You can add additional tags using the `micrometerTags` container property. +Also see <>. + +[[observation]] +===== Micrometer Observation + +Using Micrometer for observation is now supported, since version 3.0, for the `RabbitTemplate` and listener containers. + +Set `observationEnabled` on each component to enable observation; this will disable <> because the timers will now be managed with each observation. + +Refer to https://micrometer.io/docs/tracing[Micrometer Tracing] for more information. + +To add tags to timers/traces, configure a custom `RabbitTemplateObservationConvention` or `RabbitListenerObservationConvention` to the template or listener container, respectively. + +The default implementations add the `bean.name` tag for template observations and `listener.id` tag for containers. + +You can either subclass `DefaultRabbitTemplateObservationConvention` or `DefaultRabbitListenerObservationConvention` or provide completely new implementations. + [[containers-and-broker-named-queues]] ==== Containers and Broker-Named queues diff --git a/src/reference/asciidoc/whats-new.adoc b/src/reference/asciidoc/whats-new.adoc index 2b43baca63..225befa469 100644 --- a/src/reference/asciidoc/whats-new.adoc +++ b/src/reference/asciidoc/whats-new.adoc @@ -11,6 +11,11 @@ This version requires Spring Framework 6.0 and Java 17 The remoting feature (using RMI) is no longer supported. +==== Observation + +Enabling observation for timers and tracing using Micrometer is now supported. +See <> for more information. + ==== AsyncRabbitTemplate The `AsyncRabbitTemplate2`, which was added in 2.4.7 to aid migration to this release, is deprecated in favor of `AsyncRabbitTemplate`.