Skip to content

Commit 2c89597

Browse files
committed
Add observability support for JMS
This commit adds observability support for Jakarta JMS support in spring-jms support. This feature leverages the `JmsInstrumentation` infrastructure in `io.micrometer:micrometer-core` library. This instruments the `JmsTemplate` and the `@JmsListener` support to record observations: * "jms.message.publish" when the `JmsTemplate` sends a message * "jms.message.process" when a message is processed by a `@JmsListener` annotated method The observation `Convention` and `Context` implementations are shipped with "micrometer-core". Closes gh-30335
1 parent bfeca4a commit 2c89597

File tree

12 files changed

+442
-3
lines changed

12 files changed

+442
-3
lines changed

framework-docs/framework-docs.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ repositories {
6161

6262
dependencies {
6363
api(project(":spring-context"))
64+
api(project(":spring-jms"))
6465
api(project(":spring-web"))
66+
api("jakarta.jms:jakarta.jms-api")
6567
api("jakarta.servlet:jakarta.servlet-api")
6668

6769
implementation(project(":spring-core-test"))

framework-docs/modules/ROOT/pages/integration/observability.adoc

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ As outlined xref:integration/observability.adoc[at the beginning of this section
2727
|xref:integration/observability.adoc#observability.http-server[`"http.server.requests"`]
2828
|Processing time for HTTP server exchanges at the Framework level
2929

30+
|xref:integration/observability.adoc#observability.jms.publish[`"jms.message.publish"`]
31+
|Time spent sending a JMS message to a destination by a message producer.
32+
33+
|xref:integration/observability.adoc#observability.jms.process[`"jms.message.process"`]
34+
|Processing time for a JMS message that was previously received by a message consumer.
35+
3036
|xref:integration/observability.adoc#observability.tasks-scheduled[`"tasks.scheduled.execution"`]
3137
|Processing time for an execution of a `@Scheduled` task
3238
|===
@@ -108,6 +114,72 @@ By default, the following `KeyValues` are created:
108114
|===
109115

110116

117+
[[observability.jms]]
118+
== JMS messaging instrumentation
119+
120+
Spring Framework uses the Jakarta JMS instrumentation provided by Micrometer if the `io.micrometer:micrometer-core` dependency is on the classpath.
121+
The `io.micrometer.core.instrument.binder.jms.JmsInstrumentation` instruments `jakarta.jms.Session` and records the relevant observations.
122+
123+
This instrumentation will create 2 types of observations:
124+
125+
* `"jms.message.publish"` when a JMS message is sent to the broker, typically with `JmsTemplate`.
126+
* `"jms.message.process"` when a JMS message is processed by the application, typically with a `MessageListener` or a `@JmsListener` annotated method.
127+
128+
NOTE: currently there is no instrumentation for `"jms.message.receive"` observations as there is little value in measuring the time spent waiting for the reception of a message.
129+
Such an integration would typically instrument `MessageConsumer#receive` method calls. But once those return, the processing time is not measured and the trace scope cannot be propagated to the application.
130+
131+
By default, both observations share the same set of possible `KeyValues`:
132+
133+
.Low cardinality Keys
134+
[cols="a,a"]
135+
|===
136+
|Name | Description
137+
|`exception` |Class name of the exception thrown during the messaging operation (or "none").
138+
|`messaging.destination.temporary` _(required)_|Whether the destination is a `TemporaryQueue` or `TemporaryTopic` (values: `"true"` or `"false"`).
139+
|`messaging.operation` _(required)_|Name of JMS operation being performed (values: `"publish"` or `"process"`).
140+
|===
141+
142+
.High cardinality Keys
143+
[cols="a,a"]
144+
|===
145+
|Name | Description
146+
|`messaging.message.conversation_id` |The correlation ID of the JMS message.
147+
|`messaging.destination.name` |The name of destination the current message was sent to.
148+
|`messaging.message.id` |Value used by the messaging system as an identifier for the message.
149+
|===
150+
151+
[[observability.jms.publish]]
152+
=== JMS message Publication instrumentation
153+
154+
`"jms.message.publish"` observations are recorded when a JMS message is sent to the broker.
155+
They measure the time spent sending the message and propagate the tracing information with outgoing JMS message headers.
156+
157+
You will need to configure the `ObservationRegistry` on the `JmsTemplate` to enable observations:
158+
159+
include-code::./JmsTemplatePublish[]
160+
161+
It uses the `io.micrometer.core.instrument.binder.jms.DefaultJmsPublishObservationConvention` by default, backed by the `io.micrometer.core.instrument.binder.jms.JmsPublishObservationContext`.
162+
163+
[[observability.jms.process]]
164+
=== JMS message Processing instrumentation
165+
166+
`"jms.message.process"` observations are recorded when a JMS message is processed by the application.
167+
They measure the time spent processing the message and propagate the tracing context with incoming JMS message headers.
168+
169+
Most applications will use the xref:integration/jms/annotated.adoc#jms-annotated[`@JmsListener` annotated methods] mechanism to process incoming messages.
170+
You will need to ensure that the `ObservationRegistry` is configured on the dedicated `JmsListenerContainerFactory`:
171+
172+
include-code::./JmsConfiguration[]
173+
174+
A xref:integration/jms/annotated.adoc#jms-annotated-support[default container factory is required to enable the annotation support],
175+
but note that `@JmsListener` annotations can refer to specific container factory beans for specific purposes.
176+
In all cases, Observations are only recorded if the observation registry is configured on the container factory.
177+
178+
Similar observations are recorded with `JmsTemplate` when messages are processed by a `MessageListener`.
179+
Such listeners are set on a `MessageConsumer` within a session callback (see `JmsTemplate.execute(SessionCallback<T>)`).
180+
181+
This observation uses the `io.micrometer.core.instrument.binder.jms.DefaultJmsProcessObservationConvention` by default, backed by the `io.micrometer.core.instrument.binder.jms.JmsProcessObservationContext`.
182+
111183
[[observability.http-server]]
112184
== HTTP Server instrumentation
113185

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.docs.integration.observability.jms.process;
18+
19+
import io.micrometer.observation.ObservationRegistry;
20+
import jakarta.jms.ConnectionFactory;
21+
22+
import org.springframework.context.annotation.Bean;
23+
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.jms.annotation.EnableJms;
25+
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
26+
27+
@Configuration
28+
@EnableJms
29+
public class JmsConfiguration {
30+
31+
@Bean
32+
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory, ObservationRegistry observationRegistry) {
33+
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
34+
factory.setConnectionFactory(connectionFactory);
35+
factory.setObservationRegistry(observationRegistry);
36+
return factory;
37+
}
38+
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.docs.integration.observability.jms.publish;
18+
19+
import io.micrometer.observation.ObservationRegistry;
20+
import jakarta.jms.ConnectionFactory;
21+
22+
import org.springframework.jms.core.JmsMessagingTemplate;
23+
import org.springframework.jms.core.JmsTemplate;
24+
25+
public class JmsTemplatePublish {
26+
27+
private final JmsTemplate jmsTemplate;
28+
29+
private final JmsMessagingTemplate jmsMessagingTemplate;
30+
31+
public JmsTemplatePublish(ObservationRegistry observationRegistry, ConnectionFactory connectionFactory) {
32+
this.jmsTemplate = new JmsTemplate(connectionFactory);
33+
// configure the observation registry
34+
this.jmsTemplate.setObservationRegistry(observationRegistry);
35+
36+
// For JmsMessagingTemplate, instantiate it with a JMS template that has a configured registry
37+
this.jmsMessagingTemplate = new JmsMessagingTemplate(this.jmsTemplate);
38+
}
39+
40+
public void sendMessages() {
41+
this.jmsTemplate.convertAndSend("spring.observation.test", "test message");
42+
}
43+
44+
}

framework-platform/framework-platform.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ javaPlatform {
88

99
dependencies {
1010
api(platform("com.fasterxml.jackson:jackson-bom:2.15.2"))
11-
api(platform("io.micrometer:micrometer-bom:1.12.0-M1"))
11+
api(platform("io.micrometer:micrometer-bom:1.12.0-SNAPSHOT"))
1212
api(platform("io.netty:netty-bom:4.1.96.Final"))
1313
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
1414
api(platform("io.projectreactor:reactor-bom:2023.0.0-M1"))
@@ -92,6 +92,8 @@ dependencies {
9292
api("org.apache.activemq:activemq-broker:5.17.4")
9393
api("org.apache.activemq:activemq-kahadb-store:5.17.4")
9494
api("org.apache.activemq:activemq-stomp:5.17.4")
95+
api("org.apache.activemq:artemis-junit-5:2.29.0")
96+
api("org.apache.activemq:artemis-jakarta-client:2.29.0")
9597
api("org.apache.commons:commons-pool2:2.9.0")
9698
api("org.apache.derby:derby:10.16.1.1")
9799
api("org.apache.derby:derbyclient:10.16.1.1")

spring-jms/spring-jms.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,20 @@ dependencies {
55
api(project(":spring-core"))
66
api(project(":spring-messaging"))
77
api(project(":spring-tx"))
8+
api("io.micrometer:micrometer-observation")
89
compileOnly("jakarta.jms:jakarta.jms-api")
910
optional(project(":spring-aop"))
1011
optional(project(":spring-context"))
1112
optional(project(":spring-oxm"))
1213
optional("com.fasterxml.jackson.core:jackson-databind")
14+
optional("io.micrometer:micrometer-core")
1315
optional("jakarta.resource:jakarta.resource-api")
1416
optional("jakarta.transaction:jakarta.transaction-api")
1517
testImplementation(testFixtures(project(":spring-beans")))
1618
testImplementation(testFixtures(project(":spring-tx")))
1719
testImplementation("jakarta.jms:jakarta.jms-api")
20+
testImplementation('io.micrometer:context-propagation')
21+
testImplementation("io.micrometer:micrometer-observation-test")
22+
testImplementation("org.apache.activemq:artemis-junit-5")
23+
testImplementation("org.apache.activemq:artemis-jakarta-client")
1824
}

spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.jms.config;
1818

19+
import io.micrometer.observation.ObservationRegistry;
1920
import jakarta.jms.ConnectionFactory;
2021
import jakarta.jms.ExceptionListener;
2122
import org.apache.commons.logging.Log;
@@ -86,6 +87,9 @@ public abstract class AbstractJmsListenerContainerFactory<C extends AbstractMess
8687
@Nullable
8788
private Boolean autoStartup;
8889

90+
@Nullable
91+
private ObservationRegistry observationRegistry;
92+
8993

9094
/**
9195
* @see AbstractMessageListenerContainer#setConnectionFactory(ConnectionFactory)
@@ -193,6 +197,12 @@ public void setAutoStartup(boolean autoStartup) {
193197
this.autoStartup = autoStartup;
194198
}
195199

200+
/**
201+
* @see AbstractMessageListenerContainer#setObservationRegistry(ObservationRegistry)
202+
*/
203+
public void setObservationRegistry(ObservationRegistry observationRegistry) {
204+
this.observationRegistry = observationRegistry;
205+
}
196206

197207
@Override
198208
public C createListenerContainer(JmsListenerEndpoint endpoint) {
@@ -243,6 +253,9 @@ public C createListenerContainer(JmsListenerEndpoint endpoint) {
243253
if (this.autoStartup != null) {
244254
instance.setAutoStartup(this.autoStartup);
245255
}
256+
if (this.observationRegistry != null) {
257+
instance.setObservationRegistry(this.observationRegistry);
258+
}
246259

247260
initializeContainer(instance);
248261
endpoint.setupListenerContainer(instance);

spring-jms/src/main/java/org/springframework/jms/core/JmsTemplate.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.jms.core;
1818

19+
import io.micrometer.core.instrument.binder.jms.JmsInstrumentation;
20+
import io.micrometer.observation.ObservationRegistry;
1921
import jakarta.jms.Connection;
2022
import jakarta.jms.ConnectionFactory;
2123
import jakarta.jms.DeliveryMode;
@@ -40,6 +42,7 @@
4042
import org.springframework.lang.Nullable;
4143
import org.springframework.transaction.support.TransactionSynchronizationManager;
4244
import org.springframework.util.Assert;
45+
import org.springframework.util.ClassUtils;
4346

4447
/**
4548
* Helper class that simplifies synchronous JMS access code.
@@ -78,6 +81,7 @@
7881
* @author Mark Pollack
7982
* @author Juergen Hoeller
8083
* @author Stephane Nicoll
84+
* @author Brian Clozel
8185
* @since 1.1
8286
* @see #setConnectionFactory
8387
* @see #setPubSubDomain
@@ -88,6 +92,9 @@
8892
*/
8993
public class JmsTemplate extends JmsDestinationAccessor implements JmsOperations {
9094

95+
private static final boolean micrometerCorePresent = ClassUtils.isPresent(
96+
"io.micrometer.core.instrument.binder.jms.JmsInstrumentation", JmsTemplate.class.getClassLoader());
97+
9198
/** Internal ResourceFactory adapter for interacting with ConnectionFactoryUtils. */
9299
private final JmsTemplateResourceFactory transactionalResourceFactory = new JmsTemplateResourceFactory();
93100

@@ -118,6 +125,9 @@ public class JmsTemplate extends JmsDestinationAccessor implements JmsOperations
118125

119126
private long timeToLive = Message.DEFAULT_TIME_TO_LIVE;
120127

128+
@Nullable
129+
private ObservationRegistry observationRegistry;
130+
121131

122132
/**
123133
* Create a new JmsTemplate for bean-style usage.
@@ -460,6 +470,15 @@ public long getTimeToLive() {
460470
return this.timeToLive;
461471
}
462472

473+
/**
474+
* Configure the {@link ObservationRegistry} to use for recording JMS observations.
475+
* @param observationRegistry the observation registry to use.
476+
* @since 6.1
477+
* @see io.micrometer.core.instrument.binder.jms.JmsObservationDocumentation
478+
*/
479+
public void setObservationRegistry(ObservationRegistry observationRegistry) {
480+
this.observationRegistry = observationRegistry;
481+
}
463482

464483
//---------------------------------------------------------------------------------------
465484
// JmsOperations execute methods
@@ -504,6 +523,9 @@ public <T> T execute(SessionCallback<T> action, boolean startConnection) throws
504523
if (logger.isDebugEnabled()) {
505524
logger.debug("Executing callback on JMS Session: " + sessionToUse);
506525
}
526+
if (micrometerCorePresent && this.observationRegistry != null) {
527+
sessionToUse = MicrometerInstrumentation.instrumentSession(sessionToUse, this.observationRegistry);
528+
}
507529
return action.doInJms(sessionToUse);
508530
}
509531
catch (JMSException ex) {
@@ -1194,4 +1216,12 @@ public boolean isSynchedLocalTransactionAllowed() {
11941216
}
11951217
}
11961218

1219+
private static abstract class MicrometerInstrumentation {
1220+
1221+
static Session instrumentSession(Session session, ObservationRegistry registry) {
1222+
return JmsInstrumentation.instrumentSession(session, registry);
1223+
}
1224+
1225+
}
1226+
11971227
}

0 commit comments

Comments
 (0)