diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MongoMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MongoMetricsAutoConfiguration.java new file mode 100644 index 000000000000..5be419b23ddd --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/MongoMetricsAutoConfiguration.java @@ -0,0 +1,108 @@ +/* + * 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; + +import com.mongodb.MongoClientSettings; +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.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +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.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.autoconfigure.mongo.MongoClientSettingsBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Mongo metrics. + * + * @author Chris Bono + * @author Jonatan Ivanov + * @since 2.5.0 + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureBefore(MongoAutoConfiguration.class) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class }) +@ConditionalOnClass(MongoClientSettings.class) +@ConditionalOnBean(MeterRegistry.class) +public class MongoMetricsAutoConfiguration { + + @ConditionalOnClass(MongoMetricsCommandListener.class) + @ConditionalOnProperty(name = "management.metrics.mongo.command.enabled", havingValue = "true", + matchIfMissing = true) + static class MongoCommandMetricsConfiguration { + + @Bean + @ConditionalOnMissingBean + MongoMetricsCommandListener mongoMetricsCommandListener(MeterRegistry meterRegistry, + MongoMetricsCommandTagsProvider mongoMetricsCommandTagsProvider) { + return new MongoMetricsCommandListener(meterRegistry, mongoMetricsCommandTagsProvider); + } + + @Bean + @ConditionalOnMissingBean + MongoMetricsCommandTagsProvider mongoMetricsCommandTagsProvider() { + return new DefaultMongoMetricsCommandTagsProvider(); + } + + @Bean + MongoClientSettingsBuilderCustomizer mongoMetricsCommandListenerClientSettingsBuilderCustomizer( + MongoMetricsCommandListener mongoMetricsCommandListener) { + return (clientSettingsBuilder) -> clientSettingsBuilder.addCommandListener(mongoMetricsCommandListener); + } + + } + + @ConditionalOnClass(MongoMetricsConnectionPoolListener.class) + @ConditionalOnProperty(name = "management.metrics.mongo.connectionpool.enabled", havingValue = "true", + matchIfMissing = true) + static class MongoConnectionPoolMetricsConfiguration { + + @Bean + @ConditionalOnMissingBean + MongoMetricsConnectionPoolListener mongoMetricsConnectionPoolListener(MeterRegistry meterRegistry, + MongoMetricsConnectionPoolTagsProvider mongoMetricsConnectionPoolTagsProvider) { + return new MongoMetricsConnectionPoolListener(meterRegistry, mongoMetricsConnectionPoolTagsProvider); + } + + @Bean + @ConditionalOnMissingBean + MongoMetricsConnectionPoolTagsProvider mongoMetricsConnectionPoolTagsProvider() { + return new DefaultMongoMetricsConnectionPoolTagsProvider(); + } + + @Bean + MongoClientSettingsBuilderCustomizer mongoMetricsConnectionPoolListenerClientSettingsBuilderCustomizer( + MongoMetricsConnectionPoolListener mongoMetricsConnectionPoolListener) { + return (clientSettingsBuilder) -> clientSettingsBuilder + .applyToConnectionPoolSettings((connectionPoolSettingsBuilder) -> connectionPoolSettingsBuilder + .addConnectionPoolListener(mongoMetricsConnectionPoolListener)); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 1dc01fa69796..0193f7d73df2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -521,6 +521,16 @@ "level": "error" } }, + { + "name": "management.metrics.mongo.command.enabled", + "description": "Whether to enable Mongo client command metrics.", + "defaultValue": true + }, + { + "name": "management.metrics.mongo.connectionpool.enabled", + "description": "Whether to enable Mongo connection pool metrics.", + "defaultValue": true + }, { "name": "management.metrics.web.client.request.autotime.enabled", "description": "Whether to automatically time web client requests.", 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 d99ee8c7b9a4..748bd35340b2 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 @@ -46,6 +46,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.Log4J2MetricsAutoConfigur org.springframework.boot.actuate.autoconfigure.metrics.LogbackMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.MetricsEndpointAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.metrics.MongoMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.amqp.RabbitMetricsAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMetricsAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java index 6737aba194ae..81749178068e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MetricsAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-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. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MongoMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MongoMetricsAutoConfigurationTests.java new file mode 100644 index 000000000000..aeabc32e126b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/MongoMetricsAutoConfigurationTests.java @@ -0,0 +1,200 @@ +/* + * Copyright 2012-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.actuate.autoconfigure.metrics; + +import java.util.List; + +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.connection.ConnectionPoolSettings; +import com.mongodb.event.ConnectionPoolListener; +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.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.assertj.AssertableApplicationContext; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.test.context.runner.ContextConsumer; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link MongoMetricsAutoConfiguration}. + * + * @author Chris Bono + */ +class MongoMetricsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(MongoMetricsAutoConfiguration.class)); + + @Test + void whenThereIsAMeterRegistryThenMetricsCommandListenerIsAdded() { + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)).run((context) -> { + assertThat(context).hasSingleBean(MongoMetricsCommandListener.class); + assertThat(getActualMongoClientSettingsUsedToConstructClient(context)).isNotNull() + .extracting(MongoClientSettings::getCommandListeners).asList() + .containsExactly(context.getBean(MongoMetricsCommandListener.class)); + assertThat(getMongoMetricsCommandTagsProviderUsedToConstructListener(context)) + .isInstanceOf(DefaultMongoMetricsCommandTagsProvider.class); + }); + } + + @Test + void whenThereIsAMeterRegistryThenMetricsConnectionPoolListenerIsAdded() { + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)).run((context) -> { + assertThat(context).hasSingleBean(MongoMetricsConnectionPoolListener.class); + assertThat(getConnectionPoolListenersFromClient(context)) + .containsExactly(context.getBean(MongoMetricsConnectionPoolListener.class)); + assertThat(getMongoMetricsConnectionPoolTagsProviderUsedToConstructListener(context)) + .isInstanceOf(DefaultMongoMetricsConnectionPoolTagsProvider.class); + }); + } + + @Test + void whenThereIsNoMeterRegistryThenNoMetricsCommandListenerIsAdded() { + this.contextRunner.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .run((context) -> assertThatMetricsCommandListenerNotAdded()); + } + + @Test + void whenThereIsNoMeterRegistryThenNoMetricsConnectionPoolListenerIsAdded() { + this.contextRunner.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .run((context) -> assertThatMetricsConnectionPoolListenerNotAdded()); + } + + @Test + void whenThereIsACustomMetricsCommandTagsProviderItIsUsed() { + final MongoMetricsCommandTagsProvider customTagsProvider = mock(MongoMetricsCommandTagsProvider.class); + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withBean("customMongoMetricsCommandTagsProvider", MongoMetricsCommandTagsProvider.class, + () -> customTagsProvider) + .run((context) -> assertThat(getMongoMetricsCommandTagsProviderUsedToConstructListener(context)) + .isSameAs(customTagsProvider)); + } + + @Test + void whenThereIsACustomMetricsConnectionPoolTagsProviderItIsUsed() { + final MongoMetricsConnectionPoolTagsProvider customTagsProvider = mock( + MongoMetricsConnectionPoolTagsProvider.class); + this.contextRunner.with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withBean("customMongoMetricsConnectionPoolTagsProvider", MongoMetricsConnectionPoolTagsProvider.class, + () -> customTagsProvider) + .run((context) -> assertThat(getMongoMetricsConnectionPoolTagsProviderUsedToConstructListener(context)) + .isSameAs(customTagsProvider)); + } + + @Test + void whenThereIsNoMongoClientSettingsOnClasspathThenNoMetricsCommandListenerIsAdded() { + this.contextRunner.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(MongoClientSettings.class)) + .run((context) -> assertThatMetricsCommandListenerNotAdded()); + } + + @Test + void whenThereIsNoMongoClientSettingsOnClasspathThenNoMetricsConnectionPoolListenerIsAdded() { + this.contextRunner.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(MongoClientSettings.class)) + .run((context) -> assertThatMetricsConnectionPoolListenerNotAdded()); + } + + @Test + void whenThereIsNoMongoMetricsCommandListenerOnClasspathThenNoMetricsCommandListenerIsAdded() { + this.contextRunner.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(MongoMetricsCommandListener.class)) + .run((context) -> assertThatMetricsCommandListenerNotAdded()); + } + + @Test + void whenThereIsNoMongoMetricsConnectionPoolListenerOnClasspathThenNoMetricsConnectionPoolListenerIsAdded() { + this.contextRunner.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(MongoMetricsConnectionPoolListener.class)) + .run((context) -> assertThatMetricsConnectionPoolListenerNotAdded()); + } + + @Test + void whenMetricsCommandListenerEnabledPropertyFalseThenNoMetricsCommandListenerIsAdded() { + this.contextRunner.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withPropertyValues("management.metrics.mongo.command.enabled:false") + .run((context) -> assertThatMetricsCommandListenerNotAdded()); + } + + @Test + void whenMetricsConnectionPoolListenerEnabledPropertyFalseThenNoMetricsConnectionPoolListenerIsAdded() { + this.contextRunner.withConfiguration(AutoConfigurations.of(MongoAutoConfiguration.class)) + .withPropertyValues("management.metrics.mongo.connectionpool.enabled:false") + .run((context) -> assertThatMetricsConnectionPoolListenerNotAdded()); + } + + private ContextConsumer assertThatMetricsCommandListenerNotAdded() { + return (context) -> { + assertThat(context).doesNotHaveBean(MongoMetricsCommandListener.class); + assertThat(getActualMongoClientSettingsUsedToConstructClient(context)).isNotNull() + .extracting(MongoClientSettings::getCommandListeners).asList().isEmpty(); + }; + } + + private ContextConsumer assertThatMetricsConnectionPoolListenerNotAdded() { + return (context) -> { + assertThat(context).doesNotHaveBean(MongoMetricsConnectionPoolListener.class); + assertThat(getConnectionPoolListenersFromClient(context)).isEmpty(); + }; + } + + private MongoClientSettings getActualMongoClientSettingsUsedToConstructClient( + final AssertableApplicationContext context) { + final MongoClient mongoClient = context.getBean(MongoClient.class); + return (MongoClientSettings) ReflectionTestUtils.getField(mongoClient, "settings"); + } + + private List getConnectionPoolListenersFromClient( + final AssertableApplicationContext context) { + MongoClientSettings mongoClientSettings = getActualMongoClientSettingsUsedToConstructClient(context); + ConnectionPoolSettings connectionPoolSettings = mongoClientSettings.getConnectionPoolSettings(); + @SuppressWarnings("unchecked") + List listeners = (List) ReflectionTestUtils + .getField(connectionPoolSettings, "connectionPoolListeners"); + return listeners; + } + + private MongoMetricsCommandTagsProvider getMongoMetricsCommandTagsProviderUsedToConstructListener( + final AssertableApplicationContext context) { + MongoMetricsCommandListener listener = context.getBean(MongoMetricsCommandListener.class); + return (MongoMetricsCommandTagsProvider) ReflectionTestUtils.getField(listener, "tagsProvider"); + } + + private MongoMetricsConnectionPoolTagsProvider getMongoMetricsConnectionPoolTagsProviderUsedToConstructListener( + final AssertableApplicationContext context) { + MongoMetricsConnectionPoolListener listener = context.getBean(MongoMetricsConnectionPoolListener.class); + return (MongoMetricsConnectionPoolTagsProvider) ReflectionTestUtils.getField(listener, "tagsProvider"); + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc index 0db7777e0297..8b6d3ee1fcb1 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/production-ready-features.adoc @@ -2129,6 +2129,7 @@ Spring Boot registers the following core metrics when applicable: * Kafka consumer, producer, and streams metrics * Log4j2 metrics: record the number of events logged to Log4j2 at each level * Logback metrics: record the number of events logged to Logback at each level +* MongoDB metrics for all commands issued from the client and for client connection pool information * Uptime metrics: report a gauge for uptime and a fixed gauge representing the application's absolute start time * Tomcat metrics (`server.tomcat.mbeanregistry.enabled` must be set to `true` for all Tomcat metrics to be registered) * {spring-integration-docs}system-management.html#micrometer-integration[Spring Integration] metrics @@ -2387,6 +2388,94 @@ For more details refer to {spring-kafka-docs}#micrometer-native[Micrometer Nativ +[[production-ready-metrics-mongodb]] +==== MongoDB Metrics + +===== Command Metrics +Auto-configuration will register a `MongoMetricsCommandListener` for the auto-configured MongoClient. + +A timer metric with the name `mongodb.driver.commands` is created for each command issued to the underlying MongoDB driver. +Each metric is tagged with the following information by default: +|=== +| Tag | Description + +| `command` +| Name of the command issued + +| `cluster.id` +| Identifier of the cluster the command was sent to + +| `server.address` +| Address of the server the command was sent to + +| `status` +| Outcome of the command - one of (`SUCCESS`, `FAILED`) +|=== + +You can replace the default metric tags by providing a `MongoMetricsCommandTagsProvider` bean, as shown in the following example: + +[source,java,pending-extract=true,indent=0] +---- + @Bean + MongoMetricsCommandTagsProvider mongoMetricsCommandTagsProvider() { + return new MyCustomMongoMetricsCommandTagsProvider(); + } +---- + +If you want to disable the auto-configured command metrics, you can set the following property: + +[source,yaml,indent=0,configprops,configblocks] +---- + management: + metrics: + mongo: + command: + enabled: false +---- + +===== Connection Pool Metrics +Auto-configuration will register a `MongoMetricsConnectionPoolListener` for the auto-configured MongoClient. + +The following gauge metrics are created for the connection pool: + +* `mongodb.driver.pool.size` that reports the current size of the connection pool, including idle and and in-use members +* `mongodb.driver.pool.checkedout` that reports the count of connections that are currently in use +* `mongodb.driver.pool.waitqueuesize` that reports the current size of the wait queue for a connection from the pool + +Each metric is tagged with the following information by default: +|=== +| Tag | Description + +| `cluster.id` +| Identifier of the cluster the connection pool corresponds to + +| `server.address` +| Address of the server the connection pool corresponds to +|=== + +You can replace the default metric tags by providing a `MongoMetricsConnectionPoolTagsProvider` bean, as shown in the following example: + +[source,java,pending-extract=true,indent=0] +---- + @Bean + @ConditionalOnMissingBean + MongoMetricsConnectionPoolTagsProvider mongoMetricsConnectionPoolTagsProvider() { + return new DefaultMongoMetricsConnectionPoolTagsProvider(); + } +---- + +If you want to disable the auto-configured connection pool metrics, you can set the following property: + +[source,yaml,indent=0,configprops,configblocks] +---- + management: + metrics: + mongo: + connectionpool: + enabled: false +---- + + [[production-ready-metrics-custom]] === Registering custom metrics To register custom metrics, inject `MeterRegistry` into your component, as shown in the following example: