DataSource metrics

This commit adds an abstraction that provides a standard manner to
retrieve various metadata that are shared by most data sources.

DataSourceMetadata is implemented by the three data source
implementations that boot supports out-of-the-box: Tomcat, Hikari and
Commons dbcp.

This abstraction is used to provide two additional metrics per data
source defined in the application: the number of allocated
connection(s) (.active) and the current usage of the connection pool
(.usage).

All such metrics share the 'datasource.' prefix. The prefix is further
qualified for each data source:

* If the data source is the primary data source (that is either the
  only available data source or the one flagged @Primary amongst the
  existing ones), the prefix is "datasource.primary"
* If the data source bean name ends with "dataSource", the prefix is
  the name of the bean without it (i.e. batchDataSource becomes batch)
* In all other cases, the name of the bean is used

It is possible to override part or all of those defaults by
registering a bean with a customized version of
DataSourcePublicMetrics.

Additional DataSourceMetadata implementations for other data source
types can be added very easily, check
DataourceMetadataProvidersConfiguration for more details.

Fixes gh-1013
This commit is contained in:
Stephane Nicoll
2014-06-12 18:41:13 +02:00
committed by Stephane Nicoll
parent 85c95744f9
commit 3dc932db88
19 changed files with 1325 additions and 1 deletions

View File

@@ -0,0 +1,196 @@
/*
* Copyright 2012-2014 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
*
* http://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;
import static org.junit.Assert.*;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.junit.After;
import org.junit.Test;
import org.springframework.boot.actuate.endpoint.DataSourcePublicMetrics;
import org.springframework.boot.actuate.endpoint.PublicMetrics;
import org.springframework.boot.actuate.metrics.Metric;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
/**
*
* @author Stephane Nicoll
*/
public class MetricDataSourceAutoConfigurationTests {
private AnnotationConfigApplicationContext context;
@After
public void after() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void noDataSource() {
load();
assertEquals(0, this.context.getBeansOfType(PublicMetrics.class).size());
}
@Test
public void autoDataSource() {
load(DataSourceAutoConfiguration.class);
PublicMetrics bean = this.context.getBean(PublicMetrics.class);
Collection<Metric<?>> metrics = bean.metrics();
assertMetrics(metrics, "datasource.primary.active", "datasource.primary.usage");
}
@Test
public void multipleDataSources() {
load(MultipleDataSourcesConfig.class);
PublicMetrics bean = this.context.getBean(PublicMetrics.class);
Collection<Metric<?>> metrics = bean.metrics();
assertMetrics(metrics,
"datasource.tomcat.active", "datasource.tomcat.usage",
"datasource.commonsDbcp.active", "datasource.commonsDbcp.usage");
// Hikari won't work unless a first connection has been retrieved
JdbcTemplate jdbcTemplate = new JdbcTemplate(context.getBean("hikariDS", DataSource.class));
jdbcTemplate.execute(new ConnectionCallback<Void>() {
@Override
public Void doInConnection(Connection connection) throws SQLException, DataAccessException {
return null;
}
});
Collection<Metric<?>> anotherMetrics = bean.metrics();
assertMetrics(anotherMetrics,
"datasource.tomcat.active", "datasource.tomcat.usage",
"datasource.hikariDS.active", "datasource.hikariDS.usage",
"datasource.commonsDbcp.active", "datasource.commonsDbcp.usage");
}
@Test
public void multipleDataSourcesWithPrimary() {
load(MultipleDataSourcesWithPrimaryConfig.class);
PublicMetrics bean = this.context.getBean(PublicMetrics.class);
Collection<Metric<?>> metrics = bean.metrics();
assertMetrics(metrics,
"datasource.primary.active", "datasource.primary.usage",
"datasource.commonsDbcp.active", "datasource.commonsDbcp.usage");
}
@Test
public void customPrefix() {
load(MultipleDataSourcesWithPrimaryConfig.class, CustomDataSourcePublicMetrics.class);
PublicMetrics bean = this.context.getBean(PublicMetrics.class);
Collection<Metric<?>> metrics = bean.metrics();
assertMetrics(metrics,
"ds.first.active", "ds.first.usage",
"ds.second.active", "ds.second.usage");
}
private void assertMetrics(Collection<Metric<?>> metrics, String... keys) {
Map<String, Number> content = new HashMap<String, Number>();
for (Metric<?> metric : metrics) {
content.put(metric.getName(), metric.getValue());
}
for (String key : keys) {
assertTrue("Key '" + key + "' was not found", content.containsKey(key));
}
}
private void load(Class<?>... config) {
this.context = new AnnotationConfigApplicationContext();
if (config.length > 0) {
this.context.register(config);
}
this.context.register(MetricDataSourceAutoConfiguration.class);
this.context.refresh();
}
@Configuration
static class MultipleDataSourcesConfig {
@Bean
public DataSource tomcatDataSource() {
return initializeBuilder().type(org.apache.tomcat.jdbc.pool.DataSource.class).build();
}
@Bean
public DataSource hikariDS() {
return initializeBuilder().type(HikariDataSource.class).build();
}
@Bean
public DataSource commonsDbcpDataSource() {
return initializeBuilder().type(BasicDataSource.class).build();
}
}
@Configuration
static class MultipleDataSourcesWithPrimaryConfig {
@Bean
@Primary
public DataSource myDataSource() {
return initializeBuilder().type(org.apache.tomcat.jdbc.pool.DataSource.class).build();
}
@Bean
public DataSource commonsDbcpDataSource() {
return initializeBuilder().type(BasicDataSource.class).build();
}
}
@Configuration
static class CustomDataSourcePublicMetrics {
@Bean
public DataSourcePublicMetrics myDataSourcePublicMetrics() {
return new DataSourcePublicMetrics() {
@Override
protected String createPrefix(String dataSourceName, DataSource dataSource, boolean primary) {
return (primary ? "ds.first." : "ds.second");
}
};
}
}
private static DataSourceBuilder initializeBuilder() {
return DataSourceBuilder.create()
.driverClassName("org.hsqldb.jdbc.JDBCDriver")
.url("jdbc:hsqldb:mem:test")
.username("sa");
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2012-2014 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
*
* http://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.jdbc;
import static org.junit.Assert.*;
import java.sql.Connection;
import java.sql.SQLException;
import org.junit.Test;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
/**
*
* @author Stephane Nicoll
*/
public abstract class AbstractDataSourceMetadataTests<D extends AbstractDataSourceMetadata> {
/**
* Return a data source metadata instance with a min size of 0 and max size of 2.
*/
protected abstract D getDataSourceMetadata();
@Test
public void getMaxPoolSize() {
assertEquals(Integer.valueOf(2), getDataSourceMetadata().getMaxPoolSize());
}
@Test
public void getMinPoolSize() {
assertEquals(Integer.valueOf(0), getDataSourceMetadata().getMinPoolSize());
}
@Test
public void getPoolSizeNoConnection() {
// Make sure the pool is initialized
JdbcTemplate jdbcTemplate = new JdbcTemplate(getDataSourceMetadata().getDataSource());
jdbcTemplate.execute(new ConnectionCallback<Void>() {
@Override
public Void doInConnection(Connection connection) throws SQLException, DataAccessException {
return null;
}
});
assertEquals(Integer.valueOf(0), getDataSourceMetadata().getPoolSize());
assertEquals(Float.valueOf(0), getDataSourceMetadata().getPoolUsage());
}
@Test
public void getPoolSizeOneConnection() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(getDataSourceMetadata().getDataSource());
jdbcTemplate.execute(new ConnectionCallback<Void>() {
@Override
public Void doInConnection(Connection connection) throws SQLException, DataAccessException {
assertEquals(Integer.valueOf(1), getDataSourceMetadata().getPoolSize());
assertEquals(Float.valueOf(0.5F), getDataSourceMetadata().getPoolUsage());
return null;
}
});
}
@Test
public void getPoolSizeTwoConnections() {
final JdbcTemplate jdbcTemplate = new JdbcTemplate(getDataSourceMetadata().getDataSource());
jdbcTemplate.execute(new ConnectionCallback<Void>() {
@Override
public Void doInConnection(Connection connection) throws SQLException, DataAccessException {
jdbcTemplate.execute(new ConnectionCallback<Void>() {
@Override
public Void doInConnection(Connection connection) throws SQLException, DataAccessException {
assertEquals(Integer.valueOf(2), getDataSourceMetadata().getPoolSize());
assertEquals(Float.valueOf(1F), getDataSourceMetadata().getPoolUsage());
return null;
}
});
return null;
}
});
}
protected DataSourceBuilder initializeBuilder() {
return DataSourceBuilder.create()
.driverClassName("org.hsqldb.jdbc.JDBCDriver")
.url("jdbc:hsqldb:mem:test")
.username("sa");
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2012-2014 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
*
* http://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.jdbc;
import static org.junit.Assert.*;
import org.apache.commons.dbcp.BasicDataSource;
import org.junit.Before;
import org.junit.Test;
/**
*
* @author Stephane Nicoll
*/
public class CommonsDbcpDataSourceMetadataTests extends AbstractDataSourceMetadataTests<CommonsDbcpDataSourceMetadata> {
private CommonsDbcpDataSourceMetadata dataSourceMetadata;
@Before
public void setup() {
this.dataSourceMetadata = createDataSourceMetadata(0, 2);
}
@Override
protected CommonsDbcpDataSourceMetadata getDataSourceMetadata() {
return this.dataSourceMetadata;
}
@Test
public void getPoolUsageWithNoCurrent() {
CommonsDbcpDataSourceMetadata dsm = new CommonsDbcpDataSourceMetadata(createDataSource()) {
@Override
public Integer getPoolSize() {
return null;
}
};
assertNull(dsm.getPoolUsage());
}
@Test
public void getPoolUsageWithNoMax() {
CommonsDbcpDataSourceMetadata dsm = new CommonsDbcpDataSourceMetadata(createDataSource()) {
@Override
public Integer getMaxPoolSize() {
return null;
}
};
assertNull(dsm.getPoolUsage());
}
@Test
public void getPoolUsageWithUnlimitedPool() {
DataSourceMetadata unlimitedDataSource = createDataSourceMetadata(0, -1);
assertEquals(Float.valueOf(-1F), unlimitedDataSource.getPoolUsage());
}
private CommonsDbcpDataSourceMetadata createDataSourceMetadata(int minSize, int maxSize) {
BasicDataSource dataSource = createDataSource();
dataSource.setMinIdle(minSize);
dataSource.setMaxActive(maxSize);
return new CommonsDbcpDataSourceMetadata(dataSource);
}
private BasicDataSource createDataSource() {
return (BasicDataSource) initializeBuilder().type(BasicDataSource.class).build();
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2012-2014 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
*
* http://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.jdbc;
import static org.junit.Assert.*;
import static org.mockito.BDDMockito.*;
import java.util.Arrays;
import javax.sql.DataSource;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
*
* @author Stephane Nicoll
*/
public class CompositeDataSourceMetadataProviderTests {
@Mock
private DataSourceMetadataProvider firstProvider;
@Mock
private DataSourceMetadata first;
@Mock
private DataSource firstDataSource;
@Mock
private DataSourceMetadataProvider secondProvider;
@Mock
private DataSourceMetadata second;
@Mock
private DataSource secondDataSource;
@Mock
private DataSource unknownDataSource;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
given(firstProvider.getDataSourceMetadata(firstDataSource)).willReturn(first);
given(firstProvider.getDataSourceMetadata(secondDataSource)).willReturn(second);
}
@Test
public void createWithProviders() {
CompositeDataSourceMetadataProvider provider =
new CompositeDataSourceMetadataProvider(Arrays.asList(firstProvider, secondProvider));
assertSame(first, provider.getDataSourceMetadata(firstDataSource));
assertSame(second, provider.getDataSourceMetadata(secondDataSource));
assertNull(provider.getDataSourceMetadata(unknownDataSource));
}
@Test
public void addProvider() {
CompositeDataSourceMetadataProvider provider =
new CompositeDataSourceMetadataProvider();
assertNull(provider.getDataSourceMetadata(firstDataSource));
provider.addDataSourceMetadataProvider(firstProvider);
assertSame(first, provider.getDataSourceMetadata(firstDataSource));
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2012-2014 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
*
* http://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.jdbc;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.Before;
/**
*
* @author Stephane Nicoll
*/
public class HikariDataSourceMetadataTests extends AbstractDataSourceMetadataTests<HikariDataSourceMetadata> {
private HikariDataSourceMetadata dataSourceMetadata;
@Before
public void setup() {
this.dataSourceMetadata = createDataSourceMetadata(0, 2);
}
@Override
protected HikariDataSourceMetadata getDataSourceMetadata() {
return this.dataSourceMetadata;
}
private HikariDataSourceMetadata createDataSourceMetadata(int minSize, int maxSize) {
HikariDataSource dataSource = (HikariDataSource) initializeBuilder().type(HikariDataSource.class).build();
dataSource.setMinimumIdle(minSize);
dataSource.setMaximumPoolSize(maxSize);
return new HikariDataSourceMetadata(dataSource);
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012-2014 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
*
* http://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.jdbc;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.junit.Before;
/**
*
* @author Stephane Nicoll
*/
public class TomcatDataSourceMetadataTests extends AbstractDataSourceMetadataTests<TomcatDataSourceMetadata> {
private TomcatDataSourceMetadata dataSourceMetadata;
@Before
public void setup() {
this.dataSourceMetadata = createDataSourceMetadata(0, 2);
}
@Override
protected TomcatDataSourceMetadata getDataSourceMetadata() {
return this.dataSourceMetadata;
}
private TomcatDataSourceMetadata createDataSourceMetadata(int minSize, int maxSize) {
DataSource dataSource = (DataSource) initializeBuilder().type(DataSource.class).build();
dataSource.setMinIdle(minSize);
dataSource.setMaxActive(maxSize);
// Avoid warnings
dataSource.setInitialSize(minSize);
dataSource.setMaxIdle(maxSize);
return new TomcatDataSourceMetadata(dataSource);
}
}