Add initializr specific metrics

This commit adds several project related metrics that are recorded
using the standard CounterService.

The following metrics are managed:

* `counter.initializr.requests` sums the total number of requests, be
it for a project or a simple build file
* `counter.initializr.dependency.xyz` represents the number of times
the xyz dependency was requested. If the dependency is requested
through an alias, it is consolidated using the standard id
* `counter.initializr.type.xyz` represents the statistics per project
type (i.e. starter.zip, pom.xml, etc)
* `counter.initializr.java_version.xyz` represents the statistics per
java version
* `counter.initializr.packaging.xyz` represents the statistics per
packaging (war, jar)
* `counter.initializr.language.xyz` represents the statistics per
language (java, groovy)
* `counter.initializr.boot_version.xyz` represents the statistics per
Spring Boot version

The statistics are recorded by default using a
ProjectGenerationListener implementation. This can be further
customized by overriding the default ProjectGenerator bean provided
through auto configuration.

Fixes gh-32
This commit is contained in:
Stephane Nicoll
2014-08-27 13:20:57 +02:00
parent a6ef4714e8
commit 575ca6cf03
9 changed files with 524 additions and 4 deletions

View File

@@ -6,6 +6,8 @@ import java.util.concurrent.TimeUnit
import com.google.common.cache.CacheBuilder
import io.spring.initializr.web.MainController
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.actuate.metrics.CounterService
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.cache.CacheManager
@@ -32,6 +34,9 @@ import org.springframework.context.annotation.Configuration
@EnableConfigurationProperties(InitializrMetadata)
class InitializrAutoConfiguration {
@Autowired
private CounterService counterService
@Bean
@ConditionalOnMissingBean(MainController)
MainController initializrMainController() {
@@ -41,7 +46,9 @@ class InitializrAutoConfiguration {
@Bean
@ConditionalOnMissingBean(ProjectGenerator)
ProjectGenerator projectGenerator() {
new ProjectGenerator()
ProjectGenerator generator = new ProjectGenerator()
generator.listeners << metricsListener()
generator
}
@Bean
@@ -50,6 +57,11 @@ class InitializrAutoConfiguration {
return new DefaultInitializrMetadataProvider(metadata)
}
@Bean
ProjectGenerationMetricsListener metricsListener() {
new ProjectGenerationMetricsListener(counterService)
}
@Bean
@ConditionalOnMissingBean(CacheManager)
CacheManager cacheManager() {

View File

@@ -0,0 +1,33 @@
/*
* 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 io.spring.initializr
/**
* Interface to be implemented by components that need to be aware of project generation
* related events.
*
* @author Stephane Nicoll
* @since 1.0
*/
public interface ProjectGenerationListener {
/**
* Invoked when a project has been generated for the specified {@link ProjectRequest}.
*/
void onGeneratedProject(ProjectRequest request)
}

View File

@@ -0,0 +1,98 @@
/*
* 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 io.spring.initializr
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.actuate.metrics.CounterService
import org.springframework.util.StringUtils
/**
* A {@link ProjectGenerationListener} implementation that uses a {@link CounterService}
* to update various project related metrics.
*
* @author Stephane Nicoll
* @since 1.0
*/
class ProjectGenerationMetricsListener implements ProjectGenerationListener {
private final CounterService counterService;
@Autowired
ProjectGenerationMetricsListener(CounterService counterService) {
this.counterService = counterService
}
@Override
void onGeneratedProject(ProjectRequest request) {
increment(key('requests')) // Total number of requests
handleDependencies(request)
handleType(request)
handleJavaVersion(request)
handlePackaging(request)
handleLanguage(request)
handleBootVersion(request)
}
protected void handleDependencies(ProjectRequest request) {
request.dependencies.each {
increment(key('dependency.' + sanitize(it.id)))
}
}
protected void handleType(ProjectRequest request) {
if (StringUtils.hasText(request.type)) {
increment(key('type.' + sanitize(request.type)))
}
}
protected void handleJavaVersion(ProjectRequest request) {
if (StringUtils.hasText(request.javaVersion)) {
increment(key('java_version.' + sanitize(request.javaVersion)))
}
}
protected void handlePackaging(ProjectRequest request) {
if (StringUtils.hasText(request.packaging)) {
increment(key('packaging.' + sanitize(request.packaging)))
}
}
protected void handleLanguage(ProjectRequest request) {
if (StringUtils.hasText(request.language)) {
increment(key('language.' + sanitize(request.language)))
}
}
protected void handleBootVersion(ProjectRequest request) {
if (StringUtils.hasText(request.bootVersion)) {
increment(key('boot_version.' + sanitize(request.bootVersion)))
}
}
protected void increment(String key) {
counterService.increment(key)
}
protected String key(String part) {
'initializr.' + part
}
protected String sanitize(String s) {
s.replace('.', '_')
}
}

View File

@@ -37,6 +37,8 @@ class ProjectGenerator {
@Value('${TMPDIR:.}')
String tmpdir
final Set<ProjectGenerationListener> listeners = new LinkedHashSet<>()
private transient Map<String, List<File>> temporaryFiles = new HashMap<>()
/**
@@ -44,7 +46,9 @@ class ProjectGenerator {
*/
byte[] generateMavenPom(ProjectRequest request) {
Map model = initializeModel(request)
doGenerateMavenPom(model)
byte[] content = doGenerateMavenPom(model)
invokeListeners(request)
content
}
/**
@@ -52,7 +56,9 @@ class ProjectGenerator {
*/
byte[] generateGradleBuild(ProjectRequest request) {
Map model = initializeModel(request)
doGenerateGradleBuild(model)
byte[] content = doGenerateGradleBuild(model)
invokeListeners(request)
content
}
/**
@@ -104,7 +110,7 @@ class ProjectGenerator {
new File(dir, 'src/main/resources/templates').mkdirs()
new File(dir, 'src/main/resources/static').mkdirs()
}
invokeListeners(request)
dir
}
@@ -137,6 +143,12 @@ class ProjectGenerator {
}
}
private void invokeListeners(ProjectRequest request) {
listeners.each {
it.onGeneratedProject(request)
}
}
private Map initializeModel(ProjectRequest request) {
Assert.notNull request.bootVersion, 'boot version must not be null'
def model = [:]

View File

@@ -0,0 +1,217 @@
/*
* 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 io.spring.initializr
import io.spring.initializr.support.InitializrMetadataBuilder
import io.spring.initializr.support.MetricsAssert
import io.spring.initializr.support.TestCounterService
import org.junit.Before
import org.junit.Test
/**
* @author Stephane Nicoll
*/
class ProjectGenerationMetricsListenerTests {
private InitializrMetadata metadata = InitializrMetadataBuilder.withDefaults()
.addDependencyGroup('core', 'web', 'security', 'spring-data').validateAndGet()
private ProjectGenerationMetricsListener listener
private MetricsAssert metricsAssert
@Before
public void setup() {
TestCounterService counterService = new TestCounterService()
listener = new ProjectGenerationMetricsListener(counterService)
metricsAssert = new MetricsAssert(counterService)
}
@Test
public void projectGenerationCount() {
ProjectRequest request = initialize()
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.requests')
}
@Test
public void dependencies() {
ProjectRequest request = initialize()
request.style << 'security' << 'spring-data'
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.dependency.security',
'initializr.dependency.spring-data')
}
@Test
public void resolvedWebDependency() {
ProjectRequest request = initialize()
request.style << 'spring-data'
request.packaging = 'war'
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.dependency.web',
'initializr.dependency.spring-data')
}
@Test
public void aliasedDependencyUseStandardId() {
InitializrMetadata.Dependency dependency = new InitializrMetadata.Dependency()
dependency.id ='foo'
dependency.aliases << 'foo-old'
InitializrMetadata metadata = InitializrMetadataBuilder.withDefaults()
.addDependencyGroup('core', dependency).validateAndGet()
ProjectRequest request = new ProjectRequest()
metadata.initializeProjectRequest(request)
request.style << 'foo-old'
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.dependency.foo') // standard id is used
}
@Test
public void defaultType() {
ProjectRequest request = initialize()
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.type.starter_zip')
}
@Test
public void explicitType() {
ProjectRequest request = initialize()
request.type = 'build.gradle'
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.type.build_gradle')
}
@Test
public void defaultPackaging() {
ProjectRequest request = initialize()
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.packaging.jar')
}
@Test
public void explicitPackaging() {
ProjectRequest request = initialize()
request.packaging = 'war'
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.packaging.war')
}
@Test
public void defaultJavaVersion() {
ProjectRequest request = initialize()
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.java_version.1_7')
}
@Test
public void explicitJavaVersion() {
ProjectRequest request = initialize()
request.javaVersion = '1.8'
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.java_version.1_8')
}
@Test
public void defaultLanguage() {
ProjectRequest request = initialize()
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.language.java')
}
@Test
public void explicitLanguage() {
ProjectRequest request = initialize()
request.language = 'groovy'
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.language.groovy')
}
@Test
public void defaultBootVersion() {
ProjectRequest request = initialize()
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.boot_version.1_1_5_RELEASE')
}
@Test
public void explicitBootVersion() {
ProjectRequest request = initialize()
request.bootVersion = '1.0.2.RELEASE'
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.boot_version.1_0_2_RELEASE')
}
@Test
public void collectAllMetrics() {
ProjectRequest request = initialize()
request.style << 'web' << 'security'
request.type = 'gradle.zip'
request.packaging = 'jar'
request.javaVersion = '1.6'
request.language = 'groovy'
request.bootVersion = '1.0.2.RELEASE'
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.requests',
'initializr.dependency.web', 'initializr.dependency.security',
'initializr.type.gradle_zip', 'initializr.packaging.jar',
'initializr.java_version.1_6', 'initializr.language.groovy',
'initializr.boot_version.1_0_2_RELEASE').metricsCount(8)
}
@Test
public void incrementMetrics() {
ProjectRequest request = initialize()
request.style << 'security' << 'spring-data'
request.resolve(metadata)
listener.onGeneratedProject(request)
metricsAssert.hasValue(1, 'initializr.requests',
'initializr.dependency.security', 'initializr.dependency.spring-data')
ProjectRequest anotherRequest = initialize()
anotherRequest.style << 'web' << 'spring-data'
anotherRequest.resolve(metadata)
listener.onGeneratedProject(anotherRequest)
metricsAssert.hasValue(2, 'initializr.dependency.spring-data',
'initializr.dependency.spring-data')
metricsAssert.hasValue(1, 'initializr.dependency.web',
'initializr.dependency.security')
}
private ProjectRequest initialize() {
ProjectRequest request = new ProjectRequest()
metadata.initializeProjectRequest(request)
request
}
}

View File

@@ -25,6 +25,8 @@ import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import static org.mockito.Mockito.*
/**
* @author Stephane Nicoll
*/
@@ -45,22 +47,34 @@ class ProjectGeneratorTests {
@Test
void defaultMavenPom() {
ProjectGenerationListener listener = mock(ProjectGenerationListener)
projectGenerator.listeners << listener
ProjectRequest request = createProjectRequest('web')
generateMavenPom(request).hasStartClass('demo.Application')
.hasNoRepository().hasSpringBootStarterDependency('web')
verify(listener, times(1)).onGeneratedProject(request)
}
@Test
void defaultGradleBuild() {
ProjectGenerationListener listener = mock(ProjectGenerationListener)
projectGenerator.listeners << listener
ProjectRequest request = createProjectRequest('web')
generateGradleBuild(request)
verify(listener, times(1)).onGeneratedProject(request)
}
@Test
void defaultProject() {
ProjectGenerationListener listener = mock(ProjectGenerationListener)
projectGenerator.listeners << listener
ProjectRequest request = createProjectRequest('web')
generateProject(request).isJavaProject().isMavenProject().pomAssert()
.hasStartClass('demo.Application').hasNoRepository().hasSpringBootStarterDependency('web')
verify(listener, times(1)).onGeneratedProject(request)
}
@Test

View File

@@ -0,0 +1,57 @@
/*
* 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 io.spring.initializr.support
import static org.junit.Assert.assertEquals
import static org.junit.Assert.fail
/**
* Metrics assertion based on {@link TestCounterService}.
*
* @author Stephane Nicoll
*/
class MetricsAssert {
private final TestCounterService counterService
MetricsAssert(TestCounterService counterService) {
this.counterService = counterService
}
MetricsAssert hasValue(long value, String... metrics) {
metrics.each {
Long actual = counterService.values[it]
if (actual == null) {
fail('Metric ' + it + ' not found, got ' + counterService.values.keySet())
}
assertEquals 'Wrong value for metric ' + it, value, actual
}
this
}
MetricsAssert hasNoValue(String... metrics) {
metrics.each {
assertEquals 'Metric ' + it + ' should not be registered', null, counterService.values.get(it)
}
}
MetricsAssert metricsCount(int count) {
assertEquals 'Wrong number of metrics, got ' + counterService.values.keySet(),
count, counterService.values.size()
this
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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 io.spring.initializr.support
import org.springframework.boot.actuate.metrics.CounterService
/**
* A test {@link CounterService} that keeps track of the metric values.
*
* @author Stephane Nicoll
*/
class TestCounterService implements CounterService {
final Map<String, Long> values = new HashMap<>()
@Override
void increment(String metricName) {
Long value = values.get(metricName)
value != null ? values.put(metricName, ++value) : values.put(metricName, 1)
}
@Override
void decrement(String metricName) {
Long value = values.get(metricName)
value != null ? values.put(metricName, --value) : values.put(metricName, -1)
}
@Override
void reset(String metricName) {
values.put(metricName, 0)
}
}

View File

@@ -18,6 +18,7 @@ package io.spring.initializr.web
import java.nio.charset.Charset
import groovy.json.JsonSlurper
import io.spring.initializr.support.ProjectAssert
import org.json.JSONObject
import org.junit.Test
@@ -104,6 +105,36 @@ class MainControllerIntegrationTests extends AbstractInitializrControllerIntegra
JSONAssert.assertEquals(expected, new JSONObject(json), JSONCompareMode.LENIENT)
}
@Test
void metricsAvailableByDefault() {
downloadZip('/starter.zip?packaging=jar&javaVersion=1.8&style=web&style=jpa')
def result = metricsEndpoint()
Long requests = result['counter.initializr.requests']
Long packaging = result['counter.initializr.packaging.jar']
Long javaVersion = result['counter.initializr.java_version.1_8']
Long webDependency = result['counter.initializr.dependency.web']
Long jpaDependency = result['counter.initializr.dependency.jpa']
downloadZip('/starter.zip?packaging=jar&javaVersion=1.8&style=web') // No jpa dep this time
def updatedResult = metricsEndpoint()
assertEquals 'Number of request should have increased',
requests + 1, updatedResult['counter.initializr.requests']
assertEquals 'jar packaging metric should have increased',
packaging + 1, updatedResult['counter.initializr.packaging.jar']
assertEquals 'java version metric should have increased',
javaVersion + 1, updatedResult['counter.initializr.java_version.1_8']
assertEquals 'web dependency metric should have increased',
webDependency + 1, updatedResult['counter.initializr.dependency.web']
assertEquals 'jpa dependency metric should not have increased',
jpaDependency, updatedResult['counter.initializr.dependency.jpa']
}
private def metricsEndpoint() {
JsonSlurper slurper = new JsonSlurper()
slurper.parseText(restTemplate.getForObject(createUrl('/metrics'), String))
}
// Existing tests for backward compatibility
@Test