diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfiguration.java index 9b1f2acfe5f6..b6eae1687c6b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfiguration.java @@ -16,15 +16,27 @@ package org.springframework.boot.autoconfigure.mongo; +import java.util.List; import java.util.stream.Collectors; import com.mongodb.MongoClientSettings; import com.mongodb.client.MongoClient; +import com.mongodb.event.CommandListener; +import com.mongodb.event.ConnectionPoolListener; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.mongodb.DefaultMongoMetricsCommandTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.DefaultMongoMetricsConnectionPoolTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.MongoMetricsCommandListener; +import io.micrometer.core.instrument.binder.mongodb.MongoMetricsCommandTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.MongoMetricsConnectionPoolListener; +import io.micrometer.core.instrument.binder.mongodb.MongoMetricsConnectionPoolTagsProvider; import org.springframework.beans.factory.ObjectProvider; 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.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -39,6 +51,7 @@ * @author Mark Paluch * @author Stephane Nicoll * @author Scott Frederick + * @author Jonatan Ivanov * @since 1.0.0 */ @Configuration(proxyBeanMethods = false) @@ -70,6 +83,54 @@ MongoPropertiesClientSettingsBuilderCustomizer mongoPropertiesCustomizer(MongoPr return new MongoPropertiesClientSettingsBuilderCustomizer(properties, environment); } + @Bean + @ConditionalOnBean(CommandListener.class) + MongoCommandListenerClientSettingsBuilderCustomizer mongoCommandListenerClientSettingsBuilderCustomizer( + List commandListeners) { + return new MongoCommandListenerClientSettingsBuilderCustomizer(commandListeners); + } + + @Bean + @ConditionalOnBean(ConnectionPoolListener.class) + MongoConnectionPoolListenerClientSettingsBuilderCustomizer mongoConnectionPoolListenerClientSettingsBuilderCustomizer( + List connectionPoolListeners) { + return new MongoConnectionPoolListenerClientSettingsBuilderCustomizer(connectionPoolListeners); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnBean(MeterRegistry.class) + // TODO: Add to MongoProperties? + @ConditionalOnProperty(value = "spring.data.mongodb.metrics.enabled", matchIfMissing = true) + static class MongoMetricsConfiguration { + + @Bean + @ConditionalOnMissingBean + MongoMetricsCommandListener mongoMetricsCommandListener(MeterRegistry meterRegistry, + MongoMetricsCommandTagsProvider mongoMetricsCommandTagsProvider) { + return new MongoMetricsCommandListener(meterRegistry, mongoMetricsCommandTagsProvider); + } + + @Bean + @ConditionalOnMissingBean + MongoMetricsCommandTagsProvider mongoMetricsCommandTagsProvider() { + return new DefaultMongoMetricsCommandTagsProvider(); + } + + @Bean + @ConditionalOnMissingBean + MongoMetricsConnectionPoolListener mongoMetricsConnectionPoolListener(MeterRegistry meterRegistry, + MongoMetricsConnectionPoolTagsProvider mongoMetricsConnectionPoolTagsProvider) { + return new MongoMetricsConnectionPoolListener(meterRegistry, mongoMetricsConnectionPoolTagsProvider); + } + + @Bean + @ConditionalOnMissingBean + MongoMetricsConnectionPoolTagsProvider mongoMetricsConnectionPoolTagsProvider() { + return new DefaultMongoMetricsConnectionPoolTagsProvider(); + } + + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoCommandListenerClientSettingsBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoCommandListenerClientSettingsBuilderCustomizer.java new file mode 100644 index 000000000000..5ad3b03db43d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoCommandListenerClientSettingsBuilderCustomizer.java @@ -0,0 +1,45 @@ +/* + * Copyright 2021 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.autoconfigure.mongo; + +import com.mongodb.MongoClientSettings; +import com.mongodb.event.CommandListener; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Adds {@link CommandListener} instances to {@link MongoClientSettings} through + * {@link MongoClientSettingsBuilderCustomizer}. + * + * @author Jonatan Ivanov + * @since 2.5.0 + */ +public class MongoCommandListenerClientSettingsBuilderCustomizer implements MongoClientSettingsBuilderCustomizer { + + private final Iterable commandListeners; + + public MongoCommandListenerClientSettingsBuilderCustomizer(@NonNull Iterable commandListeners) { + this.commandListeners = commandListeners; + } + + @Override + public void customize(MongoClientSettings.Builder clientSettingsBuilder) { + for (CommandListener commandListener : this.commandListeners) { + clientSettingsBuilder.addCommandListener(commandListener); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoConnectionPoolListenerClientSettingsBuilderCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoConnectionPoolListenerClientSettingsBuilderCustomizer.java new file mode 100644 index 000000000000..d390f58d524a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mongo/MongoConnectionPoolListenerClientSettingsBuilderCustomizer.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021 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.autoconfigure.mongo; + +import com.mongodb.MongoClientSettings; +import com.mongodb.event.ConnectionPoolListener; + +import org.springframework.lang.NonNull; + +/** + * Adds {@link ConnectionPoolListener} instances to {@link MongoClientSettings} through + * {@link MongoClientSettingsBuilderCustomizer}. + * + * @author Jonatan Ivanov + * @since 2.5.0 + */ +public class MongoConnectionPoolListenerClientSettingsBuilderCustomizer + implements MongoClientSettingsBuilderCustomizer { + + private final Iterable connectionPoolListeners; + + public MongoConnectionPoolListenerClientSettingsBuilderCustomizer( + @NonNull Iterable connectionPoolListeners) { + this.connectionPoolListeners = connectionPoolListeners; + } + + @Override + public void customize(MongoClientSettings.Builder clientSettingsBuilder) { + for (ConnectionPoolListener connectionPoolListener : this.connectionPoolListeners) { + clientSettingsBuilder.applyToConnectionPoolSettings( + (builder) -> builder.addConnectionPoolListener(connectionPoolListener)); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfigurationTests.java index e44f4cce13a7..2338df891982 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoAutoConfigurationTests.java @@ -21,9 +21,20 @@ import com.mongodb.MongoClientSettings; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; +import com.mongodb.event.CommandListener; +import com.mongodb.event.ConnectionPoolListener; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.binder.mongodb.DefaultMongoMetricsCommandTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.DefaultMongoMetricsConnectionPoolTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.MongoMetricsCommandListener; +import io.micrometer.core.instrument.binder.mongodb.MongoMetricsCommandTagsProvider; +import io.micrometer.core.instrument.binder.mongodb.MongoMetricsConnectionPoolListener; +import io.micrometer.core.instrument.binder.mongodb.MongoMetricsConnectionPoolTagsProvider; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; @@ -38,6 +49,7 @@ * @author Dave Syer * @author Stephane Nicoll * @author Scott Frederick + * @author Jonatan Ivanov */ class MongoAutoConfigurationTests { @@ -84,6 +96,62 @@ void customizerOverridesAutoConfig() { .run((context) -> assertThat(getSettings(context).getApplicationName()).isEqualTo("overridden-name")); } + @Test + void metricsBeansShouldBeCreatedIfMeterRegistryExists() { + this.contextRunner.withUserConfiguration(MetricsConfig.class).run((context) -> assertThat(context) + .hasSingleBean(MeterRegistry.class).hasSingleBean(MongoMetricsConnectionPoolTagsProvider.class) + .hasSingleBean(MongoMetricsConnectionPoolListener.class) + .hasSingleBean(MongoMetricsCommandTagsProvider.class).hasSingleBean(MongoMetricsCommandListener.class) + .hasSingleBean(MongoConnectionPoolListenerClientSettingsBuilderCustomizer.class) + .hasSingleBean(MongoCommandListenerClientSettingsBuilderCustomizer.class)); + } + + @Test + void metricsBeansShouldNotBeCreatedIfMeterRegistryDoesNotExist() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(MeterRegistry.class) + .doesNotHaveBean(MongoMetricsConnectionPoolTagsProvider.class) + .doesNotHaveBean(MongoMetricsConnectionPoolListener.class) + .doesNotHaveBean(MongoMetricsCommandTagsProvider.class) + .doesNotHaveBean(MongoMetricsCommandListener.class) + .doesNotHaveBean(MongoConnectionPoolListenerClientSettingsBuilderCustomizer.class) + .doesNotHaveBean(MongoCommandListenerClientSettingsBuilderCustomizer.class)); + } + + @Test + void metricsBeansShouldNotBeCreatedIfMongoMetricsDisabled() { + this.contextRunner.withUserConfiguration(MetricsConfig.class) + .withPropertyValues("spring.data.mongodb.metrics.enabled=false") + .run((context) -> assertThat(context).doesNotHaveBean(MongoMetricsConnectionPoolTagsProvider.class) + .doesNotHaveBean(MongoMetricsConnectionPoolListener.class) + .doesNotHaveBean(MongoMetricsCommandTagsProvider.class) + .doesNotHaveBean(MongoMetricsCommandListener.class) + .doesNotHaveBean(MongoConnectionPoolListenerClientSettingsBuilderCustomizer.class) + .doesNotHaveBean(MongoCommandListenerClientSettingsBuilderCustomizer.class)); + } + + @Test + void mongoListenerCustomizersShouldBeCreatedIfListenersExist() { + this.contextRunner.withUserConfiguration(MongoListenersConfig.class) + .run((context) -> assertThat(context).doesNotHaveBean(MeterRegistry.class) + .doesNotHaveBean(MongoMetricsConnectionPoolTagsProvider.class) + .doesNotHaveBean(MongoMetricsConnectionPoolListener.class) + .doesNotHaveBean(MongoMetricsCommandTagsProvider.class) + .doesNotHaveBean(MongoMetricsCommandListener.class).hasSingleBean(ConnectionPoolListener.class) + .hasSingleBean(CommandListener.class) + .hasSingleBean(MongoConnectionPoolListenerClientSettingsBuilderCustomizer.class) + .hasSingleBean(MongoCommandListenerClientSettingsBuilderCustomizer.class)); + } + + @Test + void fallBackMetricsBeansShouldBeUsedIfTheyExists() { + this.contextRunner.withUserConfiguration(FallbackMetricsConfig.class).run((context) -> assertThat(context) + .hasSingleBean(MeterRegistry.class).hasSingleBean(MongoMetricsConnectionPoolTagsProvider.class) + .hasSingleBean(MongoMetricsConnectionPoolListener.class) + .hasSingleBean(MongoMetricsCommandTagsProvider.class).hasSingleBean(MongoMetricsCommandListener.class) + .hasSingleBean(MongoConnectionPoolListenerClientSettingsBuilderCustomizer.class) + .hasSingleBean(MongoCommandListenerClientSettingsBuilderCustomizer.class)); + } + private MongoClientSettings getSettings(AssertableApplicationContext context) { assertThat(context).hasSingleBean(MongoClient.class); MongoClient client = context.getBean(MongoClient.class); @@ -131,4 +199,65 @@ MongoClientSettingsBuilderCustomizer customizer() { } + @Configuration(proxyBeanMethods = false) + static class MetricsConfig { + + @Bean + MeterRegistry meterRegistry() { + return new SimpleMeterRegistry(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class MongoListenersConfig { + + @Bean + ConnectionPoolListener connectionPoolListener() { + return new ConnectionPoolListener() { + }; + } + + @Bean + CommandListener commandListener() { + return new CommandListener() { + }; + } + + } + + @Configuration(proxyBeanMethods = false) + static class FallbackMetricsConfig { + + @Bean + MeterRegistry meterRegistry() { + return new SimpleMeterRegistry(); + } + + @Bean + MongoMetricsCommandListener mongoMetricsCommandListener(MeterRegistry meterRegistry, + MongoMetricsCommandTagsProvider mongoMetricsCommandTagsProvider) { + return new MongoMetricsCommandListener(meterRegistry, mongoMetricsCommandTagsProvider); + } + + @Bean + MongoMetricsCommandTagsProvider mongoMetricsCommandTagsProvider() { + return new DefaultMongoMetricsCommandTagsProvider(); + } + + @Bean + @ConditionalOnMissingBean + MongoMetricsConnectionPoolListener mongoMetricsConnectionPoolListener(MeterRegistry meterRegistry, + MongoMetricsConnectionPoolTagsProvider mongoMetricsConnectionPoolTagsProvider) { + return new MongoMetricsConnectionPoolListener(meterRegistry, mongoMetricsConnectionPoolTagsProvider); + } + + @Bean + @ConditionalOnMissingBean + MongoMetricsConnectionPoolTagsProvider mongoMetricsConnectionPoolTagsProvider() { + return new DefaultMongoMetricsConnectionPoolTagsProvider(); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoCommandListenerClientSettingsBuilderCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoCommandListenerClientSettingsBuilderCustomizerTests.java new file mode 100644 index 000000000000..c304edd39a15 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoCommandListenerClientSettingsBuilderCustomizerTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2021-2021 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.autoconfigure.mongo; + +import java.util.List; + +import com.mongodb.MongoClientSettings; +import com.mongodb.event.CommandListener; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MongoCommandListenerClientSettingsBuilderCustomizer}. + * + * @author Jonatan Ivanov + */ +class MongoCommandListenerClientSettingsBuilderCustomizerTests { + + @Test + void shouldNotSetUpAnyCommandListeners() { + MongoClientSettings mongoClientSettings = buildAndCustomizeMongoClientSettings(Lists.list()); + assertThat(mongoClientSettings.getCommandListeners()).isEmpty(); + } + + @Test + void shouldSetUpCommandListeners() { + List commandListeners = Lists.list(createCommandListener(), createCommandListener(), + createCommandListener()); + MongoClientSettings mongoClientSettings = buildAndCustomizeMongoClientSettings(commandListeners); + assertThat(mongoClientSettings.getCommandListeners()).isEqualTo(commandListeners); + } + + private MongoClientSettings buildAndCustomizeMongoClientSettings(List listeners) { + MongoClientSettings.Builder builder = MongoClientSettings.builder(); + new MongoCommandListenerClientSettingsBuilderCustomizer(listeners).customize(builder); + + return builder.build(); + } + + private CommandListener createCommandListener() { + return new CommandListener() { + }; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoConnectionPoolListenerClientSettingsBuilderCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoConnectionPoolListenerClientSettingsBuilderCustomizerTests.java new file mode 100644 index 000000000000..e7acd9f5e36a --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/mongo/MongoConnectionPoolListenerClientSettingsBuilderCustomizerTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021-2021 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.autoconfigure.mongo; + +import java.util.List; + +import com.mongodb.MongoClientSettings; +import com.mongodb.connection.ConnectionPoolSettings; +import com.mongodb.event.ConnectionPoolListener; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MongoConnectionPoolListenerClientSettingsBuilderCustomizer}. + * + * @author Jonatan Ivanov + */ +public class MongoConnectionPoolListenerClientSettingsBuilderCustomizerTests { + + @Test + void shouldNotSetUpAnyConnectionPoolListeners() { + ConnectionPoolSettings connectionPoolSettings = buildAndCustomizeMongoClientSettings(Lists.list()); + assertThat(connectionPoolSettings.getConnectionPoolListeners()).isEmpty(); + } + + @Test + void shouldSetUpConnectionPoolListeners() { + List connectionPoolListeners = Lists.list(createConnectionPoolListener(), + createConnectionPoolListener(), createConnectionPoolListener()); + ConnectionPoolSettings connectionPoolSettings = buildAndCustomizeMongoClientSettings(connectionPoolListeners); + assertThat(connectionPoolSettings.getConnectionPoolListeners()).isEqualTo(connectionPoolListeners); + } + + private ConnectionPoolSettings buildAndCustomizeMongoClientSettings(List listeners) { + MongoClientSettings.Builder builder = MongoClientSettings.builder(); + new MongoConnectionPoolListenerClientSettingsBuilderCustomizer(listeners).customize(builder); + + return builder.build().getConnectionPoolSettings(); + } + + private ConnectionPoolListener createConnectionPoolListener() { + return new ConnectionPoolListener() { + }; + } + +}