diff --git a/.gitignore b/.gitignore index fb3644ab..084817f7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ tmp* initializer-service/spring grapes spring.zip +*.jar repository/ .idea -*.iml \ No newline at end of file +*.iml diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index c8897346..a3912a56 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -7,6 +7,7 @@ order. === Release 1.0.0 (In progress) +* https://github.com/spring-io/initializr/issues/96[#96]: export service metrics to redis * https://github.com/spring-io/initializr/issues/115[#115]: rename /metadata/service to /metadata/config * https://github.com/spring-io/initializr/issues/89[#89]: better describe service capability * https://github.com/spring-io/initializr/issues/105[#105]: support for dependencies group defaults diff --git a/initializr-service/app.groovy b/initializr-service/app.groovy index fe88e7e2..8de70715 100644 --- a/initializr-service/app.groovy +++ b/initializr-service/app.groovy @@ -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 diff --git a/initializr-service/application-cloud.yml b/initializr-service/application-cloud.yml new file mode 100644 index 00000000..089ddd66 --- /dev/null +++ b/initializr-service/application-cloud.yml @@ -0,0 +1,3 @@ +spring: + metrics: + export: enabled \ No newline at end of file diff --git a/initializr-service/manifest.yml b/initializr-service/manifest.yml index bdf58360..631a3a63 100644 --- a/initializr-service/manifest.yml +++ b/initializr-service/manifest.yml @@ -6,3 +6,5 @@ applications: host: start-development domain: cfapps.io path: . + services: + start-redis diff --git a/initializr/pom.xml b/initializr/pom.xml index a98df6d7..e80cab89 100644 --- a/initializr/pom.xml +++ b/initializr/pom.xml @@ -27,6 +27,11 @@ org.springframework.boot spring-boot-starter-groovy-templates + + org.springframework.boot + spring-boot-starter-redis + true + com.fasterxml.jackson.core jackson-core @@ -234,4 +239,4 @@ - \ No newline at end of file + diff --git a/initializr/src/main/groovy/io/spring/initializr/config/InitializrMetricsExporterAutoConfiguration.groovy b/initializr/src/main/groovy/io/spring/initializr/config/InitializrMetricsExporterAutoConfiguration.groovy new file mode 100644 index 00000000..1101d0f2 --- /dev/null +++ b/initializr/src/main/groovy/io/spring/initializr/config/InitializrMetricsExporterAutoConfiguration.groovy @@ -0,0 +1,96 @@ +/* + * 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 + +/** + * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration + * Auto-configuration} to export the metrics of an initializr instnace. + * + * @author Dave Syer + * @since 1.0 + */ +@Configuration +@ConditionalOnBean(RedisConnectionFactory) +@ConditionalOnProperty(value = 'spring.metrics.export.enabled') +@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() + } + } + } + +} diff --git a/initializr/src/main/groovy/io/spring/initializr/config/MetricsProperties.groovy b/initializr/src/main/groovy/io/spring/initializr/config/MetricsProperties.groovy new file mode 100644 index 00000000..54828a15 --- /dev/null +++ b/initializr/src/main/groovy/io/spring/initializr/config/MetricsProperties.groovy @@ -0,0 +1,67 @@ +/* + * 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 + +/** + * Metrics-related configuration. + * + * @author Dave Syer + * @since 1.0 + */ +@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 + + String getPrefix() { + if (prefix.endsWith('.')) { + return prefix + } + prefix + '.' + } + + String getId(String defaultValue) { + if (id) return id + defaultValue + } +} diff --git a/initializr/src/main/groovy/io/spring/initializr/metadata/InitializrConfiguration.groovy b/initializr/src/main/groovy/io/spring/initializr/metadata/InitializrConfiguration.groovy index 94300fa0..3d0acbdb 100644 --- a/initializr/src/main/groovy/io/spring/initializr/metadata/InitializrConfiguration.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/metadata/InitializrConfiguration.groovy @@ -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. *

No suitable application name can be generated if the name is {@code null} or diff --git a/initializr/src/main/groovy/io/spring/initializr/metadata/InitializrProperties.groovy b/initializr/src/main/groovy/io/spring/initializr/metadata/InitializrProperties.groovy index 47b09946..f9b9df30 100644 --- a/initializr/src/main/groovy/io/spring/initializr/metadata/InitializrProperties.groovy +++ b/initializr/src/main/groovy/io/spring/initializr/metadata/InitializrProperties.groovy @@ -26,7 +26,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties * @author Stephane Nicoll * @since 1.0 */ -@ConfigurationProperties(prefix = 'initializr', ignoreUnknownFields = false) +@ConfigurationProperties(prefix = 'initializr') class InitializrProperties extends InitializrConfiguration { @JsonIgnore diff --git a/initializr/src/main/resources/META-INF/spring.factories b/initializr/src/main/resources/META-INF/spring.factories index a6629e17..6c786bad 100644 --- a/initializr/src/main/resources/META-INF/spring.factories +++ b/initializr/src/main/resources/META-INF/spring.factories @@ -1 +1,3 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=io.spring.initializr.config.InitializrAutoConfiguration \ No newline at end of file +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +io.spring.initializr.config.InitializrAutoConfiguration,\ +io.spring.initializr.config.InitializrMetricsExporterAutoConfiguration \ No newline at end of file diff --git a/initializr/src/test/groovy/io/spring/initializr/metrics/MetricsExportTests.groovy b/initializr/src/test/groovy/io/spring/initializr/metrics/MetricsExportTests.groovy new file mode 100644 index 00000000..4e72087c --- /dev/null +++ b/initializr/src/test/groovy/io/spring/initializr/metrics/MetricsExportTests.groovy @@ -0,0 +1,88 @@ +/* + * 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 io.spring.initializr.generator.ProjectGenerationMetricsListener +import io.spring.initializr.generator.ProjectRequest +import io.spring.initializr.metadata.InitializrMetadata +import io.spring.initializr.metadata.InitializrMetadataProvider +import io.spring.initializr.test.OfflineInitializrMetadataProvider +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 + +import static org.junit.Assert.assertTrue + +/** + * @author Dave Syer + */ +@RunWith(SpringJUnit4ClassRunner) +@SpringApplicationConfiguration(classes = Config) +@IntegrationTest(['spring.metrics.export.default.delayMillis:500', + 'spring.metrics.export.enabled:true', + '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 OfflineInitializrMetadataProvider(metadata) + } + } +} diff --git a/initializr/src/test/groovy/io/spring/initializr/test/OfflineInitializrMetadataProvider.groovy b/initializr/src/test/groovy/io/spring/initializr/test/OfflineInitializrMetadataProvider.groovy new file mode 100644 index 00000000..4497ecec --- /dev/null +++ b/initializr/src/test/groovy/io/spring/initializr/test/OfflineInitializrMetadataProvider.groovy @@ -0,0 +1,40 @@ +/* + * Copyright 2012-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 io.spring.initializr.metadata.DefaultMetadataElement +import io.spring.initializr.metadata.InitializrMetadata +import io.spring.initializr.support.DefaultInitializrMetadataProvider + +/** + * A {@link DefaultInitializrMetadataProvider} that does not attempt to + * use the network to refresh its configuration. + * + * @author Stephane Nicoll + * @since 1.0 + */ +class OfflineInitializrMetadataProvider extends DefaultInitializrMetadataProvider { + + OfflineInitializrMetadataProvider(InitializrMetadata metadata) { + super(metadata) + } + + @Override + protected List fetchBootVersions() { + null // Disable metadata fetching from spring.io + } +} diff --git a/initializr/src/test/groovy/io/spring/initializr/test/RedisRunning.groovy b/initializr/src/test/groovy/io/spring/initializr/test/RedisRunning.groovy new file mode 100644 index 00000000..978f7020 --- /dev/null +++ b/initializr/src/test/groovy/io/spring/initializr/test/RedisRunning.groovy @@ -0,0 +1,50 @@ +/* + * 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 + +/** + * A {@link org.junit.rules.TestRule} that validates Redis is available. + * + * @author Dave Syer + * @since 1.0 + */ +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) + } + +} diff --git a/initializr/src/test/groovy/io/spring/initializr/web/AbstractInitializrControllerIntegrationTests.groovy b/initializr/src/test/groovy/io/spring/initializr/web/AbstractInitializrControllerIntegrationTests.groovy index d2bcf146..38f09b49 100644 --- a/initializr/src/test/groovy/io/spring/initializr/web/AbstractInitializrControllerIntegrationTests.groovy +++ b/initializr/src/test/groovy/io/spring/initializr/web/AbstractInitializrControllerIntegrationTests.groovy @@ -19,10 +19,9 @@ package io.spring.initializr.web import java.nio.charset.Charset import io.spring.initializr.mapper.InitializrMetadataVersion -import io.spring.initializr.metadata.DefaultMetadataElement import io.spring.initializr.metadata.InitializrMetadata -import io.spring.initializr.support.DefaultInitializrMetadataProvider import io.spring.initializr.metadata.InitializrMetadataProvider +import io.spring.initializr.test.OfflineInitializrMetadataProvider import io.spring.initializr.test.ProjectAssert import org.json.JSONObject import org.junit.Rule @@ -85,7 +84,7 @@ abstract class AbstractInitializrControllerIntegrationTests { */ protected void validateContentType(ResponseEntity response, MediaType expected) { def actual = response.headers.getContentType() - assertTrue "Non compatible media-type, expected $expected, got $actual" , + assertTrue "Non compatible media-type, expected $expected, got $actual", actual.isCompatibleWith(expected) assertEquals 'All text content should be UTF-8 encoded', 'UTF-8', actual.getParameter('charset') @@ -93,7 +92,7 @@ abstract class AbstractInitializrControllerIntegrationTests { protected void validateMetadata(ResponseEntity response, MediaType mediaType, - String version, JSONCompareMode compareMode) { + String version, JSONCompareMode compareMode) { validateContentType(response, mediaType) def json = new JSONObject(response.body) def expected = readMetadataJson(version) @@ -147,7 +146,7 @@ abstract class AbstractInitializrControllerIntegrationTests { } protected ResponseEntity execute(String contextPath, Class responseType, - String userAgentHeader, String... acceptHeaders) { + String userAgentHeader, String... acceptHeaders) { HttpHeaders headers = new HttpHeaders(); if (userAgentHeader) { headers.set("User-Agent", userAgentHeader); @@ -217,12 +216,7 @@ abstract class AbstractInitializrControllerIntegrationTests { @Bean InitializrMetadataProvider initializrMetadataProvider(InitializrMetadata metadata) { - new DefaultInitializrMetadataProvider(metadata) { - @Override - protected List fetchBootVersions() { - null // Disable metadata fetching from spring.io - } - } + new OfflineInitializrMetadataProvider(metadata) } }