Browse Source
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-33768pull/36604/head
10 changed files with 374 additions and 10 deletions
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
/* |
||||
* Copyright 2012-2023 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.boot.actuate.autoconfigure.r2dbc; |
||||
|
||||
import io.micrometer.observation.ObservationRegistry; |
||||
import io.r2dbc.proxy.ProxyConnectionFactory; |
||||
import io.r2dbc.proxy.observation.ObservationProxyExecutionListener; |
||||
import io.r2dbc.proxy.observation.QueryObservationConvention; |
||||
import io.r2dbc.proxy.observation.QueryParametersTagProvider; |
||||
import io.r2dbc.spi.ConnectionFactory; |
||||
import io.r2dbc.spi.ConnectionFactoryOptions; |
||||
|
||||
import org.springframework.beans.factory.ObjectProvider; |
||||
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration; |
||||
import org.springframework.boot.autoconfigure.AutoConfiguration; |
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; |
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; |
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties; |
||||
import org.springframework.boot.r2dbc.ConnectionFactoryDecorator; |
||||
import org.springframework.boot.r2dbc.OptionsCapableConnectionFactory; |
||||
import org.springframework.context.annotation.Bean; |
||||
|
||||
/** |
||||
* {@link EnableAutoConfiguration Auto-configuration} for R2DBC observability support. |
||||
* |
||||
* @author Moritz Halbritter |
||||
* @since 3.2.0 |
||||
*/ |
||||
@AutoConfiguration(after = ObservationAutoConfiguration.class) |
||||
@ConditionalOnClass({ ConnectionFactory.class, ProxyConnectionFactory.class }) |
||||
@EnableConfigurationProperties(R2dbcObservationProperties.class) |
||||
public class R2dbcObservationAutoConfiguration { |
||||
|
||||
@Bean |
||||
@ConditionalOnBean(ObservationRegistry.class) |
||||
ConnectionFactoryDecorator connectionFactoryDecorator(R2dbcObservationProperties properties, |
||||
ObservationRegistry observationRegistry, |
||||
ObjectProvider<QueryObservationConvention> queryObservationConvention, |
||||
ObjectProvider<QueryParametersTagProvider> queryParametersTagProvider) { |
||||
return (connectionFactory) -> { |
||||
ObservationProxyExecutionListener listener = new ObservationProxyExecutionListener(observationRegistry, |
||||
connectionFactory, extractUrl(connectionFactory)); |
||||
listener.setIncludeParameterValues(properties.isIncludeParameterValues()); |
||||
queryObservationConvention.ifAvailable(listener::setQueryObservationConvention); |
||||
queryParametersTagProvider.ifAvailable(listener::setQueryParametersTagProvider); |
||||
return ProxyConnectionFactory.builder(connectionFactory).listener(listener).build(); |
||||
}; |
||||
} |
||||
|
||||
private String extractUrl(ConnectionFactory connectionFactory) { |
||||
OptionsCapableConnectionFactory optionsCapableConnectionFactory = OptionsCapableConnectionFactory |
||||
.unwrapFrom(connectionFactory); |
||||
if (optionsCapableConnectionFactory == null) { |
||||
return null; |
||||
} |
||||
ConnectionFactoryOptions options = optionsCapableConnectionFactory.getOptions(); |
||||
Object host = options.getValue(ConnectionFactoryOptions.HOST); |
||||
Object port = options.getValue(ConnectionFactoryOptions.PORT); |
||||
if (host == null || !(port instanceof Integer portAsInt)) { |
||||
return null; |
||||
} |
||||
// See https://github.com/r2dbc/r2dbc-proxy/issues/135
|
||||
return "r2dbc:dummy://%s:%d/".formatted(host, portAsInt); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
/* |
||||
* Copyright 2012-2023 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.boot.actuate.autoconfigure.r2dbc; |
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties; |
||||
|
||||
/** |
||||
* Configuration properties for R2DBC observability. |
||||
* |
||||
* @author Moritz Halbritter |
||||
* @since 3.2.0 |
||||
*/ |
||||
@ConfigurationProperties("management.observations.r2dbc") |
||||
public class R2dbcObservationProperties { |
||||
|
||||
/** |
||||
* Whether to tag actual query parameter values. |
||||
*/ |
||||
private boolean includeParameterValues; |
||||
|
||||
public boolean isIncludeParameterValues() { |
||||
return this.includeParameterValues; |
||||
} |
||||
|
||||
public void setIncludeParameterValues(boolean includeParameterValues) { |
||||
this.includeParameterValues = includeParameterValues; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,131 @@
@@ -0,0 +1,131 @@
|
||||
/* |
||||
* Copyright 2012-2023 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.boot.actuate.autoconfigure.r2dbc; |
||||
|
||||
import java.util.UUID; |
||||
import java.util.concurrent.atomic.AtomicReference; |
||||
|
||||
import io.micrometer.observation.Observation.Context; |
||||
import io.micrometer.observation.ObservationHandler; |
||||
import io.micrometer.observation.ObservationRegistry; |
||||
import io.r2dbc.spi.ConnectionFactory; |
||||
import org.awaitility.Awaitility; |
||||
import org.hamcrest.Matchers; |
||||
import org.junit.jupiter.api.Test; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration; |
||||
import org.springframework.boot.autoconfigure.AutoConfigurations; |
||||
import org.springframework.boot.context.annotation.ImportCandidates; |
||||
import org.springframework.boot.r2dbc.ConnectionFactoryBuilder; |
||||
import org.springframework.boot.r2dbc.ConnectionFactoryDecorator; |
||||
import org.springframework.boot.test.context.FilteredClassLoader; |
||||
import org.springframework.boot.test.context.assertj.AssertableApplicationContext; |
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link R2dbcObservationAutoConfiguration}. |
||||
* |
||||
* @author Moritz Halbritter |
||||
*/ |
||||
class R2dbcObservationAutoConfigurationTests { |
||||
|
||||
private final ApplicationContextRunner runnerWithoutObservationRegistry = new ApplicationContextRunner() |
||||
.withConfiguration(AutoConfigurations.of(R2dbcObservationAutoConfiguration.class)); |
||||
|
||||
private final ApplicationContextRunner runner = this.runnerWithoutObservationRegistry |
||||
.withBean(ObservationRegistry.class, ObservationRegistry::create); |
||||
|
||||
@Test |
||||
void shouldBeRegisteredInAutoConfigurationImports() { |
||||
assertThat(ImportCandidates.load(AutoConfiguration.class, null).getCandidates()) |
||||
.contains(R2dbcObservationAutoConfiguration.class.getName()); |
||||
} |
||||
|
||||
@Test |
||||
void shouldSupplyConnectionFactoryDecorator() { |
||||
this.runner.run((context) -> assertThat(context).hasSingleBean(ConnectionFactoryDecorator.class)); |
||||
} |
||||
|
||||
@Test |
||||
void shouldNotSupplyBeansIfR2dbcSpiIsNotOnClasspath() { |
||||
this.runner.withClassLoader(new FilteredClassLoader("io.r2dbc.spi")) |
||||
.run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactoryDecorator.class)); |
||||
} |
||||
|
||||
@Test |
||||
void shouldNotSupplyBeansIfR2dbcProxyIsNotOnClasspath() { |
||||
this.runner.withClassLoader(new FilteredClassLoader("io.r2dbc.proxy")) |
||||
.run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactoryDecorator.class)); |
||||
} |
||||
|
||||
@Test |
||||
void shouldNotSupplyBeansIfObservationRegistryIsNotPresent() { |
||||
this.runnerWithoutObservationRegistry |
||||
.run((context) -> assertThat(context).doesNotHaveBean(ConnectionFactoryDecorator.class)); |
||||
} |
||||
|
||||
@Test |
||||
void decoratorShouldReportObservations() { |
||||
this.runner.run((context) -> { |
||||
CapturingObservationHandler handler = registerCapturingObservationHandler(context); |
||||
ConnectionFactoryDecorator decorator = context.getBean(ConnectionFactoryDecorator.class); |
||||
assertThat(decorator).isNotNull(); |
||||
ConnectionFactory connectionFactory = ConnectionFactoryBuilder |
||||
.withUrl("r2dbc:h2:mem:///" + UUID.randomUUID()) |
||||
.build(); |
||||
ConnectionFactory decorated = decorator.decorate(connectionFactory); |
||||
Mono.from(decorated.create()) |
||||
.flatMap((c) -> Mono.from(c.createStatement("SELECT 1;").execute()) |
||||
.flatMap((ignore) -> Mono.from(c.close()))) |
||||
.block(); |
||||
assertThat(handler.awaitContext().getName()).as("context.getName()").isEqualTo("r2dbc.query"); |
||||
}); |
||||
} |
||||
|
||||
private static CapturingObservationHandler registerCapturingObservationHandler( |
||||
AssertableApplicationContext context) { |
||||
ObservationRegistry observationRegistry = context.getBean(ObservationRegistry.class); |
||||
assertThat(observationRegistry).isNotNull(); |
||||
CapturingObservationHandler handler = new CapturingObservationHandler(); |
||||
observationRegistry.observationConfig().observationHandler(handler); |
||||
return handler; |
||||
} |
||||
|
||||
private static class CapturingObservationHandler implements ObservationHandler<Context> { |
||||
|
||||
private final AtomicReference<Context> context = new AtomicReference<>(); |
||||
|
||||
@Override |
||||
public boolean supportsContext(Context context) { |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public void onStart(Context context) { |
||||
this.context.set(context); |
||||
} |
||||
|
||||
Context awaitContext() { |
||||
return Awaitility.await().untilAtomic(this.context, Matchers.notNullValue()); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
/* |
||||
* Copyright 2012-2023 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.boot.r2dbc; |
||||
|
||||
import io.r2dbc.spi.ConnectionFactory; |
||||
|
||||
/** |
||||
* Decorator for {@link ConnectionFactory connection factories}. |
||||
* |
||||
* @author Moritz Halbritter |
||||
* @since 3.2.0 |
||||
* @see ConnectionFactoryBuilder |
||||
*/ |
||||
@FunctionalInterface |
||||
public interface ConnectionFactoryDecorator { |
||||
|
||||
/** |
||||
* Decorates the given {@link ConnectionFactory}. |
||||
* @param delegate the connection factory which should be decorated |
||||
* @return the decorated connection factory |
||||
*/ |
||||
ConnectionFactory decorate(ConnectionFactory delegate); |
||||
|
||||
} |
||||
Loading…
Reference in new issue