Add metrics exporter for initializr service

If a RedisConnectionFactory is detected the system will now export
metrics to redis every 5s (initializr.metrics.rateMillis). Metric
names are prefixed with <id>.<hex>. where <id> defaults to the
application name (initializr.metrics.id) and <hex> is unique. An
aggregator app can then pick up the values and (for instance)
continue counters across restarts.

The code is meant to be easier to upgrade Spring Boot 1.3

There are 2 beans that we can simply remove when we upgrade to 1.3.
Meanwhile, the app will run with 1.3 (or 1.2) for testing if you
uncomment the @ExportMetricWriter.
This commit is contained in:
Dave Syer 2015-03-24 11:38:00 +00:00 committed by Stephane Nicoll
parent 7fdde4b0e4
commit 6f4d3f4e03
10 changed files with 315 additions and 4 deletions

3
.gitignore vendored
View File

@ -16,6 +16,7 @@ tmp*
initializer-service/spring
grapes
spring.zip
*.jar
repository/
.idea
*.iml
*.iml

View File

@ -3,6 +3,7 @@ package app
import io.spring.initializr.web.LegacyStsController
@Grab('io.spring.initalizr:initializr:1.0.0.BUILD-SNAPSHOT')
@Grab('spring-boot-starter-redis')
class InitializerService {
@Bean

View File

@ -23,6 +23,11 @@
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-groovy-templates</artifactId>
@ -234,4 +239,4 @@
</profile>
</profiles>
</project>
</project>

View File

@ -0,0 +1,92 @@
/*
* Copyright 2014-2015 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 io.spring.initializr.config
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.actuate.metrics.export.Exporter
import org.springframework.boot.actuate.metrics.export.MetricCopyExporter
import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository
import org.springframework.boot.actuate.metrics.repository.MetricRepository
import org.springframework.boot.actuate.metrics.repository.redis.RedisMetricRepository
import org.springframework.boot.actuate.metrics.writer.MetricWriter
import org.springframework.boot.autoconfigure.AutoConfigureAfter
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.autoconfigure.redis.RedisAutoConfiguration
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.util.ObjectUtils
/**
* @author Dave Syer
*
*/
@Configuration
@ConditionalOnBean(RedisConnectionFactory)
@ConditionalOnProperty(value='spring.metrics.export.enabled', matchIfMissing=true)
@EnableScheduling
@EnableConfigurationProperties(MetricsProperties)
@AutoConfigureAfter(value=RedisAutoConfiguration, name="org.springframework.boot.actuate.autoconfigure.MetricExportAutoConfiguration")
class InitializrMetricsExporterAutoConfiguration {
@Autowired
RedisConnectionFactory connectionFactory
@Autowired
MetricsProperties metrics
@Autowired
ApplicationContext context
@Bean
// @ExportMetricWriter // Add this when upgrading to Boot 1.3
MetricWriter writer() {
new RedisMetricRepository(connectionFactory,
metrics.prefix + metrics.getId(context.getId()) + '.'
+ ObjectUtils.getIdentityHexString(context) + '.',
metrics.key)
}
// Remove this when upgrading to Boot 1.3
@Bean
@ConditionalOnMissingClass(name='org.springframework.boot.actuate.autoconfigure.ActuatorMetricWriter')
@Primary
MetricRepository reader() {
new InMemoryMetricRepository()
}
// Remove this when upgrading to Boot 1.3
@Bean
@ConditionalOnMissingClass(name='org.springframework.boot.actuate.autoconfigure.ActuatorMetricWriter')
Exporter exporter(InMemoryMetricRepository reader) {
new MetricCopyExporter(reader, writer()) {
@Override
@Scheduled(fixedRateString = '${spring.metrics.export.default.delayMillis:5000}')
void export() {
super.export()
}
}
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2014-2015 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 io.spring.initializr.config
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Dave Syer
*
*/
@ConfigurationProperties('initializr.metrics')
class MetricsProperties {
/**
* Prefix for redis keys holding metrics in data store.
*/
String prefix = 'spring.metrics.collector.'
/**
* Redis key holding index to metrics keys in data store.
*/
String key = 'keys.spring.metrics.collector'
/**
* Identifier for application in metrics keys. Keys will be exported in the form
* '[id].[hex].[name]' (where '[id]' is this value, '[hex]' is unique per application
* context, and '[name]' is the "natural" name for the metric.
*/
@Value('${spring.application.name:${vcap.application.name:application}}')
String id
/**
* The rate (in milliseconds) at which metrics are exported to Redis. If the value is
* <=0 then the export is disabled.
*/
@Value('${spring.metrics.export.default.delayMillis:5000}')
long rateMillis = 5000L
boolean isEnabled() {
rateMillis > 0
}
String getPrefix() {
if (prefix.endsWith('.')) {
return prefix
}
prefix + '.'
}
String getId(String defaultValue) {
if (id) return id
defaultValue
}
}

View File

@ -30,7 +30,7 @@ class InitializrConfiguration {
}
/**
* Generate a suitable application mame based on the specified name. If no suitable
* Generate a suitable application name based on the specified name. If no suitable
* application name can be generated from the specified {@code name}, the
* {@link Env#fallbackApplicationName} is used instead.
* <p>No suitable application name can be generated if the name is {@code null} or

View File

@ -20,6 +20,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import org.springframework.boot.context.properties.ConfigurationProperties
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
/**
* Configuration of the initializr service.
*

View File

@ -1 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=io.spring.initializr.config.InitializrAutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.spring.initializr.config.InitializrAutoConfiguration,\
io.spring.initializr.config.InitializrMetricsExporterAutoConfiguration

View File

@ -0,0 +1,92 @@
/*
* Copyright 2014-2015 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 io.spring.initializr.metrics;
import static org.junit.Assert.*
import io.spring.initializr.generator.ProjectGenerationMetricsListener
import io.spring.initializr.generator.ProjectRequest
import io.spring.initializr.metadata.DefaultMetadataElement
import io.spring.initializr.metadata.InitializrMetadata
import io.spring.initializr.metadata.InitializrMetadataProvider
import io.spring.initializr.support.DefaultInitializrMetadataProvider
import io.spring.initializr.test.RedisRunning
import org.junit.Before;
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.boot.actuate.metrics.repository.redis.RedisMetricRepository
import org.springframework.boot.actuate.metrics.writer.MetricWriter
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.boot.test.IntegrationTest
import org.springframework.boot.test.SpringApplicationConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner
/**
* @author Dave Syer
*
*/
@RunWith(SpringJUnit4ClassRunner)
@SpringApplicationConfiguration(classes = Config)
@IntegrationTest(['spring.metrics.export.default.delayMillis:500','initializr.metrics.prefix:test.prefix','initializr.metrics.key:key.test'])
public class MetricsExportTests {
@Rule
public RedisRunning running = new RedisRunning()
@Autowired
ProjectGenerationMetricsListener listener
@Autowired
@Qualifier("writer")
MetricWriter writer
RedisMetricRepository repository
@Before
void init() {
repository = (RedisMetricRepository) writer
repository.findAll().each {
repository.reset(it.name)
}
assertTrue("Metrics not empty", repository.findAll().size()==0)
}
@Test
void exportAndCheckMetricsExist() {
listener.onGeneratedProject(new ProjectRequest())
Thread.sleep(1000L)
assertTrue("No metrics exported", repository.findAll().size()>0)
}
@EnableAutoConfiguration
static class Config {
@Bean
InitializrMetadataProvider initializrMetadataProvider(InitializrMetadata metadata) {
new DefaultInitializrMetadataProvider(metadata) {
@Override
protected List<DefaultMetadataElement> fetchBootVersions() {
null // Disable metadata fetching from spring.io
}
}
}
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2014-2015 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 io.spring.initializr.test
import org.junit.Assume
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.junit.runners.model.Statement
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory
/**
* @author Dave Syer
*
*/
class RedisRunning extends TestWatcher {
JedisConnectionFactory connectionFactory;
@Override
Statement apply(Statement base, Description description) {
if (connectionFactory==null) {
connectionFactory = new JedisConnectionFactory()
connectionFactory.afterPropertiesSet()
}
try {
connectionFactory.connection
} catch (Exception e) {
Assume.assumeNoException('Cannot connect to Redis (so skipping tests)', e)
}
super.apply(base, description)
}
}