diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index f0039e9303c..41b31ac249d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -61,6 +61,7 @@ dependencies { optional("io.micrometer:micrometer-registry-statsd") optional("io.micrometer:micrometer-registry-wavefront") optional("io.projectreactor.netty:reactor-netty") + optional("io.r2dbc:r2dbc-pool") optional("io.r2dbc:r2dbc-spi") optional("jakarta.jms:jakarta.jms-api") optional("jakarta.servlet:jakarta.servlet-api") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/ConnectionPoolMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/ConnectionPoolMetricsAutoConfiguration.java new file mode 100644 index 00000000000..931ecc9da31 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/ConnectionPoolMetricsAutoConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2020 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.metrics.r2dbc; + +import java.util.Map; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.r2dbc.pool.ConnectionPool; +import io.r2dbc.spi.ConnectionFactory; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +import org.springframework.boot.actuate.metrics.r2dbc.ConnectionPoolMetrics; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for metrics on all available + * {@link ConnectionFactory R2DBC connection factories}. + * + * @author Tadaya Tsuyukubo + * @author Stephane Nicoll + * @since 2.3.0 + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class, + R2dbcAutoConfiguration.class }) +@ConditionalOnClass({ ConnectionPool.class, MeterRegistry.class }) +@ConditionalOnBean({ ConnectionPool.class, MeterRegistry.class }) +public class ConnectionPoolMetricsAutoConfiguration { + + @Autowired + public void bindConnectionPoolsToRegistry(Map connectionPools, MeterRegistry registry) { + connectionPools.forEach((beanName, + connectionPool) -> new ConnectionPoolMetrics(connectionPool, beanName, Tags.empty()).bindTo(registry)); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/package-info.java new file mode 100644 index 00000000000..14f5c1fe0b1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 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. + */ + +/** + * Auto-configuration for R2DBC metrics. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.r2dbc; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index ab776cb6d2f..c14ab114342 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -67,6 +67,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront.Wavefron org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.jersey.JerseyServerMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.metrics.r2dbc.ConnectionPoolMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.client.HttpClientMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.jetty.JettyMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.web.reactive.WebFluxMetricsAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/ConnectionPoolMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/ConnectionPoolMetricsAutoConfigurationTests.java new file mode 100644 index 00000000000..acb4697fdcd --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/r2dbc/ConnectionPoolMetricsAutoConfigurationTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-2020 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.metrics.r2dbc; + +import java.util.Collections; +import java.util.UUID; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.r2dbc.h2.CloseableConnectionFactory; +import io.r2dbc.h2.H2ConnectionFactory; +import io.r2dbc.h2.H2ConnectionOption; +import io.r2dbc.pool.ConnectionPool; +import io.r2dbc.pool.ConnectionPoolConfiguration; +import io.r2dbc.spi.ConnectionFactory; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConnectionPoolMetricsAutoConfiguration}. + * + * @author Tadaya Tsuyukubo + * @author Stephane Nicoll + */ +class ConnectionPoolMetricsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.r2dbc.generate-unique-name=true").with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(ConnectionPoolMetricsAutoConfiguration.class)) + .withUserConfiguration(BaseConfiguration.class); + + @Test + void autoConfiguredDataSourceIsInstrumented() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("r2dbc.pool.acquired").gauges()).hasSize(1); + }); + } + + @Test + void connectionPoolInstrumentationCanBeDisabled() { + this.contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .withPropertyValues("management.metrics.enable.r2dbc=false").run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("r2dbc.pool.acquired").gauge()).isNull(); + }); + } + + @Test + void allConnectionPoolsCanBeInstrumented() { + this.contextRunner.withUserConfiguration(TwoConnectionPoolsConfiguration.class).run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("r2dbc.pool.acquired").gauges()).extracting(Meter::getId) + .extracting((id) -> id.getTag("name")).containsExactlyInAnyOrder("firstPool", "secondPool"); + }); + } + + @Configuration(proxyBeanMethods = false) + static class BaseConfiguration { + + @Bean + SimpleMeterRegistry registry() { + return new SimpleMeterRegistry(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TwoConnectionPoolsConfiguration { + + @Bean + CloseableConnectionFactory connectionFactory() { + return H2ConnectionFactory.inMemory("db-" + UUID.randomUUID(), "sa", "", + Collections.singletonMap(H2ConnectionOption.DB_CLOSE_DELAY, "-1")); + } + + @Bean + ConnectionPool firstPool(ConnectionFactory connectionFactory) { + return new ConnectionPool(ConnectionPoolConfiguration.builder(connectionFactory).build()); + } + + @Bean + ConnectionPool secondPool(ConnectionFactory connectionFactory) { + return new ConnectionPool(ConnectionPoolConfiguration.builder(connectionFactory).build()); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/build.gradle b/spring-boot-project/spring-boot-actuator/build.gradle index 1fcf02e8e67..ae3cdc34e9a 100644 --- a/spring-boot-project/spring-boot-actuator/build.gradle +++ b/spring-boot-project/spring-boot-actuator/build.gradle @@ -24,6 +24,7 @@ dependencies { optional("io.micrometer:micrometer-core") optional("io.micrometer:micrometer-registry-prometheus") optional("io.prometheus:simpleclient_pushgateway") + optional("io.r2dbc:r2dbc-pool") optional("io.r2dbc:r2dbc-spi") optional("io.reactivex:rxjava-reactive-streams") optional("org.elasticsearch.client:elasticsearch-rest-client") diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/r2dbc/ConnectionPoolMetrics.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/r2dbc/ConnectionPoolMetrics.java new file mode 100644 index 00000000000..845ceae8eef --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/r2dbc/ConnectionPoolMetrics.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2020 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.metrics.r2dbc; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Gauge.Builder; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.binder.MeterBinder; +import io.r2dbc.pool.ConnectionPool; +import io.r2dbc.pool.PoolMetrics; + +/** + * A {@link MeterBinder} for a {@link ConnectionPool}. + * + * @author Tadaya Tsuyukubo + * @author Stephane Nicoll + * @since 2.3.0 + */ +public class ConnectionPoolMetrics implements MeterBinder { + + private static final String CONNECTIONS = "connections"; + + private final ConnectionPool pool; + + private final Iterable tags; + + public ConnectionPoolMetrics(ConnectionPool pool, String name, Iterable tags) { + this.pool = pool; + this.tags = Tags.concat(tags, "name", name); + } + + @Override + public void bindTo(MeterRegistry registry) { + this.pool.getMetrics().ifPresent((poolMetrics) -> { + bindConnectionPoolMetric(registry, + Gauge.builder(metricKey("acquired"), poolMetrics, PoolMetrics::acquiredSize) + .description("Size of successfully acquired connections which are in active use.")); + bindConnectionPoolMetric(registry, + Gauge.builder(metricKey("allocated"), poolMetrics, PoolMetrics::allocatedSize) + .description("Size of allocated connections in the pool which are in active use or idle.")); + bindConnectionPoolMetric(registry, Gauge.builder(metricKey("idle"), poolMetrics, PoolMetrics::idleSize) + .description("Size of idle connections in the pool.")); + bindConnectionPoolMetric(registry, + Gauge.builder(metricKey("pending"), poolMetrics, PoolMetrics::pendingAcquireSize).description( + "Size of pending to acquire connections from the underlying connection factory.")); + bindConnectionPoolMetric(registry, + Gauge.builder(metricKey("max.allocated"), poolMetrics, PoolMetrics::getMaxAllocatedSize) + .description("Maximum size of allocated connections that this pool allows.")); + bindConnectionPoolMetric(registry, + Gauge.builder(metricKey("max.pending"), poolMetrics, PoolMetrics::getMaxPendingAcquireSize) + .description( + "Maximum size of pending state to acquire connections that this pool allows.")); + }); + } + + private void bindConnectionPoolMetric(MeterRegistry registry, Builder builder) { + builder.tags(this.tags).baseUnit(CONNECTIONS).register(registry); + } + + private static String metricKey(String name) { + return "r2dbc.pool." + name; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/r2dbc/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/r2dbc/package-info.java new file mode 100644 index 00000000000..d0545dbb161 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/r2dbc/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 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. + */ + +/** + * Actuator support for R2DBC metrics. + */ +package org.springframework.boot.actuate.metrics.r2dbc; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/r2dbc/ConnectionPoolMetricsTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/r2dbc/ConnectionPoolMetricsTests.java new file mode 100644 index 00000000000..e9fd2f0267f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/r2dbc/ConnectionPoolMetricsTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2012-2020 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.metrics.r2dbc; + +import java.util.Collections; +import java.util.UUID; + +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.r2dbc.h2.CloseableConnectionFactory; +import io.r2dbc.h2.H2ConnectionFactory; +import io.r2dbc.h2.H2ConnectionOption; +import io.r2dbc.pool.ConnectionPool; +import io.r2dbc.pool.ConnectionPoolConfiguration; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ConnectionPoolMetrics}. + * + * @author Tadaya Tsuyukubo + * @author Mark Paluch + * @author Stephane Nicoll + */ +class ConnectionPoolMetricsTests { + + private static final Tag testTag = Tag.of("test", "yes"); + + private static final Tag regionTag = Tag.of("region", "eu-2"); + + private CloseableConnectionFactory connectionFactory; + + @BeforeEach + void init() { + this.connectionFactory = H2ConnectionFactory.inMemory("db-" + UUID.randomUUID(), "sa", "", + Collections.singletonMap(H2ConnectionOption.DB_CLOSE_DELAY, "-1")); + } + + @AfterEach + void close() { + if (this.connectionFactory != null) { + this.connectionFactory.close(); + } + } + + @Test + void connectionFactoryIsInstrumented() { + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + ConnectionPool connectionPool = new ConnectionPool( + ConnectionPoolConfiguration.builder(this.connectionFactory).initialSize(3).maxSize(7).build()); + ConnectionPoolMetrics metrics = new ConnectionPoolMetrics(connectionPool, "test-pool", + Tags.of(testTag, regionTag)); + metrics.bindTo(registry); + // acquire two connections + connectionPool.create().as(StepVerifier::create).expectNextCount(1).verifyComplete(); + connectionPool.create().as(StepVerifier::create).expectNextCount(1).verifyComplete(); + assertGauge(registry, "r2dbc.pool.acquired", 2); + assertGauge(registry, "r2dbc.pool.allocated", 3); + assertGauge(registry, "r2dbc.pool.idle", 1); + assertGauge(registry, "r2dbc.pool.pending", 0); + assertGauge(registry, "r2dbc.pool.max.allocated", 7); + assertGauge(registry, "r2dbc.pool.max.pending", Integer.MAX_VALUE); + } + + private void assertGauge(SimpleMeterRegistry registry, String metric, int expectedValue) { + Gauge gauge = registry.get(metric).gauge(); + assertThat(gauge.value()).isEqualTo(expectedValue); + assertThat(gauge.getId().getTags()).containsExactlyInAnyOrder(Tag.of("name", "test-pool"), testTag, regionTag); + } + +}