Modularize project structure

This commit splits the feature of Spring Initializr in several modules:

* `initializr-generator` is a standalone library that is responsible for
generating projects based on a `File` directory. It has a minimal set of
dependencies and is not web-related
* `initializr-web` provides the web integration (project generation,
meta-data, etc)
* `initializr-actuator` is an optional module that can be added to
support project-generation-specific statistics

Closes gh-214
This commit is contained in:
Stephane Nicoll
2016-04-05 17:50:45 +02:00
parent 35cfc8f139
commit 542ee7d91b
185 changed files with 1184 additions and 893 deletions

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2012-2016 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.actuate.autoconfigure
import io.spring.initializr.actuate.metric.ProjectGenerationMetricsListener
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.actuate.autoconfigure.ExportMetricWriter
import org.springframework.boot.actuate.autoconfigure.MetricExportAutoConfiguration
import org.springframework.boot.actuate.metrics.CounterService
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.ConditionalOnProperty
import org.springframework.boot.autoconfigure.data.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.data.redis.connection.RedisConnectionFactory
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.util.ObjectUtils
/**
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
* Auto-configuration} to handle the metrics of an initializr instance.
*
* @author Dave Syer
* @since 1.0
*/
@Configuration
@AutoConfigureAfter([RedisAutoConfiguration, MetricExportAutoConfiguration])
class InitializrMetricsConfiguration {
@Bean
ProjectGenerationMetricsListener metricsListener(CounterService counterService) {
new ProjectGenerationMetricsListener(counterService)
}
@ConditionalOnBean(RedisConnectionFactory)
@ConditionalOnProperty(value = 'spring.metrics.export.enabled')
@EnableScheduling
@EnableConfigurationProperties(MetricsProperties)
@Configuration
public static class MetricsExportConfiguration {
@Autowired
RedisConnectionFactory connectionFactory
@Autowired
MetricsProperties metrics
@Autowired
ApplicationContext context
@Bean
@ExportMetricWriter
MetricWriter writer() {
new RedisMetricRepository(connectionFactory,
metrics.prefix + metrics.getId(context.getId()) + '.'
+ ObjectUtils.getIdentityHexString(context) + '.',
metrics.key)
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2012-2016 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.actuate.autoconfigure
import io.spring.initializr.actuate.stat.ProjectGenerationStatPublisher
import io.spring.initializr.actuate.stat.ProjectRequestDocumentFactory
import io.spring.initializr.actuate.stat.StatsProperties
import io.spring.initializr.metadata.InitializrMetadataProvider
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.retry.backoff.ExponentialBackOffPolicy
import org.springframework.retry.policy.SimpleRetryPolicy
import org.springframework.retry.support.RetryTemplate
/**
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
* Auto-configuration} to publish statistics of each generated project.
*
* @author Stephane Nicoll
* @since 1.0
*/
@Configuration
@EnableConfigurationProperties(StatsProperties)
@ConditionalOnProperty('initializr.stats.elastic.uri')
class InitializrStatsAutoConfiguration {
@Autowired
private StatsProperties statsProperties
@Bean
ProjectGenerationStatPublisher projectRequestStatHandler(InitializrMetadataProvider provider) {
new ProjectGenerationStatPublisher(new ProjectRequestDocumentFactory(provider),
statsProperties, statsRetryTemplate())
}
@Bean
@ConditionalOnMissingBean(name = "statsRetryTemplate")
RetryTemplate statsRetryTemplate() {
RetryTemplate retryTemplate = new RetryTemplate()
def backOffPolicy = new ExponentialBackOffPolicy(initialInterval: 3000L, multiplier: 3)
def retryPolicy = new SimpleRetryPolicy(statsProperties.elastic.maxAttempts, Collections
.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true))
retryTemplate.setBackOffPolicy(backOffPolicy)
retryTemplate.setRetryPolicy(retryPolicy)
retryTemplate
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2012-2016 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.actuate.autoconfigure
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
}
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright 2012-2016 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.actuate.metric
import io.spring.initializr.generator.ProjectFailedEvent
import io.spring.initializr.generator.ProjectGeneratedEvent
import io.spring.initializr.generator.ProjectRequest
import io.spring.initializr.util.Agent
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.actuate.metrics.CounterService
import org.springframework.context.event.EventListener
import org.springframework.util.StringUtils
/**
* A {@link ProjectGeneratedEvent} listener that uses a {@link CounterService} to update
* various project related metrics.
*
* @author Stephane Nicoll
* @since 1.0
*/
class ProjectGenerationMetricsListener {
private final CounterService counterService
@Autowired
ProjectGenerationMetricsListener(CounterService counterService) {
this.counterService = counterService
}
@EventListener
void onGeneratedProject(ProjectGeneratedEvent event) {
handleProjectRequest(event.projectRequest)
}
@EventListener
void onFailedProject(ProjectFailedEvent event) {
handleProjectRequest(event.projectRequest)
increment(key('failures'))
}
protected void handleProjectRequest(ProjectRequest request) {
increment(key('requests')) // Total number of requests
handleDependencies(request)
handleType(request)
handleJavaVersion(request)
handlePackaging(request)
handleLanguage(request)
handleBootVersion(request)
handleUserAgent(request)
}
protected void handleDependencies(ProjectRequest request) {
request.resolvedDependencies.each {
if (!ProjectRequest.DEFAULT_STARTER.equals(it.id)) {
def id = sanitize(it.id)
increment(key("dependency.$id"))
}
}
}
protected void handleType(ProjectRequest request) {
if (StringUtils.hasText(request.type)) {
def type = sanitize(request.type)
increment(key("type.$type"))
}
}
protected void handleJavaVersion(ProjectRequest request) {
if (StringUtils.hasText(request.javaVersion)) {
def javaVersion = sanitize(request.javaVersion)
increment(key("java_version.$javaVersion"))
}
}
protected void handlePackaging(ProjectRequest request) {
if (StringUtils.hasText(request.packaging)) {
def packaging = sanitize(request.packaging)
increment(key("packaging.$packaging"))
}
}
protected void handleLanguage(ProjectRequest request) {
if (StringUtils.hasText(request.language)) {
def language = sanitize(request.language)
increment(key("language.$language"))
}
}
protected void handleBootVersion(ProjectRequest request) {
if (StringUtils.hasText(request.bootVersion)) {
def bootVersion = sanitize(request.bootVersion)
increment(key("boot_version.$bootVersion"))
}
}
protected void handleUserAgent(ProjectRequest request) {
String userAgent = request.parameters['user-agent']
if (userAgent) {
Agent agent = Agent.fromUserAgent(userAgent)
if (agent) {
increment(key("client_id.$agent.id.id"))
}
}
}
protected void increment(String key) {
counterService.increment(key)
}
protected String key(String part) {
"initializr.$part"
}
protected String sanitize(String s) {
s.replace('.', '_')
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2012-2016 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.actuate.stat
import java.nio.charset.StandardCharsets
import org.springframework.http.HttpRequest
import org.springframework.http.client.ClientHttpRequestExecution
import org.springframework.http.client.ClientHttpRequestInterceptor
import org.springframework.http.client.ClientHttpResponse
import org.springframework.http.client.InterceptingClientHttpRequestFactory
import org.springframework.util.Base64Utils
import org.springframework.web.client.RestTemplate
/**
* A simple {@link RestTemplate} extension that automatically provides the
* {@code Authorization} header if credentials are provided.
* <p>
* Largely inspired from Spring Boot's {@code TestRestTemplate}.
*
* @author Stephane Nicoll
* @since 1.0
*/
class BasicAuthRestTemplate extends RestTemplate {
/**
* Create a new instance. {@code username} and {@code password} can be
* {@code null} if no authentication is necessary.
*/
BasicAuthRestTemplate(String username, String password) {
addAuthentication(username, password)
}
private void addAuthentication(String username, String password) {
if (!username) {
return;
}
List<ClientHttpRequestInterceptor> interceptors = Collections
.<ClientHttpRequestInterceptor> singletonList(
new BasicAuthorizationInterceptor(username, password))
setRequestFactory(new InterceptingClientHttpRequestFactory(getRequestFactory(),
interceptors))
}
private static class BasicAuthorizationInterceptor
implements ClientHttpRequestInterceptor {
private final String username
private final String password
BasicAuthorizationInterceptor(String username, String password) {
this.username = username;
this.password = (password == null ? "" : password)
}
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
String token = Base64Utils.encodeToString(
(this.username + ":" + this.password).getBytes(StandardCharsets.UTF_8))
request.getHeaders().add("Authorization", "Basic " + token)
return execution.execute(request, body)
}
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2012-2016 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.actuate.stat
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.databind.ObjectMapper
import groovy.util.logging.Slf4j
import io.spring.initializr.generator.ProjectRequestEvent
import org.springframework.context.event.EventListener
import org.springframework.http.MediaType
import org.springframework.http.RequestEntity
import org.springframework.retry.RetryCallback
import org.springframework.retry.RetryContext
import org.springframework.retry.support.RetryTemplate
import org.springframework.scheduling.annotation.Async
import org.springframework.web.client.RestTemplate
/**
* Publish stats for each project generated to an Elastic index.
*
* @author Stephane Nicoll
* @since 1.0
*/
@Slf4j
class ProjectGenerationStatPublisher {
private final ProjectRequestDocumentFactory documentFactory
private final StatsProperties statsProperties
private final ObjectMapper objectMapper
private final RestTemplate restTemplate
private final RetryTemplate retryTemplate
ProjectGenerationStatPublisher(ProjectRequestDocumentFactory documentFactory,
StatsProperties statsProperties,
RetryTemplate retryTemplate) {
this.documentFactory = documentFactory
this.statsProperties = statsProperties
this.objectMapper = createObjectMapper()
this.restTemplate = new BasicAuthRestTemplate(
statsProperties.elastic.username, statsProperties.elastic.password)
this.retryTemplate = retryTemplate
}
@EventListener
@Async
void handleEvent(ProjectRequestEvent event) {
String json = null
try {
ProjectRequestDocument document = documentFactory.createDocument(event)
if (log.isDebugEnabled()) {
log.debug("Publishing $document")
}
json = toJson(document)
RequestEntity<String> request = RequestEntity
.post(this.statsProperties.elastic.entityUrl)
.contentType(MediaType.APPLICATION_JSON)
.body(json)
this.retryTemplate.execute(new RetryCallback<Void, RuntimeException>() {
@Override
Void doWithRetry(RetryContext context) {
restTemplate.exchange(request, String)
return null
}
})
} catch (Exception ex) {
log.warn(String.format(
"Failed to publish stat to index, document follows %n%n%s%n", json), ex)
}
}
private String toJson(ProjectRequestDocument stats) {
this.objectMapper.writeValueAsString(stats)
}
private static ObjectMapper createObjectMapper() {
def mapper = new ObjectMapper()
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL)
mapper
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-2016 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.actuate.stat
import groovy.transform.ToString
/**
* Define the statistics of a project generation.
*
* @author Stephane Nicoll
* @since 1.0
*/
@ToString(ignoreNulls = true, includePackage = false, includeNames = true)
class ProjectRequestDocument {
long generationTimestamp
String requestIp
String requestIpv4
String requestCountry
String clientId
String clientVersion
String groupId
String artifactId
String packageName
String bootVersion
String javaVersion
String language
String packaging
String type
final List<String> dependencies = []
String errorMessage
boolean invalid
boolean invalidJavaVersion
boolean invalidLanguage
boolean invalidPackaging
boolean invalidType
final List<String> invalidDependencies = []
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright 2012-2016 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.actuate.stat
import java.util.regex.Matcher
import java.util.regex.Pattern
import io.spring.initializr.generator.ProjectFailedEvent
import io.spring.initializr.generator.ProjectRequest
import io.spring.initializr.generator.ProjectRequestEvent
import io.spring.initializr.metadata.InitializrMetadataProvider
import io.spring.initializr.util.Agent
/**
* Create {@link ProjectRequestDocument} instances.
*
* @author Stephane Nicoll
* @since 1.0
*/
class ProjectRequestDocumentFactory {
private static final IP_PATTERN = Pattern.compile("[0-9]*\\.[0-9]*\\.[0-9]*\\.[0-9]*")
private final InitializrMetadataProvider metadataProvider
ProjectRequestDocumentFactory(InitializrMetadataProvider metadataProvider) {
this.metadataProvider = metadataProvider
}
ProjectRequestDocument createDocument(ProjectRequestEvent event) {
def metadata = metadataProvider.get()
def request = event.projectRequest
ProjectRequestDocument document = new ProjectRequestDocument()
document.generationTimestamp = event.timestamp
handleCloudFlareHeaders(request, document)
def candidate = request.parameters['x-forwarded-for']
if (!document.requestIp && candidate) {
document.requestIp = candidate
document.requestIpv4 = extractIpv4(candidate)
}
Agent agent = extractAgentInformation(request)
if (agent) {
document.clientId = agent.id.id
document.clientVersion = agent.version
}
document.groupId = request.groupId
document.artifactId = request.artifactId
document.packageName = request.packageName
document.bootVersion = request.bootVersion
document.javaVersion = request.javaVersion
if (request.javaVersion && !metadata.javaVersions.get(request.javaVersion)) {
document.invalid = true
document.invalidJavaVersion = true
}
document.language = request.language
if (request.language && !metadata.languages.get(request.language)) {
document.invalid = true
document.invalidLanguage = true
}
document.packaging = request.packaging
if (request.packaging && !metadata.packagings.get(request.packaging)) {
document.invalid = true
document.invalidPackaging = true
}
document.type = request.type
if (request.type && !metadata.types.get(request.type)) {
document.invalid = true
document.invalidType = true
}
// Let's not rely on the resolved dependencies here
def dependencies = []
dependencies.addAll(request.style)
dependencies.addAll(request.dependencies)
dependencies.each { id ->
if (metadata.dependencies.get(id)) {
document.dependencies << id
} else {
document.invalid = true
document.invalidDependencies << id
}
}
// Let's make sure that the document is flagged as invalid no matter what
if (event instanceof ProjectFailedEvent) {
document.invalid = true
if (event.cause) {
document.errorMessage = event.cause.message
}
}
document
}
private static void handleCloudFlareHeaders(ProjectRequest request, ProjectRequestDocument document) {
def candidate = request.parameters['cf-connecting-ip']
if (candidate) {
document.requestIp = candidate
document.requestIpv4 = extractIpv4(candidate)
}
String country = request.parameters['cf-ipcountry']
if (country && !country.toLowerCase().equals('xx')) {
document.requestCountry = country
}
}
private static Agent extractAgentInformation(ProjectRequest request) {
String userAgent = request.parameters['user-agent']
if (userAgent) {
return Agent.fromUserAgent(userAgent)
}
return null
}
private static String extractIpv4(def candidate) {
if (candidate) {
Matcher matcher = IP_PATTERN.matcher(candidate)
if (matcher.find()) {
return matcher.group()
}
}
return null
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2012-2016 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.actuate.stat
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.util.StringUtils
/**
* Statistics-related properties.
*
* @author Stephane Nicoll
* @since 1.0
*/
@ConfigurationProperties("initializr.stats")
class StatsProperties {
final Elastic elastic = new Elastic()
static final class Elastic {
/**
* Elastic service uri.
*/
String uri
/**
* Elastic service username.
*/
String username
/**
* Elastic service password
*/
String password
/**
* Name of the index.
*/
String indexName = 'initializr'
/**
* Name of the entity to use to publish stats.
*/
String entityName = 'request'
/**
* Number of attempts before giving up.
*/
int maxAttempts = 3
void setUri(String uri) {
this.uri = cleanUri(uri)
}
URI getEntityUrl() {
def string = "$uri/$indexName/$entityName"
new URI(string)
}
private static String cleanUri(String contextPath) {
if (StringUtils.hasText(contextPath) && contextPath.endsWith("/")) {
return contextPath.substring(0, contextPath.length() - 1)
}
return contextPath
}
}
}

View File

@@ -0,0 +1,55 @@
{
"groups": [
{
"name": "initializr.stats",
"type": "io.spring.initializr.actuate.stat.StatsProperties",
"sourceType": "io.spring.initializr.actuate.stat.StatsProperties"
},
{
"name": "initializr.stats.elastic",
"type": "io.spring.initializr.actuate.stat.StatsProperties$Elastic",
"sourceType": "io.spring.initializr.actuate.stat.StatsProperties",
"sourceMethod": "getElastic()"
}
],
"properties": [
{
"name": "initializr.stats.elastic.uri",
"type": "java.lang.String",
"description": "Elastic service uri.",
"sourceType": "io.spring.initializr.actuate.stat.StatsProperties$Elastic"
},
{
"name": "initializr.stats.elastic.username",
"type": "java.lang.String",
"description": "Elastic service username.",
"sourceType": "io.spring.initializr.actuate.stat.StatsProperties$Elastic"
},
{
"name": "initializr.stats.elastic.password",
"type": "java.lang.String",
"description": "Elastic service password.",
"sourceType": "io.spring.initializr.actuate.stat.StatsProperties$Elastic"
},
{
"name": "initializr.stats.elastic.index-name",
"type": "java.lang.String",
"description": "Name of the index.",
"sourceType": "io.spring.initializr.actuate.stat.StatsProperties$Elastic"
},
{
"name": "initializr.stats.elastic.entity-name",
"type": "java.lang.String",
"description": "Name of the entity to use to publish stats.",
"sourceType": "io.spring.initializr.actuate.stat.StatsProperties$Elastic"
},
{
"name": "initializr.stats.elastic.max-attempts",
"type": "java.lang.Integer",
"description": "Number of attempts before giving up.",
"defaultValue": 3,
"sourceType": "io.spring.initializr.actuate.stat.StatsProperties$Elastic"
}
],
"hints": []
}

View File

@@ -0,0 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
io.spring.initializr.actuate.autoconfigure.InitializrStatsAutoConfiguration,\
io.spring.initializr.actuate.autoconfigure.InitializrMetricsConfiguration

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2012-2016 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.actuate
import groovy.json.JsonSlurper
import io.spring.initializr.web.AbstractInitializrControllerIntegrationTests
import org.junit.Test
import org.springframework.test.context.ActiveProfiles
import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertTrue
/**
* Tests for actuator specific features.
*
* @author Stephane Nicoll
*/
@ActiveProfiles('test-default')
class ActuatorIntegrationTests extends AbstractInitializrControllerIntegrationTests {
private final def slurper = new JsonSlurper()
@Test
void infoHasExternalProperties() {
def body = restTemplate.getForObject(createUrl('/info'), String)
assertTrue("Wrong body:\n$body", body.contains('"spring-boot"'))
assertTrue("Wrong body:\n$body", body.contains('"version":"1.1.4.RELEASE"'))
}
@Test
void metricsAvailableByDefault() {
downloadZip('/starter.zip?packaging=jar&javaVersion=1.8&style=web&style=jpa')
def result = metricsEndpoint()
def requests = result['counter.initializr.requests']
def packaging = result['counter.initializr.packaging.jar']
def javaVersion = result['counter.initializr.java_version.1_8']
def webDependency = result['counter.initializr.dependency.web']
def 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() {
parseJson(restTemplate.getForObject(createUrl('/metrics'), String))
}
private def parseJson(String content) {
slurper.parseText(content)
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2012-2016 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.actuate.metric
import io.spring.initializr.actuate.test.RedisRunning
import io.spring.initializr.generator.ProjectGeneratedEvent
import io.spring.initializr.generator.ProjectRequest
import io.spring.initializr.metadata.InitializrMetadataBuilder
import io.spring.initializr.metadata.InitializrMetadataProvider
import io.spring.initializr.metadata.InitializrProperties
import io.spring.initializr.metadata.SimpleInitializrMetadataProvider
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.context.properties.EnableConfigurationProperties
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.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 ProjectGeneratedEvent(new ProjectRequest()))
Thread.sleep(1000L)
assertTrue("No metrics exported", repository.findAll().size() > 0)
}
@EnableAutoConfiguration
@EnableConfigurationProperties(InitializrProperties)
static class Config {
@Bean
InitializrMetadataProvider initializrMetadataProvider(InitializrProperties properties) {
def metadata = InitializrMetadataBuilder.fromInitializrProperties(properties).build()
new SimpleInitializrMetadataProvider(metadata)
}
}
}

View File

@@ -0,0 +1,267 @@
/*
* Copyright 2012-2016 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.actuate.metric
import io.spring.initializr.actuate.test.MetricsAssert
import io.spring.initializr.actuate.test.TestCounterService
import io.spring.initializr.generator.ProjectFailedEvent
import io.spring.initializr.generator.ProjectGeneratedEvent
import io.spring.initializr.generator.ProjectRequest
import io.spring.initializr.metadata.Dependency
import io.spring.initializr.metadata.InitializrMetadata
import io.spring.initializr.test.metadata.InitializrMetadataTestBuilder
import org.junit.Before
import org.junit.Test
/**
* @author Stephane Nicoll
*/
class ProjectGenerationMetricsListenerTests {
private InitializrMetadata metadata = InitializrMetadataTestBuilder.withDefaults()
.addDependencyGroup('core', 'web', 'security', 'spring-data').build()
private ProjectGenerationMetricsListener listener
private MetricsAssert metricsAssert
@Before
void setup() {
def counterService = new TestCounterService()
listener = new ProjectGenerationMetricsListener(counterService)
metricsAssert = new MetricsAssert(counterService)
}
@Test
void projectGenerationCount() {
def request = initialize()
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.requests')
}
@Test
void projectGenerationCountWithFailure() {
def request = initialize()
request.resolve(metadata)
fireProjectFailedEvent(request)
metricsAssert.hasValue(1, 'initializr.requests')
metricsAssert.hasValue(1, 'initializr.failures')
}
@Test
void dependencies() {
def request = initialize()
request.style << 'security' << 'spring-data'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.dependency.security',
'initializr.dependency.spring-data')
}
@Test
void noDependencies() {
def request = initialize()
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasNoValue('initializr.dependency.')
}
@Test
void resolvedWebDependency() {
def request = initialize()
request.style << 'spring-data'
request.packaging = 'war'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.dependency.web',
'initializr.dependency.spring-data')
}
@Test
void aliasedDependencyUseStandardId() {
def dependency = new Dependency()
dependency.id = 'foo'
dependency.aliases << 'foo-old'
def metadata = InitializrMetadataTestBuilder.withDefaults()
.addDependencyGroup('core', dependency).build()
def request = new ProjectRequest()
request.initialize(metadata)
request.style << 'foo-old'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.dependency.foo') // standard id is used
}
@Test
void defaultType() {
def request = initialize()
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.type.maven-project')
}
@Test
void explicitType() {
def request = initialize()
request.type = 'gradle-build'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.type.gradle-build')
}
@Test
void defaultPackaging() {
def request = initialize()
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.packaging.jar')
}
@Test
void explicitPackaging() {
def request = initialize()
request.packaging = 'war'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.packaging.war')
}
@Test
void defaultJavaVersion() {
def request = initialize()
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.java_version.1_8')
}
@Test
void explicitJavaVersion() {
def request = initialize()
request.javaVersion = '1.7'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.java_version.1_7')
}
@Test
void defaultLanguage() {
def request = initialize()
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.language.java')
}
@Test
void explicitGroovyLanguage() {
def request = initialize()
request.language = 'groovy'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.language.groovy')
}
@Test
void explicitKotlinLanguage() {
def request = initialize()
request.language = 'kotlin'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.language.kotlin')
}
@Test
void defaultBootVersion() {
def request = initialize()
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.boot_version.1_2_3_RELEASE')
}
@Test
void explicitBootVersion() {
def request = initialize()
request.bootVersion = '1.0.2.RELEASE'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.boot_version.1_0_2_RELEASE')
}
@Test
void userAgentAvailable() {
def request = initialize()
request.parameters['user-agent'] = 'HTTPie/0.9.2'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.client_id.httpie')
}
@Test
void collectAllMetrics() {
def request = initialize()
request.style << 'web' << 'security'
request.type = 'gradle-project'
request.packaging = 'jar'
request.javaVersion = '1.6'
request.language = 'groovy'
request.bootVersion = '1.0.2.RELEASE'
request.parameters['user-agent'] = 'SpringBootCli/1.3.0.RELEASE'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.requests',
'initializr.dependency.web', 'initializr.dependency.security',
'initializr.type.gradle-project', 'initializr.packaging.jar',
'initializr.java_version.1_6', 'initializr.language.groovy',
'initializr.boot_version.1_0_2_RELEASE',
'initializr.client_id.spring').metricsCount(9)
}
@Test
void incrementMetrics() {
def request = initialize()
request.style << 'security' << 'spring-data'
request.resolve(metadata)
fireProjectGeneratedEvent(request)
metricsAssert.hasValue(1, 'initializr.requests',
'initializr.dependency.security', 'initializr.dependency.spring-data')
def anotherRequest = initialize()
anotherRequest.style << 'web' << 'spring-data'
anotherRequest.resolve(metadata)
fireProjectGeneratedEvent(anotherRequest)
metricsAssert.hasValue(2, 'initializr.dependency.spring-data',
'initializr.dependency.spring-data')
metricsAssert.hasValue(1, 'initializr.dependency.web',
'initializr.dependency.security')
}
private fireProjectGeneratedEvent(ProjectRequest projectRequest) {
listener.onGeneratedProject(new ProjectGeneratedEvent(projectRequest))
}
private fireProjectFailedEvent(ProjectRequest projectRequest) {
listener.onFailedProject(new ProjectFailedEvent(projectRequest, null))
}
private ProjectRequest initialize() {
def request = new ProjectRequest()
request.initialize(metadata)
request
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2012-2016 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.actuate.stat
import io.spring.initializr.generator.ProjectRequest
import io.spring.initializr.metadata.InitializrMetadataProvider
import io.spring.initializr.metadata.SimpleInitializrMetadataProvider
import io.spring.initializr.test.metadata.InitializrMetadataTestBuilder
/**
* @author Stephane Nicoll
*/
abstract class AbstractInitializrStatTests {
def metadata = InitializrMetadataTestBuilder
.withDefaults()
.addDependencyGroup('core', 'security', 'validation', 'aop')
.addDependencyGroup('web', 'web', 'data-rest', 'jersey')
.addDependencyGroup('data', 'data-jpa', 'jdbc')
.addDependencyGroup('database', 'h2', 'mysql')
.build()
protected InitializrMetadataProvider createProvider(def metadata) {
new SimpleInitializrMetadataProvider(metadata)
}
protected ProjectRequest createProjectRequest() {
ProjectRequest request = new ProjectRequest()
request.initialize(metadata)
request
}
}

View File

@@ -0,0 +1,197 @@
/*
* Copyright 2012-2016 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.actuate.stat
import groovy.json.JsonSlurper
import io.spring.initializr.web.AbstractInitializrControllerIntegrationTests
import org.junit.Before
import org.junit.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.SpringApplicationConfiguration
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.RequestEntity
import org.springframework.test.context.ActiveProfiles
import org.springframework.util.Base64Utils
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.client.HttpClientErrorException
import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertFalse
import static org.junit.Assert.assertNotNull
import static org.junit.Assert.assertTrue
import static org.junit.Assert.fail
/**
* Integration tests for stats processing.
*
* @author Stephane Nicoll
*/
@SpringApplicationConfiguration(StatsMockController.class)
@ActiveProfiles(['test-default', 'test-custom-stats'])
class MainControllerStatsIntegrationTests extends AbstractInitializrControllerIntegrationTests {
@Autowired
private StatsMockController statsMockController
@Autowired
private StatsProperties statsProperties
private final JsonSlurper slurper = new JsonSlurper()
@Before
public void setup() {
this.statsMockController.stats.clear()
// Make sure our mock is going to be invoked with the stats
this.statsProperties.elastic.uri = "http://localhost:$port/elastic"
}
@Test
void simpleProject() {
downloadArchive('/starter.zip?groupId=com.foo&artifactId=bar&dependencies=web')
assertEquals 'No stat got generated', 1, statsMockController.stats.size()
def content = statsMockController.stats[0]
def json = slurper.parseText(content.json)
assertEquals 'com.foo', json.groupId
assertEquals 'bar', json.artifactId
assertEquals 1, json.dependencies.size()
assertEquals 'web', json.dependencies[0]
}
@Test
void authorizationHeaderIsSet() {
downloadArchive('/starter.zip')
assertEquals 'No stat got generated', 1, statsMockController.stats.size()
def content = statsMockController.stats[0]
def authorization = content.authorization
assertNotNull 'Authorization header must be set', authorization
assertTrue 'Wrong value for authorization header', authorization.startsWith('Basic ')
def token = authorization.substring('Basic '.length(), authorization.size())
def data = new String(Base64Utils.decodeFromString(token)).split(':')
assertEquals "Wrong user from $token", 'test-user', data[0]
assertEquals "Wrong password $token", 'test-password', data[1]
}
@Test
void requestIpNotSetByDefault() {
downloadArchive('/starter.zip?groupId=com.foo&artifactId=bar&dependencies=web')
assertEquals 'No stat got generated', 1, statsMockController.stats.size()
def content = statsMockController.stats[0]
def json = slurper.parseText(content.json)
assertFalse 'requestIp property should not be set', json.containsKey('requestIp')
}
@Test
void requestIpIsSetWhenHeaderIsPresent() {
RequestEntity<?> request = RequestEntity.get(new URI(createUrl('/starter.zip')))
.header('X-FORWARDED-FOR', '10.0.0.123').build()
restTemplate.exchange(request, String)
assertEquals 'No stat got generated', 1, statsMockController.stats.size()
def content = statsMockController.stats[0]
def json = slurper.parseText(content.json)
assertEquals 'Wrong requestIp', '10.0.0.123', json.requestIp
}
@Test
void requestIpv4IsNotSetWhenHeaderHasGarbage() {
RequestEntity<?> request = RequestEntity.get(new URI(createUrl('/starter.zip')))
.header('x-forwarded-for', 'foo-bar').build()
restTemplate.exchange(request, String)
assertEquals 'No stat got generated', 1, statsMockController.stats.size()
def content = statsMockController.stats[0]
def json = slurper.parseText(content.json)
assertFalse 'requestIpv4 property should not be set if value is not a valid IPv4',
json.containsKey('requestIpv4')
}
@Test
void requestCountryIsNotSetWhenHeaderIsSetToXX() {
RequestEntity<?> request = RequestEntity.get(new URI(createUrl('/starter.zip')))
.header('cf-ipcountry', 'XX').build()
restTemplate.exchange(request, String)
assertEquals 'No stat got generated', 1, statsMockController.stats.size()
def content = statsMockController.stats[0]
def json = slurper.parseText(content.json)
assertFalse 'requestCountry property should not be set if value is set to xx',
json.containsKey('requestCountry')
}
@Test
void invalidProjectSillHasStats() {
try {
downloadArchive('/starter.zip?type=invalid-type')
fail("Should have failed to generate project with invalid type")
} catch (HttpClientErrorException ex) {
assertEquals HttpStatus.BAD_REQUEST, ex.statusCode
}
assertEquals 'No stat got generated', 1, statsMockController.stats.size()
def content = statsMockController.stats[0]
def json = slurper.parseText(content.json)
assertEquals 'com.example', json.groupId
assertEquals 'demo', json.artifactId
assertEquals true, json.invalid
assertEquals true, json.invalidType
assertNotNull json.errorMessage
assertTrue json.errorMessage.contains('invalid-type')
}
@Test
void errorPublishingStatsDoesNotBubbleUp() {
this.statsProperties.elastic.uri = "http://localhost:$port/elastic-error"
downloadArchive('/starter.zip')
assertEquals 'No stat should be available', 0, statsMockController.stats.size()
}
@RestController
static class StatsMockController {
private final List<Content> stats = []
@RequestMapping(path = '/elastic/test/my-entity', method = RequestMethod.POST)
void handleProjectRequestDocument(RequestEntity<String> input) {
def authorization = input.headers.getFirst(HttpHeaders.AUTHORIZATION)
def content = new Content(authorization: authorization, json: input.body)
this.stats << content
}
@RequestMapping(path = '/elastic-error/test/my-entity', method = RequestMethod.POST)
void handleExpectedError() {
throw new IllegalStateException('Expected exception')
}
static class Content {
String authorization
String json
}
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright 2012-2016 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.actuate.stat
import io.spring.initializr.generator.ProjectGeneratedEvent
import io.spring.initializr.generator.ProjectRequest
import org.junit.Before
import org.junit.Test
import org.springframework.http.HttpMethod
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.retry.policy.SimpleRetryPolicy
import org.springframework.retry.support.RetryTemplate
import org.springframework.test.web.client.MockRestServiceServer
import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
/**
* @author Stephane Nicoll
*/
class ProjectGenerationStatPublisherTests extends AbstractInitializrStatTests {
private StatsProperties properties
private RetryTemplate retryTemplate
private ProjectGenerationStatPublisher statPublisher
private MockRestServiceServer mockServer
@Before
public void setUp() {
this.properties = createProperties()
ProjectRequestDocumentFactory documentFactory =
new ProjectRequestDocumentFactory(createProvider(metadata))
this.retryTemplate = new RetryTemplate()
this.statPublisher = new ProjectGenerationStatPublisher(documentFactory, properties, retryTemplate)
mockServer = MockRestServiceServer.createServer(this.statPublisher.restTemplate);
}
@Test
public void publishSimpleDocument() {
ProjectRequest request = createProjectRequest()
request.groupId = 'com.example.foo'
request.artifactId = 'my-project'
mockServer.expect(requestTo('http://example.com/elastic/initializr/request'))
.andExpect(method(HttpMethod.POST))
.andExpect(jsonPath('$.groupId').value('com.example.foo'))
.andExpect(jsonPath('$.artifactId').value('my-project'))
.andRespond(withStatus(HttpStatus.CREATED)
.body(mockResponse(UUID.randomUUID().toString(), true))
.contentType(MediaType.APPLICATION_JSON)
)
this.statPublisher.handleEvent(new ProjectGeneratedEvent(request))
mockServer.verify()
}
@Test
public void recoverFromError() {
ProjectRequest request = createProjectRequest()
mockServer.expect(requestTo('http://example.com/elastic/initializr/request'))
.andExpect(method(HttpMethod.POST))
.andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR))
mockServer.expect(requestTo('http://example.com/elastic/initializr/request'))
.andExpect(method(HttpMethod.POST))
.andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR))
mockServer.expect(requestTo('http://example.com/elastic/initializr/request'))
.andExpect(method(HttpMethod.POST))
.andRespond(withStatus(HttpStatus.CREATED)
.body(mockResponse(UUID.randomUUID().toString(), true))
.contentType(MediaType.APPLICATION_JSON))
this.statPublisher.handleEvent(new ProjectGeneratedEvent(request))
mockServer.verify()
}
@Test
public void fatalErrorOnlyLogs() {
ProjectRequest request = createProjectRequest()
this.retryTemplate.setRetryPolicy(new SimpleRetryPolicy(2,
Collections.<Class<? extends Throwable>, Boolean> singletonMap(Exception.class, true)))
mockServer.expect(requestTo('http://example.com/elastic/initializr/request'))
.andExpect(method(HttpMethod.POST))
.andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR))
mockServer.expect(requestTo('http://example.com/elastic/initializr/request'))
.andExpect(method(HttpMethod.POST))
.andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR))
this.statPublisher.handleEvent(new ProjectGeneratedEvent(request))
mockServer.verify()
}
private static String mockResponse(String id, boolean created) {
'{"_index":"initializr","_type":"request","_id":"' + id + '","_version":1,"_shards"' +
':{"total":1,"successful":1,"failed":0},"created":' + created + '}'
}
private static StatsProperties createProperties() {
def properties = new StatsProperties()
properties.elastic.uri = 'http://example.com/elastic'
properties.elastic.username = 'foo'
properties.elastic.password = 'bar'
properties
}
}

View File

@@ -0,0 +1,219 @@
/*
* Copyright 2012-2016 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.actuate.stat
import io.spring.initializr.generator.ProjectFailedEvent
import io.spring.initializr.generator.ProjectGeneratedEvent
import io.spring.initializr.generator.ProjectRequest
import org.junit.Test
import static org.junit.Assert.assertEquals
import static org.junit.Assert.assertFalse
import static org.junit.Assert.assertNull
import static org.junit.Assert.assertTrue
/**
*
* @author Stephane Nicoll
*/
class ProjectRequestDocumentFactoryTests extends AbstractInitializrStatTests {
private final ProjectRequestDocumentFactory factory =
new ProjectRequestDocumentFactory(createProvider(metadata))
@Test
void createDocumentForSimpleProject() {
ProjectRequest request = createProjectRequest()
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertEquals event.timestamp, document.generationTimestamp
assertEquals null, document.requestIp
assertEquals 'com.example', document.groupId
assertEquals 'demo', document.artifactId
assertEquals 'com.example', document.packageName
assertEquals '1.2.3.RELEASE', document.bootVersion
assertEquals '1.8', document.javaVersion
assertEquals 'java', document.language
assertEquals 'jar', document.packaging
assertEquals 'maven-project', document.type
assertEquals 0, document.dependencies.size()
assertValid document
}
@Test
void createDocumentWithRequestIp() {
ProjectRequest request = createProjectRequest()
request.parameters['x-forwarded-for'] = '10.0.0.123'
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertEquals '10.0.0.123', document.requestIp
assertEquals '10.0.0.123', document.requestIpv4
assertNull document.requestCountry
}
@Test
void createDocumentWithRequestIpv6() {
ProjectRequest request = createProjectRequest()
request.parameters['x-forwarded-for'] = '2001:db8:a0b:12f0::1'
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertEquals '2001:db8:a0b:12f0::1', document.requestIp
assertNull document.requestIpv4
assertNull document.requestCountry
}
@Test
void createDocumentWithCloudFlareHeaders() {
ProjectRequest request = createProjectRequest()
request.parameters['cf-connecting-ip'] = '10.0.0.123'
request.parameters['cf-ipcountry'] = 'BE'
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertEquals '10.0.0.123', document.requestIp
assertEquals '10.0.0.123', document.requestIpv4
assertEquals 'BE', document.requestCountry
}
@Test
void createDocumentWithCloudFlareIpv6() {
ProjectRequest request = createProjectRequest()
request.parameters['cf-connecting-ip'] = '2001:db8:a0b:12f0::1'
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertEquals '2001:db8:a0b:12f0::1', document.requestIp
assertNull document.requestIpv4
assertNull document.requestCountry
}
@Test
void createDocumentWithCloudFlareHeadersAndOtherHeaders() {
ProjectRequest request = createProjectRequest()
request.parameters['cf-connecting-ip'] = '10.0.0.123'
request.parameters['x-forwarded-for'] = '192.168.1.101'
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertEquals '10.0.0.123', document.requestIp
assertEquals '10.0.0.123', document.requestIpv4
assertNull document.requestCountry
}
@Test
void createDocumentWithCloudFlareCountrySetToXX() {
ProjectRequest request = createProjectRequest()
request.parameters['cf-connecting-ip'] = 'Xx' // case insensitive
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertNull document.requestCountry
}
@Test
void createDocumentWithUserAgent() {
ProjectRequest request = createProjectRequest()
request.parameters['user-agent'] = 'HTTPie/0.8.0'
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertEquals 'httpie', document.clientId
assertEquals '0.8.0', document.clientVersion
}
@Test
void createDocumentWithUserAgentNoVersion() {
ProjectRequest request = createProjectRequest()
request.parameters['user-agent'] = 'IntelliJ IDEA'
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertEquals 'intellijidea', document.clientId
assertEquals null, document.clientVersion
}
@Test
void createDocumentInvalidJavaVersion() {
ProjectRequest request = createProjectRequest()
request.javaVersion = '1.2'
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertEquals '1.2', document.javaVersion
assertTrue document.invalid
assertTrue document.invalidJavaVersion
}
@Test
void createDocumentInvalidLanguage() {
ProjectRequest request = createProjectRequest()
request.language = 'c++'
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertEquals 'c++', document.language
assertTrue document.invalid
assertTrue document.invalidLanguage
}
@Test
void createDocumentInvalidPackaging() {
ProjectRequest request = createProjectRequest()
request.packaging = 'ear'
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertEquals 'ear', document.packaging
assertTrue document.invalid
assertTrue document.invalidPackaging
}
@Test
void createDocumentInvalidType() {
ProjectRequest request = createProjectRequest()
request.type = 'ant-project'
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertEquals 'ant-project', document.type
assertTrue document.invalid
assertTrue document.invalidType
}
@Test
void createDocumentInvalidDependency() {
ProjectRequest request = createProjectRequest()
request.dependencies << 'web' << 'invalid' << 'data-jpa' << 'invalid-2'
def event = new ProjectGeneratedEvent(request)
def document = factory.createDocument(event)
assertEquals 'web', document.dependencies[0]
assertEquals 'data-jpa', document.dependencies[1]
assertEquals 2, document.dependencies.size()
assertTrue document.invalid
assertEquals 'invalid', document.invalidDependencies[0]
assertEquals 'invalid-2', document.invalidDependencies[1]
assertEquals 2, document.invalidDependencies.size()
}
@Test
void createDocumentWithProjectFailedEvent() {
ProjectRequest request = createProjectRequest()
def event = new ProjectFailedEvent(request, new IllegalStateException('my test message'))
def document = factory.createDocument(event)
assertTrue document.invalid
assertEquals 'my test message', document.errorMessage
}
private static void assertValid(ProjectRequestDocument document) {
assertFalse document.invalid
assertFalse document.invalidJavaVersion
assertFalse document.invalidLanguage
assertFalse document.invalidPackaging
assertEquals 0, document.invalidDependencies.size()
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2012-2016 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.actuate.stat
import io.spring.initializr.actuate.stat.StatsProperties
import org.junit.Test
import static org.junit.Assert.assertThat
import static org.hamcrest.CoreMatchers.is
/**
* @author Stephane Nicoll
*/
class StatsPropertiesTests {
private final StatsProperties properties = new StatsProperties()
@Test
void cleanTrailingSlash() {
properties.elastic.uri = 'http://example.com/'
assertThat(properties.elastic.uri, is('http://example.com'))
}
@Test
void provideEntityUrl() {
properties.elastic.uri = 'http://example.com/'
properties.elastic.indexName = 'my-index'
properties.elastic.entityName = 'foo'
assertThat(properties.elastic.entityUrl.toString(),
is('http://example.com/my-index/foo'))
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012-2016 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.actuate.test
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 {
def 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[it]
}
this
}
MetricsAssert metricsCount(int count) {
assertEquals "Wrong number of metrics, got '${counterService.values.keySet()}",
count, counterService.values.size()
this
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2012-2016 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.actuate.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)
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2012-2016 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.actuate.test
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 = [:]
@Override
void increment(String metricName) {
def value = values[metricName]
def valueToSet = value ? ++value : 1
values[metricName] = valueToSet
}
@Override
void decrement(String metricName) {
def value = values[metricName]
def valueToSet = value ? --value : -1
values[metricName] = valueToSet
}
@Override
void reset(String metricName) {
values[metricName] = 0
}
}

View File

@@ -0,0 +1,9 @@
initializr:
stats:
elastic:
uri: http://localhost:${server.port}/elastic
indexName: test
entityName: my-entity
username: test-user
password: test-password
max-attempts: 1