diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 47da1fd424..967e75fe3b 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -108,6 +108,7 @@ dependencies { } optional("org.flywaydb:flyway-core") optional("org.hibernate.orm:hibernate-core") + optional("org.hibernate.orm:hibernate-micrometer") optional("org.hibernate.validator:hibernate-validator") optional("org.influxdb:influxdb-java") optional("org.liquibase:liquibase-core") { diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfiguration.java new file mode 100644 index 0000000000..0d96bace0f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfiguration.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-2022 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.orm.jpa; + +import java.util.Collections; +import java.util.Map; + +import io.micrometer.core.instrument.MeterRegistry; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceException; +import org.hibernate.SessionFactory; +import org.hibernate.stat.HibernateMetrics; + +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; +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.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for metrics on all available + * Hibernate {@link EntityManagerFactory} instances that have statistics enabled. + * + * @author Rui Figueira + * @author Stephane Nicoll + * @since 2.1.0 + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, HibernateJpaAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class }) +@ConditionalOnClass({ EntityManagerFactory.class, SessionFactory.class, HibernateMetrics.class, MeterRegistry.class }) +@ConditionalOnBean({ EntityManagerFactory.class, MeterRegistry.class }) +public class HibernateMetricsAutoConfiguration implements SmartInitializingSingleton { + + private static final String ENTITY_MANAGER_FACTORY_SUFFIX = "entityManagerFactory"; + + private final Map entityManagerFactories; + + private final MeterRegistry meterRegistry; + + public HibernateMetricsAutoConfiguration(Map entityManagerFactories, + MeterRegistry meterRegistry) { + this.entityManagerFactories = entityManagerFactories; + this.meterRegistry = meterRegistry; + } + + @Override + public void afterSingletonsInstantiated() { + bindEntityManagerFactoriesToRegistry(this.entityManagerFactories, this.meterRegistry); + } + + public void bindEntityManagerFactoriesToRegistry(Map entityManagerFactories, + MeterRegistry registry) { + entityManagerFactories.forEach((name, factory) -> bindEntityManagerFactoryToRegistry(name, factory, registry)); + } + + private void bindEntityManagerFactoryToRegistry(String beanName, EntityManagerFactory entityManagerFactory, + MeterRegistry registry) { + String entityManagerFactoryName = getEntityManagerFactoryName(beanName); + try { + new HibernateMetrics(entityManagerFactory.unwrap(SessionFactory.class), entityManagerFactoryName, + Collections.emptyList()).bindTo(registry); + } + catch (PersistenceException ex) { + // Continue + } + } + + /** + * Get the name of an {@link EntityManagerFactory} based on its {@code beanName}. + * @param beanName the name of the {@link EntityManagerFactory} bean + * @return a name for the given entity manager factory + */ + private String getEntityManagerFactoryName(String beanName) { + if (beanName.length() > ENTITY_MANAGER_FACTORY_SUFFIX.length() + && StringUtils.endsWithIgnoreCase(beanName, ENTITY_MANAGER_FACTORY_SUFFIX)) { + return beanName.substring(0, beanName.length() - ENTITY_MANAGER_FACTORY_SUFFIX.length()); + } + return beanName; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/package-info.java new file mode 100644 index 0000000000..d52ff49399 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2022 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 JPA metrics. + */ +package org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 52263600c4..6fdfc58211 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -70,6 +70,7 @@ org.springframework.boot.actuate.autoconfigure.metrics.export.wavefront.Wavefron org.springframework.boot.actuate.autoconfigure.metrics.integration.IntegrationMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.mongo.MongoMetricsAutoConfiguration +org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.r2dbc.ConnectionPoolMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.redis.LettuceMetricsAutoConfiguration org.springframework.boot.actuate.autoconfigure.metrics.startup.StartupTimeMetricsListenerAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfigurationTests.java new file mode 100644 index 0000000000..9e97a097fb --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/orm/jpa/HibernateMetricsAutoConfigurationTests.java @@ -0,0 +1,207 @@ +/* + * Copyright 2012-2022 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.orm.jpa; + +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.PersistenceException; +import org.hibernate.SessionFactory; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatchers; + +import org.springframework.boot.actuate.autoconfigure.metrics.test.MetricsRun; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilderCustomizer; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration; +import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link HibernateMetricsAutoConfiguration}. + * + * @author Rui Figueira + * @author Stephane Nicoll + */ +class HibernateMetricsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().with(MetricsRun.simple()) + .withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class, + HibernateJpaAutoConfiguration.class, HibernateMetricsAutoConfiguration.class)) + .withUserConfiguration(BaseConfiguration.class); + + @Test + void autoConfiguredEntityManagerFactoryWithStatsIsInstrumented() { + this.contextRunner.withPropertyValues("spring.jpa.properties.hibernate.generate_statistics:true") + .run((context) -> { + MeterRegistry registry = context.getBean(MeterRegistry.class); + registry.get("hibernate.statements").tags("entityManagerFactory", "entityManagerFactory").meter(); + }); + } + + @Test + void autoConfiguredEntityManagerFactoryWithoutStatsIsNotInstrumented() { + this.contextRunner.withPropertyValues("spring.jpa.properties.hibernate.generate_statistics:false") + .run((context) -> { + context.getBean(EntityManagerFactory.class).unwrap(SessionFactory.class); + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("hibernate.statements").meter()).isNull(); + }); + } + + @Test + void entityManagerFactoryInstrumentationCanBeDisabled() { + this.contextRunner.withPropertyValues("management.metrics.enable.hibernate=false", + "spring.jpa.properties.hibernate.generate_statistics:true").run((context) -> { + context.getBean(EntityManagerFactory.class).unwrap(SessionFactory.class); + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("hibernate.statements").meter()).isNull(); + }); + } + + @Test + void allEntityManagerFactoriesCanBeInstrumented() { + this.contextRunner.withPropertyValues("spring.jpa.properties.hibernate.generate_statistics:true") + .withUserConfiguration(TwoEntityManagerFactoriesConfiguration.class).run((context) -> { + context.getBean("firstEntityManagerFactory", EntityManagerFactory.class) + .unwrap(SessionFactory.class); + context.getBean("secondOne", EntityManagerFactory.class).unwrap(SessionFactory.class); + MeterRegistry registry = context.getBean(MeterRegistry.class); + registry.get("hibernate.statements").tags("entityManagerFactory", "first").meter(); + registry.get("hibernate.statements").tags("entityManagerFactory", "secondOne").meter(); + }); + } + + @Test + void entityManagerFactoryInstrumentationIsDisabledIfNotHibernateSessionFactory() { + this.contextRunner.withPropertyValues("spring.jpa.properties.hibernate.generate_statistics:true") + .withUserConfiguration(NonHibernateEntityManagerFactoryConfiguration.class).run((context) -> { + // ensure EntityManagerFactory is not a Hibernate SessionFactory + assertThatThrownBy(() -> context.getBean(EntityManagerFactory.class).unwrap(SessionFactory.class)) + .isInstanceOf(PersistenceException.class); + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("hibernate.statements").meter()).isNull(); + }); + } + + @Test + void entityManagerFactoryInstrumentationIsDisabledIfHibernateIsNotAvailable() { + this.contextRunner.withClassLoader(new FilteredClassLoader(SessionFactory.class)) + .withUserConfiguration(NonHibernateEntityManagerFactoryConfiguration.class).run((context) -> { + assertThat(context).doesNotHaveBean(HibernateMetricsAutoConfiguration.class); + MeterRegistry registry = context.getBean(MeterRegistry.class); + assertThat(registry.find("hibernate.statements").meter()).isNull(); + }); + } + + @Test + void entityManagerFactoryInstrumentationDoesNotDeadlockWithDeferredInitialization() { + this.contextRunner.withPropertyValues("spring.jpa.properties.hibernate.generate_statistics:true", + "spring.sql.init.schema-locations:city-schema.sql", "spring.sql.init.data-locations=city-data.sql") + .withConfiguration(AutoConfigurations.of(SqlInitializationAutoConfiguration.class)) + .withBean(EntityManagerFactoryBuilderCustomizer.class, + () -> (builder) -> builder.setBootstrapExecutor(new SimpleAsyncTaskExecutor())) + .run((context) -> { + JdbcTemplate jdbcTemplate = new JdbcTemplate(context.getBean(DataSource.class)); + assertThat(jdbcTemplate.queryForObject("SELECT COUNT(*) from CITY", Integer.class)).isEqualTo(1); + MeterRegistry registry = context.getBean(MeterRegistry.class); + registry.get("hibernate.statements").tags("entityManagerFactory", "entityManagerFactory").meter(); + }); + } + + @Configuration(proxyBeanMethods = false) + static class BaseConfiguration { + + @Bean + SimpleMeterRegistry simpleMeterRegistry() { + return new SimpleMeterRegistry(); + } + + } + + @Entity + static class MyEntity { + + @Id + @GeneratedValue + private Long id; + + } + + @Configuration(proxyBeanMethods = false) + static class TwoEntityManagerFactoriesConfiguration { + + private static final Class[] PACKAGE_CLASSES = new Class[] { MyEntity.class }; + + @Primary + @Bean + LocalContainerEntityManagerFactoryBean firstEntityManagerFactory(DataSource ds) { + return createSessionFactory(ds); + } + + @Bean + LocalContainerEntityManagerFactoryBean secondOne(DataSource ds) { + return createSessionFactory(ds); + } + + private LocalContainerEntityManagerFactoryBean createSessionFactory(DataSource ds) { + Map jpaProperties = new HashMap<>(); + jpaProperties.put("hibernate.generate_statistics", "true"); + return new EntityManagerFactoryBuilder(new HibernateJpaVendorAdapter(), jpaProperties, null).dataSource(ds) + .packages(PACKAGE_CLASSES).build(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class NonHibernateEntityManagerFactoryConfiguration { + + @Bean + EntityManagerFactory entityManagerFactory() { + EntityManagerFactory mockedFactory = mock(EntityManagerFactory.class); + // enforces JPA contract + given(mockedFactory.unwrap(ArgumentMatchers.>any())) + .willThrow(PersistenceException.class); + return mockedFactory; + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsIntegrationTests.java index 697bb0e086..9523c48cb2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/metrics/test/MetricsIntegrationTests.java @@ -40,6 +40,7 @@ import org.springframework.boot.actuate.autoconfigure.metrics.SystemMetricsAutoC import org.springframework.boot.actuate.autoconfigure.metrics.amqp.RabbitMetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.cache.CacheMetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.jdbc.DataSourcePoolMetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.orm.jpa.HibernateMetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.web.client.HttpClientMetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.web.reactive.WebFluxMetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.web.servlet.WebMvcMetricsAutoConfiguration; @@ -137,8 +138,9 @@ class MetricsIntegrationTests { @ImportAutoConfiguration({ MetricsAutoConfiguration.class, JvmMetricsAutoConfiguration.class, LogbackMetricsAutoConfiguration.class, SystemMetricsAutoConfiguration.class, RabbitMetricsAutoConfiguration.class, CacheMetricsAutoConfiguration.class, - DataSourcePoolMetricsAutoConfiguration.class, HttpClientMetricsAutoConfiguration.class, - WebFluxMetricsAutoConfiguration.class, WebMvcMetricsAutoConfiguration.class, JacksonAutoConfiguration.class, + DataSourcePoolMetricsAutoConfiguration.class, HibernateMetricsAutoConfiguration.class, + HttpClientMetricsAutoConfiguration.class, WebFluxMetricsAutoConfiguration.class, + WebMvcMetricsAutoConfiguration.class, JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, RestTemplateAutoConfiguration.class, WebMvcAutoConfiguration.class, DispatcherServletAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class }) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc index 8d7b8b7827..69e8910956 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/actuator/metrics.adoc @@ -906,6 +906,25 @@ Each metric is tagged by the name of the pool (you can control it with `spring.d +[[actuator.metrics.supported.hibernate]] +==== Hibernate Metrics +If `org.hibernate.orm:hibernate-micrometer` is on the classpath, all available Hibernate `EntityManagerFactory` instances that have statistics enabled are instrumented with a metric named `hibernate`. + +Metrics are also tagged by the name of the `EntityManagerFactory`, which is derived from the bean name. + +To enable statistics, the standard JPA property `hibernate.generate_statistics` must be set to `true`. +You can enable that on the auto-configured `EntityManagerFactory`: + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + jpa: + properties: + "[hibernate.generate_statistics]": true +---- + + + [[actuator.metrics.supported.spring-data-repository]] ==== Spring Data Repository Metrics Auto-configuration enables the instrumentation of all Spring Data `Repository` method invocations.