Skip to content

Commit 6050fff

Browse files
committed
Auto-configure observability for R2DBC
The new ConnectionFactoryDecorator can be used to decorate the ConnectionFactory built by the ConnectionFactoryBuilder. The new R2dbcObservationAutoConfiguration configures a ConnectionFactoryDecorator to attach a ObservationProxyExecutionListener to ConnectionFactories. This enables Micrometer Observations for R2DBC queries. Closes gh-33768
1 parent cc7f5a2 commit 6050fff

File tree

10 files changed

+374
-10
lines changed

10 files changed

+374
-10
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ dependencies {
7474
optional("io.projectreactor.netty:reactor-netty-http")
7575
optional("io.r2dbc:r2dbc-pool")
7676
optional("io.r2dbc:r2dbc-spi")
77+
optional("io.r2dbc:r2dbc-proxy")
7778
optional("jakarta.jms:jakarta.jms-api")
7879
optional("jakarta.persistence:jakarta.persistence-api")
7980
optional("jakarta.servlet:jakarta.servlet-api")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2012-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.boot.actuate.autoconfigure.r2dbc;
18+
19+
import io.micrometer.observation.ObservationRegistry;
20+
import io.r2dbc.proxy.ProxyConnectionFactory;
21+
import io.r2dbc.proxy.observation.ObservationProxyExecutionListener;
22+
import io.r2dbc.proxy.observation.QueryObservationConvention;
23+
import io.r2dbc.proxy.observation.QueryParametersTagProvider;
24+
import io.r2dbc.spi.ConnectionFactory;
25+
import io.r2dbc.spi.ConnectionFactoryOptions;
26+
27+
import org.springframework.beans.factory.ObjectProvider;
28+
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
29+
import org.springframework.boot.autoconfigure.AutoConfiguration;
30+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
31+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
32+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
33+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
34+
import org.springframework.boot.r2dbc.ConnectionFactoryDecorator;
35+
import org.springframework.boot.r2dbc.OptionsCapableConnectionFactory;
36+
import org.springframework.context.annotation.Bean;
37+
38+
/**
39+
* {@link EnableAutoConfiguration Auto-configuration} for R2DBC observability support.
40+
*
41+
* @author Moritz Halbritter
42+
* @since 3.2.0
43+
*/
44+
@AutoConfiguration(after = ObservationAutoConfiguration.class)
45+
@ConditionalOnClass({ ConnectionFactory.class, ProxyConnectionFactory.class })
46+
@EnableConfigurationProperties(R2dbcObservationProperties.class)
47+
public class R2dbcObservationAutoConfiguration {
48+
49+
@Bean
50+
@ConditionalOnBean(ObservationRegistry.class)
51+
ConnectionFactoryDecorator connectionFactoryDecorator(R2dbcObservationProperties properties,
52+
ObservationRegistry observationRegistry,
53+
ObjectProvider<QueryObservationConvention> queryObservationConvention,
54+
ObjectProvider<QueryParametersTagProvider> queryParametersTagProvider) {
55+
return (connectionFactory) -> {
56+
ObservationProxyExecutionListener listener = new ObservationProxyExecutionListener(observationRegistry,
57+
connectionFactory, extractUrl(connectionFactory));
58+
listener.setIncludeParameterValues(properties.isIncludeParameterValues());
59+
queryObservationConvention.ifAvailable(listener::setQueryObservationConvention);
60+
queryParametersTagProvider.ifAvailable(listener::setQueryParametersTagProvider);
61+
return ProxyConnectionFactory.builder(connectionFactory).listener(listener).build();
62+
};
63+
}
64+
65+
private String extractUrl(ConnectionFactory connectionFactory) {
66+
OptionsCapableConnectionFactory optionsCapableConnectionFactory = OptionsCapableConnectionFactory
67+
.unwrapFrom(connectionFactory);
68+
if (optionsCapableConnectionFactory == null) {
69+
return null;
70+
}
71+
ConnectionFactoryOptions options = optionsCapableConnectionFactory.getOptions();
72+
Object host = options.getValue(ConnectionFactoryOptions.HOST);
73+
Object port = options.getValue(ConnectionFactoryOptions.PORT);
74+
if (host == null || !(port instanceof Integer portAsInt)) {
75+
return null;
76+
}
77+
// See https://github.com/r2dbc/r2dbc-proxy/issues/135
78+
return "r2dbc:dummy://%s:%d/".formatted(host, portAsInt);
79+
}
80+
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2012-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.boot.actuate.autoconfigure.r2dbc;
18+
19+
import org.springframework.boot.context.properties.ConfigurationProperties;
20+
21+
/**
22+
* Configuration properties for R2DBC observability.
23+
*
24+
* @author Moritz Halbritter
25+
* @since 3.2.0
26+
*/
27+
@ConfigurationProperties("management.observations.r2dbc")
28+
public class R2dbcObservationProperties {
29+
30+
/**
31+
* Whether to tag actual query parameter values.
32+
*/
33+
private boolean includeParameterValues;
34+
35+
public boolean isIncludeParameterValues() {
36+
return this.includeParameterValues;
37+
}
38+
39+
public void setIncludeParameterValues(boolean includeParameterValues) {
40+
this.includeParameterValues = includeParameterValues;
41+
}
42+
43+
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfig
9090
org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration
9191
org.springframework.boot.actuate.autoconfigure.quartz.QuartzEndpointAutoConfiguration
9292
org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthContributorAutoConfiguration
93+
org.springframework.boot.actuate.autoconfigure.r2dbc.R2dbcObservationAutoConfiguration
9394
org.springframework.boot.actuate.autoconfigure.data.redis.RedisHealthContributorAutoConfiguration
9495
org.springframework.boot.actuate.autoconfigure.data.redis.RedisReactiveHealthContributorAutoConfiguration
9596
org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpointAutoConfiguration
@@ -112,4 +113,4 @@ org.springframework.boot.actuate.autoconfigure.web.exchanges.HttpExchangesEndpoi
112113
org.springframework.boot.actuate.autoconfigure.web.mappings.MappingsEndpointAutoConfiguration
113114
org.springframework.boot.actuate.autoconfigure.web.reactive.ReactiveManagementContextAutoConfiguration
114115
org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration
115-
org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration
116+
org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Copyright 2012-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.boot.actuate.autoconfigure.r2dbc;
18+
19+
import java.util.UUID;
20+
import java.util.concurrent.atomic.AtomicReference;
21+
22+
import io.micrometer.observation.Observation.Context;
23+
import io.micrometer.observation.ObservationHandler;
24+
import io.micrometer.observation.ObservationRegistry;
25+
import io.r2dbc.spi.ConnectionFactory;
26+
import org.awaitility.Awaitility;
27+
import org.hamcrest.Matchers;
28+
import org.junit.jupiter.api.Test;
29+
import reactor.core.publisher.Mono;
30+
31+
import org.springframework.boot.autoconfigure.AutoConfiguration;
32+
import org.springframework.boot.autoconfigure.AutoConfigurations;
33+
import org.springframework.boot.context.annotation.ImportCandidates;
34+
import org.springframework.boot.r2dbc.ConnectionFactoryBuilder;
35+
import org.springframework.boot.r2dbc.ConnectionFactoryDecorator;
36+
import org.springframework.boot.test.context.FilteredClassLoader;
37+
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
38+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
39+
40+
import static org.assertj.core.api.Assertions.assertThat;
41+
42+
/**
43+
* Tests for {@link R2dbcObservationAutoConfiguration}.
44+
*
45+
* @author Moritz Halbritter
46+
*/
47+
class R2dbcObservationAutoConfigurationTests {
48+
49+
private final ApplicationContextRunner runnerWithoutObservationRegistry = new ApplicationContextRunner()
50+
.withConfiguration(AutoConfigurations.of(R2dbcObservationAutoConfiguration.class));
51+
52+
private final ApplicationContextRunner runner = this.runnerWithoutObservationRegistry
53+
.withBean(ObservationRegistry.class, ObservationRegistry::create);
54+
55+
@Test
56+
void shouldBeRegisteredInAutoConfigurationImports() {
57+
assertThat(ImportCandidates.load(AutoConfiguration.class, null).getCandidates())
58+
.contains(R2dbcObservationAutoConfiguration.class.getName());
59+
}
60+
61+
@Test
62+
void shouldSupplyConnectionFactoryDecorator() {
63+
this.runner.run((context) -> assertThat(context).hasSingleBean(ConnectionFactoryDecorator.class));
64+
}
65+
66+
@Test
67+
void shouldNotSupplyBeansIfR2dbcSpiIsNotOnClasspath() {
68+
this.runner.withClassLoader(new FilteredClassLoader("io.r2dbc.spi"))
69+
.run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactoryDecorator.class));
70+
}
71+
72+
@Test
73+
void shouldNotSupplyBeansIfR2dbcProxyIsNotOnClasspath() {
74+
this.runner.withClassLoader(new FilteredClassLoader("io.r2dbc.proxy"))
75+
.run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactoryDecorator.class));
76+
}
77+
78+
@Test
79+
void shouldNotSupplyBeansIfObservationRegistryIsNotPresent() {
80+
this.runnerWithoutObservationRegistry
81+
.run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactoryDecorator.class));
82+
}
83+
84+
@Test
85+
void decoratorShouldReportObservations() {
86+
this.runner.run((context) -> {
87+
CapturingObservationHandler handler = registerCapturingObservationHandler(context);
88+
ConnectionFactoryDecorator decorator = context.getBean(ConnectionFactoryDecorator.class);
89+
assertThat(decorator).isNotNull();
90+
ConnectionFactory connectionFactory = ConnectionFactoryBuilder
91+
.withUrl("r2dbc:h2:mem:///" + UUID.randomUUID())
92+
.build();
93+
ConnectionFactory decorated = decorator.decorate(connectionFactory);
94+
Mono.from(decorated.create())
95+
.flatMap((c) -> Mono.from(c.createStatement("SELECT 1;").execute())
96+
.flatMap((ignore) -> Mono.from(c.close())))
97+
.block();
98+
assertThat(handler.awaitContext().getName()).as("context.getName()").isEqualTo("r2dbc.query");
99+
});
100+
}
101+
102+
private static CapturingObservationHandler registerCapturingObservationHandler(
103+
AssertableApplicationContext context) {
104+
ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class);
105+
assertThat(observationRegistry).isNotNull();
106+
CapturingObservationHandler handler = new CapturingObservationHandler();
107+
observationRegistry.observationConfig().observationHandler(handler);
108+
return handler;
109+
}
110+
111+
private static class CapturingObservationHandler implements ObservationHandler<Context> {
112+
113+
private final AtomicReference<Context> context = new AtomicReference<>();
114+
115+
@Override
116+
public boolean supportsContext(Context context) {
117+
return true;
118+
}
119+
120+
@Override
121+
public void onStart(Context context) {
122+
this.context.set(context);
123+
}
124+
125+
Context awaitContext() {
126+
return Awaitility.await().untilAtomic(this.context, Matchers.notNullValue());
127+
}
128+
129+
}
130+
131+
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/r2dbc/ConnectionFactoryConfigurations.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.boot.context.properties.bind.BindResult;
3434
import org.springframework.boot.context.properties.bind.Bindable;
3535
import org.springframework.boot.context.properties.bind.Binder;
36+
import org.springframework.boot.r2dbc.ConnectionFactoryDecorator;
3637
import org.springframework.boot.r2dbc.EmbeddedDatabaseConnection;
3738
import org.springframework.context.annotation.Bean;
3839
import org.springframework.context.annotation.Condition;
@@ -54,12 +55,14 @@
5455
* @author Moritz Halbritter
5556
* @author Andy Wilkinson
5657
* @author Phillip Webb
58+
* @author Moritz Halbritter
5759
*/
5860
abstract class ConnectionFactoryConfigurations {
5961

6062
protected static ConnectionFactory createConnectionFactory(R2dbcProperties properties,
6163
R2dbcConnectionDetails connectionDetails, ClassLoader classLoader,
62-
List<ConnectionFactoryOptionsBuilderCustomizer> optionsCustomizers) {
64+
List<ConnectionFactoryOptionsBuilderCustomizer> optionsCustomizers,
65+
List<ConnectionFactoryDecorator> decorators) {
6366
try {
6467
return org.springframework.boot.r2dbc.ConnectionFactoryBuilder
6568
.withOptions(new ConnectionFactoryOptionsInitializer().initialize(properties, connectionDetails,
@@ -69,6 +72,7 @@ protected static ConnectionFactory createConnectionFactory(R2dbcProperties prope
6972
optionsCustomizer.customize(options);
7073
}
7174
})
75+
.decorators(decorators)
7276
.build();
7377
}
7478
catch (IllegalStateException ex) {
@@ -93,10 +97,11 @@ static class PooledConnectionFactoryConfiguration {
9397
@Bean(destroyMethod = "dispose")
9498
ConnectionPool connectionFactory(R2dbcProperties properties,
9599
ObjectProvider<R2dbcConnectionDetails> connectionDetails, ResourceLoader resourceLoader,
96-
ObjectProvider<ConnectionFactoryOptionsBuilderCustomizer> customizers) {
100+
ObjectProvider<ConnectionFactoryOptionsBuilderCustomizer> customizers,
101+
ObjectProvider<ConnectionFactoryDecorator> decorators) {
97102
ConnectionFactory connectionFactory = createConnectionFactory(properties,
98103
connectionDetails.getIfAvailable(), resourceLoader.getClassLoader(),
99-
customizers.orderedStream().toList());
104+
customizers.orderedStream().toList(), decorators.orderedStream().toList());
100105
R2dbcProperties.Pool pool = properties.getPool();
101106
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
102107
ConnectionPoolConfiguration.Builder builder = ConnectionPoolConfiguration.builder(connectionFactory);
@@ -126,9 +131,11 @@ static class GenericConfiguration {
126131
@Bean
127132
ConnectionFactory connectionFactory(R2dbcProperties properties,
128133
ObjectProvider<R2dbcConnectionDetails> connectionDetails, ResourceLoader resourceLoader,
129-
ObjectProvider<ConnectionFactoryOptionsBuilderCustomizer> customizers) {
134+
ObjectProvider<ConnectionFactoryOptionsBuilderCustomizer> customizers,
135+
ObjectProvider<ConnectionFactoryDecorator> decorators) {
130136
return createConnectionFactory(properties, connectionDetails.getIfAvailable(),
131-
resourceLoader.getClassLoader(), customizers.orderedStream().toList());
137+
resourceLoader.getClassLoader(), customizers.orderedStream().toList(),
138+
decorators.orderedStream().toList());
132139
}
133140

134141
}

spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/observability.adoc

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ You can additionally register any number of `ObservationRegistryCustomizer` bean
1616

1717
For more details please see the https://micrometer.io/docs/observation[Micrometer Observation documentation].
1818

19-
TIP: Observability for JDBC and R2DBC can be configured using separate projects.
20-
For JDBC, the https://github.com/jdbc-observations/datasource-micrometer[Datasource Micrometer project] provides a Spring Boot starter which automatically creates observations when JDBC operations are invoked.
19+
TIP: Observability for JDBC can be configured using a separate project.
20+
The https://github.com/jdbc-observations/datasource-micrometer[Datasource Micrometer project] provides a Spring Boot starter which automatically creates observations when JDBC operations are invoked.
2121
Read more about it https://jdbc-observations.github.io/datasource-micrometer/docs/current/docs/html/[in the reference documentation].
22-
For R2DBC, the https://github.com/spring-projects-experimental/r2dbc-micrometer-spring-boot[Spring Boot Auto Configuration for R2DBC Observation] creates observations for R2DBC query invocations.
22+
23+
TIP: Observability for R2DBC is built into Spring Boot.
24+
To enable it, add the `io.r2dbc:r2dbc-proxy` dependency to your project.
2325

2426
[[actuator.observability.common-key-values]]
2527
=== Common Key-Values

0 commit comments

Comments
 (0)