diff --git a/pom.xml b/pom.xml index 30086f24..1fb88167 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ 8.11.0 1.4.18 2.1.0 - 0.9.4 + 0.10.0 1.1.0-rc.1 1.0.11 1.7 @@ -83,6 +83,11 @@ pom import + + org.springframework.cloud + spring-cloud-starter-atlas + 1.1.0.BUILD-SNAPSHOT + org.springframework.cloud spring-cloud-starter-eureka @@ -113,6 +118,11 @@ spring-cloud-starter-ribbon ${project.version} + + org.springframework.cloud + spring-cloud-starter-spectator + 1.1.0.BUILD-SNAPSHOT + org.springframework.cloud spring-cloud-starter-turbine @@ -153,6 +163,11 @@ spring-cloud-netflix-sidecar ${project.version} + + org.springframework.cloud + spring-cloud-netflix-spectator + 1.1.0.BUILD-SNAPSHOT + org.springframework.cloud spring-cloud-netflix-turbine @@ -221,6 +236,11 @@ servo-core ${servo.version} + + com.fasterxml.jackson.dataformat + jackson-dataformat-smile + ${jackson.version} + com.netflix.eureka eureka-client @@ -410,15 +430,18 @@ spring-cloud-netflix-hystrix-amqp spring-cloud-netflix-hystrix-stream spring-cloud-netflix-eureka-server + spring-cloud-netflix-spectator spring-cloud-netflix-turbine spring-cloud-netflix-turbine-stream spring-cloud-netflix-sidecar + spring-cloud-starter-atlas spring-cloud-starter-eureka spring-cloud-starter-eureka-server spring-cloud-starter-feign spring-cloud-starter-hystrix spring-cloud-starter-hystrix-dashboard spring-cloud-starter-ribbon + spring-cloud-starter-spectator spring-cloud-starter-turbine spring-cloud-starter-turbine-amqp spring-cloud-starter-turbine-stream diff --git a/spring-cloud-netflix-core/pom.xml b/spring-cloud-netflix-core/pom.xml index b9fd731e..ed1623a4 100644 --- a/spring-cloud-netflix-core/pom.xml +++ b/spring-cloud-netflix-core/pom.xml @@ -208,5 +208,20 @@ h2 test + + org.aspectj + aspectjrt + true + + + org.aspectj + aspectjweaver + test + + + com.fasterxml.jackson.dataformat + jackson-dataformat-smile + true + diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/DefaultMetricsTagProvider.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/DefaultMetricsTagProvider.java new file mode 100644 index 00000000..929ba47c --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/DefaultMetricsTagProvider.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.web.servlet.HandlerMapping; + +import com.google.common.collect.ImmutableMap; + +/** + * @author Jon Schneider + */ +public class DefaultMetricsTagProvider implements MetricsTagProvider { + @Override + public Map clientHttpRequestTags(HttpRequest request, + ClientHttpResponse response) { + String urlTemplate = RestTemplateUrlTemplateHolder.getRestTemplateUrlTemplate(); + if (urlTemplate == null) + urlTemplate = "none"; + + String status; + try { + status = (response == null) ? "CLIENT_ERROR" : ((Integer) response + .getRawStatusCode()).toString(); + } + catch (IOException e) { + status = "IO_ERROR"; + } + + String host = request.getURI().getHost(); + + return ImmutableMap.of("method", request.getMethod().name(), "uri", + sanitizeUrlTemplate(urlTemplate.replaceAll("^https?://[^/]+/", "")), + "status", status, "clientName", host != null ? host : "none"); + } + + @Override + public Map httpRequestTags(HttpServletRequest request, + HttpServletResponse response, Object handler, String caller) { + Map tags = new HashMap<>(); + + tags.put("method", request.getMethod()); + tags.put("status", ((Integer) response.getStatus()).toString()); + + String uri = sanitizeUrlTemplate(request + .getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE).toString() + .substring(1)); + tags.put("uri", uri.isEmpty() ? "root" : uri); + + Object exception = request.getAttribute("exception"); + if (exception != null) + tags.put("exception", exception.getClass().getSimpleName()); + + if (caller != null) + tags.put("caller", caller); + + return tags; + } + + /** + * As is, the urlTemplate is not suitable for use with Atlas, as all interactions with + * Atlas take place via query parameters + */ + private String sanitizeUrlTemplate(String urlTemplate) { + return urlTemplate.replaceAll("/", "_").replaceAll("[{}]", "-"); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsClientHttpRequestInterceptor.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsClientHttpRequestInterceptor.java new file mode 100644 index 00000000..556b904e --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsClientHttpRequestInterceptor.java @@ -0,0 +1,82 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.netflix.metrics.servo.ServoMonitorCache; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +import com.netflix.servo.MonitorRegistry; +import com.netflix.servo.monitor.MonitorConfig; +import com.netflix.servo.tag.SmallTagMap; +import com.netflix.servo.tag.Tags; + +/** + * Intercepts RestTemplate requests and records metrics about execution time and results. + * + * @author Jon Schneider + */ +public class MetricsClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { + /** + * The interceptor writes to a Servo MonitorRegistry, which we get away with for now + * because our Spectator implementation is underpinned by a ServoRegistry. When Spring + * Boot (Actuator) provides a more general purpose abstraction for dimensional metrics + * systems, this can be moved there and rewritten against that abstraction. + */ + @Autowired + MonitorRegistry registry; + + @Autowired + Collection tagProviders; + + @Value("${netflix.metrics.restClient.metricName:restclient}") + String metricName; + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + long startTime = System.nanoTime(); + + ClientHttpResponse response = null; + try { + response = execution.execute(request, body); + return response; + } + finally { + SmallTagMap.Builder builder = SmallTagMap.builder(); + for (MetricsTagProvider tagProvider : tagProviders) { + for (Map.Entry tag : tagProvider.clientHttpRequestTags( + request, response).entrySet()) { + builder.add(Tags.newTag(tag.getKey(), tag.getValue())); + } + } + + MonitorConfig.Builder monitorConfigBuilder = MonitorConfig + .builder(metricName); + monitorConfigBuilder.withTags(builder); + + ServoMonitorCache.getTimer(monitorConfigBuilder.build()).record( + System.nanoTime() - startTime, TimeUnit.NANOSECONDS); + } + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsHandlerInterceptor.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsHandlerInterceptor.java new file mode 100644 index 00000000..7ee8dfdc --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsHandlerInterceptor.java @@ -0,0 +1,95 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cloud.netflix.metrics.servo.ServoMonitorCache; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import com.netflix.servo.MonitorRegistry; +import com.netflix.servo.monitor.MonitorConfig; +import com.netflix.servo.tag.SmallTagMap; +import com.netflix.servo.tag.Tags; + +import static org.springframework.web.context.request.RequestAttributes.SCOPE_REQUEST; + +/** + * Intercepts incoming HTTP requests and records metrics about execution time and results. + * + * @author Jon Schneider + */ +public class MetricsHandlerInterceptor extends HandlerInterceptorAdapter { + @Value("${netflix.metrics.rest.metricName:rest}") + String metricName; + + @Value("${netflix.metrics.rest.callerHeader:#{null}}") + String callerHeader; + + @Autowired + MonitorRegistry registry; + + @Autowired + Collection tagProviders; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + RequestContextHolder.getRequestAttributes().setAttribute("requestStartTime", + System.nanoTime(), SCOPE_REQUEST); + return super.preHandle(request, response, handler); + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, + Object handler, Exception ex) throws Exception { + RequestContextHolder.getRequestAttributes().setAttribute("exception", ex, + SCOPE_REQUEST); + Long startTime = (Long) RequestContextHolder.getRequestAttributes().getAttribute( + "requestStartTime", SCOPE_REQUEST); + if (startTime != null) + recordMetric(request, response, handler, startTime); + super.afterCompletion(request, response, handler, ex); + } + + protected void recordMetric(HttpServletRequest request, HttpServletResponse response, + Object handler, Long startTime) { + String caller = null; + if (callerHeader != null) { + caller = request.getHeader(callerHeader); + } + + SmallTagMap.Builder builder = SmallTagMap.builder(); + for (MetricsTagProvider tagProvider : tagProviders) { + Map tags = tagProvider.httpRequestTags(request, response, + handler, caller); + for (Map.Entry tag : tags.entrySet()) { + builder.add(Tags.newTag(tag.getKey(), tag.getValue())); + } + } + + MonitorConfig.Builder monitorConfigBuilder = MonitorConfig.builder(metricName); + monitorConfigBuilder.withTags(builder); + + ServoMonitorCache.getTimer(monitorConfigBuilder.build()).record( + System.nanoTime() - startTime, TimeUnit.NANOSECONDS); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsInterceptorConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsInterceptorConfiguration.java new file mode 100644 index 00000000..d73c1e8a --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsInterceptorConfiguration.java @@ -0,0 +1,80 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics; + +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +import com.netflix.servo.monitor.Monitors; + +/** + * @author Jon Schneider + */ +@Configuration +@ConditionalOnClass({ Monitors.class, MetricReader.class }) +public class MetricsInterceptorConfiguration { + @Configuration + @ConditionalOnWebApplication + static class MetricsWebResourceConfiguration extends WebMvcConfigurerAdapter { + @Bean + MetricsHandlerInterceptor spectatorMonitoringWebResourceInterceptor() { + return new MetricsHandlerInterceptor(); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(spectatorMonitoringWebResourceInterceptor()); + } + } + + @Configuration + @ConditionalOnBean({ RestTemplate.class }) + static class MetricsRestTemplateConfiguration { + @Bean + RestTemplateUrlTemplateCapturingAspect restTemplateUrlTemplateCapturingAspect() { + return new RestTemplateUrlTemplateCapturingAspect(); + } + + @Bean + MetricsClientHttpRequestInterceptor spectatorLoggingClientHttpRequestInterceptor() { + return new MetricsClientHttpRequestInterceptor(); + } + + @Bean + BeanPostProcessor spectatorRestTemplateInterceptorPostProcessor( + final MetricsClientHttpRequestInterceptor interceptor) { + return new BeanPostProcessor() { + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) { + if (bean instanceof RestTemplate) + ((RestTemplate) bean).getInterceptors().add(interceptor); + return bean; + } + }; + } + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsTagProvider.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsTagProvider.java new file mode 100644 index 00000000..34a66eae --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsTagProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics; + +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpResponse; + +/** + * @author Jon Schneider + */ +public interface MetricsTagProvider { + /** + * @param request RestTemplate client HTTP request + * @param response may be null in the event of a client error + * @return a map of tags added to every client HTTP request metric + */ + Map clientHttpRequestTags(HttpRequest request, + ClientHttpResponse response); + + /** + * @param request HTTP request + * @param response HTTP response + * @param handler the request method that is responsible for handling the request + * @return a map of tags added to every Spring MVC HTTP request metric + */ + Map httpRequestTags(HttpServletRequest request, + HttpServletResponse response, Object handler, String caller); +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsTagProviderAdapter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsTagProviderAdapter.java new file mode 100644 index 00000000..3ec1b9f1 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/MetricsTagProviderAdapter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics; + +import java.util.Collections; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpResponse; + +/** + * @author Jon Schneider + */ +public class MetricsTagProviderAdapter implements MetricsTagProvider { + @Override + public Map clientHttpRequestTags(HttpRequest request, + ClientHttpResponse response) { + return Collections.emptyMap(); + } + + @Override + public Map httpRequestTags(HttpServletRequest request, + HttpServletResponse response, Object handler, String caller) { + return Collections.emptyMap(); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/RestTemplateUrlTemplateCapturingAspect.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/RestTemplateUrlTemplateCapturingAspect.java new file mode 100644 index 00000000..7c044edf --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/RestTemplateUrlTemplateCapturingAspect.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +/** + * Captures the still-templated URI because currently the ClientHttpRequestInterceptor + * currently only gives us the means to retrieve the substituted URI. + * + * @author Jon Schneider + */ +@Aspect +public class RestTemplateUrlTemplateCapturingAspect { + @Around("execution(* org.springframework.web.client.RestTemplate.*(String, ..))") + void captureUrlTemplate(ProceedingJoinPoint joinPoint) throws Throwable { + try { + String urlTemplate = (String) joinPoint.getArgs()[0]; + RestTemplateUrlTemplateHolder.setRestTemplateUrlTemplate(urlTemplate); + joinPoint.proceed(); + } + finally { + RestTemplateUrlTemplateHolder.clear(); + } + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/RestTemplateUrlTemplateHolder.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/RestTemplateUrlTemplateHolder.java new file mode 100644 index 00000000..28bfea1d --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/RestTemplateUrlTemplateHolder.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics; + +import org.springframework.core.NamedThreadLocal; + +/** + * Holding area for the still-templated URI because currently the + * ClientHttpRequestInterceptor only gives us the means to retrieve the substituted URI. + * + * @author Jon Schneider + */ +public class RestTemplateUrlTemplateHolder { + private static final ThreadLocal restTemplateUrlTemplateHolder = new NamedThreadLocal( + "Rest Template URL Template"); + + public static String getRestTemplateUrlTemplate() { + return restTemplateUrlTemplateHolder.get(); + } + + public static void setRestTemplateUrlTemplate(String urlTemplate) { + restTemplateUrlTemplateHolder.set(urlTemplate); + } + + public static void clear() { + restTemplateUrlTemplateHolder.remove(); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasAutoConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasAutoConfiguration.java new file mode 100644 index 00000000..bd1edabc --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasAutoConfiguration.java @@ -0,0 +1,66 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.atlas; + +import java.util.Collection; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.metrics.export.Exporter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +import com.netflix.servo.publish.MetricPoller; +import com.netflix.servo.tag.BasicTagList; + +/** + * Configures the Atlas metrics backend, also configuring Spectator to collect metrics if necessary. + * + * @author Jon Schneider + */ +@Configuration +@ConditionalOnClass(AtlasMetricObserver.class) +public class AtlasAutoConfiguration { + @Autowired(required = false) + private Collection tagProviders; + + @Bean + public AtlasMetricObserverConfigBean atlasObserverConfig() { + return new AtlasMetricObserverConfigBean(); + } + + @Bean + @ConditionalOnMissingBean + public AtlasMetricObserver atlasObserver(AtlasMetricObserverConfigBean atlasObserverConfig, RestTemplate restTemplate) { + BasicTagList tags = (BasicTagList) BasicTagList.EMPTY; + if (tagProviders != null) { + for (AtlasTagProvider tagProvider : tagProviders) { + for (Map.Entry tag : tagProvider.defaultTags().entrySet()) { + if (tag.getValue() != null) + tags = tags.copy(tag.getKey(), tag.getValue()); + } + } + } + return new AtlasMetricObserver(atlasObserverConfig, restTemplate, tags); + } + + @Bean + @ConditionalOnMissingBean + public Exporter exporter(AtlasMetricObserver observer, MetricPoller poller) { + return new AtlasExporter(observer, poller); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasExporter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasExporter.java new file mode 100644 index 00000000..95b2dca5 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasExporter.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.atlas; + +import org.springframework.boot.actuate.metrics.export.Exporter; + +import com.netflix.servo.publish.BasicMetricFilter; +import com.netflix.servo.publish.MetricPoller; + +/** + * @author Jon Schneider + */ +public class AtlasExporter implements Exporter { + private AtlasMetricObserver observer; + private MetricPoller poller; + + public AtlasExporter(AtlasMetricObserver observer, MetricPoller poller) { + this.observer = observer; + this.poller = poller; + } + + @Override + public void export() { + observer.update(poller.poll(BasicMetricFilter.MATCH_ALL)); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasMetricObserver.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasMetricObserver.java new file mode 100644 index 00000000..e55be266 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasMetricObserver.java @@ -0,0 +1,236 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.atlas; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; +import com.netflix.servo.Metric; +import com.netflix.servo.annotations.DataSourceType; +import com.netflix.servo.publish.MetricObserver; +import com.netflix.servo.tag.BasicTag; +import com.netflix.servo.tag.Tag; +import com.netflix.servo.tag.TagList; + +/** + * Observer that forwards metrics to atlas. In addition to being a MetricObserver, it also + * supports a push model that sends metrics as soon as possible (asynchronously). + * + * @author Jon Schneider + */ +public class AtlasMetricObserver implements MetricObserver { + private static final Log logger = LogFactory.getLog(AtlasMetricObserver.class); + private static final SmileFactory smileFactory = new SmileFactory(); + private static final Tag atlasRateTag = new BasicTag("atlas.dstype", "rate"); + private static final Tag atlasCounterTag = new BasicTag("atlas.dstype", "counter"); + private static final Tag atlasGaugeTag = new BasicTag("atlas.dstype", "gauge"); + private static final Pattern validAtlasTag = Pattern.compile("[\\.\\-\\w]+"); + + private AtlasMetricObserverConfigBean config; + private RestTemplate restTemplate; + private TagList commonTags; + private String uri; + + public AtlasMetricObserver(AtlasMetricObserverConfigBean config, + RestTemplate restTemplate, TagList commonTags) { + this.config = config; + this.commonTags = commonTags; + this.restTemplate = restTemplate; + this.uri = normalizeAtlasUri(config.getUri()); + + if (!validTags(commonTags)) { + throw new IllegalArgumentException( + "One or more atlas tags contain invalid characters, must match [\\.\\-\\w]+"); + } + } + + @Override + public String getName() { + return "atlas"; + } + + protected static boolean validTags(TagList tags) { + for (Tag tag : tags) { + if (!validAtlasTag.matcher(tag.getKey()).matches()) { + logger.error("Invalid tag key " + tag.getKey()); + return false; + } + + if (!validAtlasTag.matcher(tag.getValue()).matches()) { + logger.error("Invalid tag value " + tag.getValue()); + return false; + } + } + + return true; + } + + protected static String normalizeAtlasUri(String uri) { + Matcher matcher = Pattern.compile("(.+?)(/api/v1/publish)?/?").matcher(uri); + matcher.matches(); + return matcher.group(1) + "/api/v1/publish"; + } + + @Override + public void update(List rawMetrics) { + if (!config.isEnabled()) { + logger.debug("Atlas metric observer disabled. Not sending metrics."); + return; + } + + if (rawMetrics.isEmpty()) { + logger.debug("Metrics list is empty, no data being sent to server."); + return; + } + + List metrics = addTypeTagsAsNecessary(rawMetrics); + + for (int i = 0; i < metrics.size(); i += config.getBatchSize()) { + List batch = metrics.subList(i, + Math.min(metrics.size(), config.getBatchSize() + i)); + logger.debug("Sending a metrics batch of size " + batch.size()); + sendMetricsBatch(batch); + } + } + + private void sendMetricsBatch(List metrics) { + try { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + JsonGenerator gen = smileFactory.createGenerator(output, JsonEncoding.UTF8); + + gen.writeStartObject(); + + writeCommonTags(gen); + if (writeMetrics(gen, metrics) == 0) + return; // short circuit this batch if no valid/numeric metrics existed + + gen.writeEndObject(); + gen.flush(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.valueOf("application/x-jackson-smile")); + HttpEntity entity = new HttpEntity<>(output.toByteArray(), headers); + try { + restTemplate.exchange(uri, HttpMethod.POST, entity, Map.class); + } + catch (HttpClientErrorException e) { + logger.error( + "Failed to write metrics to atlas: " + + e.getResponseBodyAsString(), e); + } + catch (RestClientException e) { + logger.error("Failed to write metrics to atlas", e); + } + } + catch (IOException e) { + // an IOException stemming from the generator writing to a + // ByteArrayOutputStream is impossible + throw new RuntimeException(e); + } + } + + private void writeCommonTags(JsonGenerator gen) throws IOException { + gen.writeObjectFieldStart("tags"); + for (Tag tag : commonTags) + gen.writeStringField(tag.getKey(), tag.getValue()); + gen.writeEndObject(); + } + + private int writeMetrics(JsonGenerator gen, List metrics) throws IOException { + int totalMetricsInBatch = 0; + gen.writeArrayFieldStart("metrics"); + + for (Metric m : metrics) { + if (!validTags(m.getConfig().getTags())) + continue; + + if (!Number.class.isAssignableFrom(m.getValue().getClass())) + continue; + + gen.writeStartObject(); + + gen.writeObjectFieldStart("tags"); + gen.writeStringField("name", m.getConfig().getName()); + for (Tag tag : m.getConfig().getTags()) + gen.writeStringField(tag.getKey(), tag.getValue()); + gen.writeEndObject(); + + gen.writeNumberField("start", m.getTimestamp()); + gen.writeNumberField("value", m.getNumberValue().doubleValue()); + + gen.writeEndObject(); + + totalMetricsInBatch++; + } + + gen.writeEndArray(); + return totalMetricsInBatch; + } + + protected static List addTypeTagsAsNecessary(List metrics) { + List typedMetrics = new ArrayList<>(); + for (Metric m : metrics) { + String value = m.getConfig().getTags().getValue(DataSourceType.KEY); + Metric transformed; + + // Atlas will not normalize metrics tagged with atlas.dstype=gauge. Since + // these metric types are + // pre-normalized, we do not want Atlas to touch the value + if (DataSourceType.GAUGE.name().equals(value) + || DataSourceType.RATE.name().equals(value) + || DataSourceType.NORMALIZED.name().equals(value)) { + transformed = new Metric(m.getConfig().withAdditionalTag(atlasGaugeTag), + m.getTimestamp(), m.getValue()); + } + + // atlas.dstype=counter means you're sending the absolute value of the counter + // (a monotonically + // increasing value), and Atlas will keep the previous value and convert it to + // a rate per second + // when the metric is received + else if (DataSourceType.COUNTER.name().equals(value)) { + transformed = new Metric( + m.getConfig().withAdditionalTag(atlasCounterTag), + m.getTimestamp(), m.getValue()); + } + + // Atlas will normalize the value to a minute boundary based on its timestamp + else { + transformed = new Metric(m.getConfig().withAdditionalTag(atlasRateTag), + m.getTimestamp(), m.getValue()); + } + + typedMetrics.add(transformed); + } + return typedMetrics; + } +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasMetricObserverConfigBean.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasMetricObserverConfigBean.java new file mode 100644 index 00000000..9c04eac4 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasMetricObserverConfigBean.java @@ -0,0 +1,50 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.atlas; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Jon Schneider + */ +@ConfigurationProperties("netflix.atlas") +public class AtlasMetricObserverConfigBean { + private String uri; + private boolean enabled = true; + private Integer batchSize = 10000; + + public boolean isEnabled() { + return enabled; + } + + public int getBatchSize() { + return batchSize; + } + + public String getUri() { + return uri; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public void setBatchSize(Integer batchSize) { + this.batchSize = batchSize; + } + + public void setUri(String uri) { + this.uri = uri; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasTagProvider.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasTagProvider.java new file mode 100644 index 00000000..e05e49c5 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/AtlasTagProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.atlas; + +import java.util.Map; + +/** + * Provide implementations of this interface in your application context to add a set of static tags to every metric + * sent to Atlas. + * + * @author Jon Schneider + */ +public interface AtlasTagProvider { + Map defaultTags(); +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/EnableAtlas.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/EnableAtlas.java new file mode 100644 index 00000000..3929d194 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/atlas/EnableAtlas.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.atlas; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * Annotation for clients to enable Atlas metrics publishing. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import(AtlasAutoConfiguration.class) +public @interface EnableAtlas { +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/DimensionalServoMetricNaming.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/DimensionalServoMetricNaming.java new file mode 100644 index 00000000..eba61e0f --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/DimensionalServoMetricNaming.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.servo; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang.StringUtils; + +import com.netflix.servo.monitor.Monitor; +import com.netflix.servo.monitor.MonitorConfig; +import com.netflix.servo.tag.Tag; + +/** + * @author Jon Schneider + */ +public class DimensionalServoMetricNaming implements ServoMetricNaming { + @Override + public String asHierarchicalName(Monitor monitor) { + MonitorConfig config = monitor.getConfig(); + List tags = new ArrayList<>(config.getTags().size()); + for (Tag t : config.getTags()) { + tags.add(t.getKey() + "=" + t.getValue()); + } + return config.getName() + "(" + StringUtils.join(tags, ",") + ")"; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/DefaultServoMetricNaming.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/HierarchicalServoMetricNaming.java similarity index 80% rename from spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/DefaultServoMetricNaming.java rename to spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/HierarchicalServoMetricNaming.java index 0df15966..2f8b4ab5 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/DefaultServoMetricNaming.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/HierarchicalServoMetricNaming.java @@ -1,23 +1,20 @@ /* * Copyright 2013-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. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. */ -package org.springframework.cloud.netflix.servo; +package org.springframework.cloud.netflix.metrics.servo; -import com.netflix.servo.Metric; import com.netflix.servo.annotations.DataSourceType; +import com.netflix.servo.monitor.Monitor; import com.netflix.servo.monitor.MonitorConfig; import com.netflix.servo.tag.Tag; import com.netflix.servo.tag.TagList; @@ -25,21 +22,20 @@ import com.netflix.servo.tag.TagList; /** * @author Spencer Gibb */ -public class DefaultServoMetricNaming implements ServoMetricNaming { +public class HierarchicalServoMetricNaming implements ServoMetricNaming { private static final String JMX_DOMAIN_KEY = "JmxDomain"; public static final String SERVO = "servo."; @Override - public String getName(Metric metric) { - MonitorConfig config = metric.getConfig(); + public String asHierarchicalName(Monitor monitor) { + MonitorConfig config = monitor.getConfig(); TagList tags = config.getTags(); Tag domainTag = tags.getTag(JMX_DOMAIN_KEY); String name; if (domainTag != null) { // jmx metric name = handleJmxMetric(config, tags); - } - else { + } else { name = handleMetric(config, tags); } return name.toLowerCase(); @@ -111,4 +107,4 @@ public class DefaultServoMetricNaming implements ServoMetricNaming { } return s.replace(" ", "_"); } -} +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/ServoMetricNaming.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricNaming.java similarity index 61% rename from spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/ServoMetricNaming.java rename to spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricNaming.java index 99c7d0e3..4fc240a1 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/ServoMetricNaming.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricNaming.java @@ -14,13 +14,19 @@ * limitations under the License. */ -package org.springframework.cloud.netflix.servo; +package org.springframework.cloud.netflix.metrics.servo; -import com.netflix.servo.Metric; +import com.netflix.servo.monitor.Monitor; /** * @author Spencer Gibb */ public interface ServoMetricNaming { - String getName(Metric metric); -} + /** + * @param monitor a monitor representing a single statistic (not a CompositeMonitor) + * @return a hierarchical name representing a single statistic for a servo monitor; + * note that this method will be called once for each statistic on a composite servo + * Monitor like a Timer. + */ + String asHierarchicalName(Monitor monitor); +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricReader.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricReader.java new file mode 100644 index 00000000..d1acebd7 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricReader.java @@ -0,0 +1,87 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.servo; + +import java.util.ArrayList; +import java.util.Collection; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.reader.MetricReader; + +import com.netflix.servo.MonitorRegistry; +import com.netflix.servo.monitor.CompositeMonitor; +import com.netflix.servo.monitor.Monitor; + +/** + * @author Jon Schneider + */ +public class ServoMetricReader implements MetricReader { + MonitorRegistry monitorRegistry; + ServoMetricNaming metricNaming; + + public ServoMetricReader(MonitorRegistry monitorRegistry, + ServoMetricNaming metricNaming) { + this.monitorRegistry = monitorRegistry; + this.metricNaming = metricNaming; + } + + @Override + public Metric findOne(String s) { + throw new UnsupportedOperationException( + "cannot construct a tag-based Servo id from a hierarchical name"); + } + + @Override + public Iterable> findAll() { + Collection> metrics = new ArrayList<>(); + for (Monitor monitor : monitorRegistry.getRegisteredMonitors()) { + addToMetrics(monitor, metrics); + } + return metrics; + } + + private void addToMetrics(Monitor monitor, Collection> metrics) { + if (monitor instanceof CompositeMonitor) { + for (Monitor nestedMonitor : ((CompositeMonitor) monitor).getMonitors()) { + addToMetrics(nestedMonitor, metrics); + } + } + else if (monitor.getValue() instanceof Number) { + // Servo does support non-numeric values, but there is no such concept in + // Spring Boot + metrics.add(new Metric<>(metricNaming.asHierarchicalName(monitor), + (Number) monitor.getValue())); + } + } + + @Override + public long count() { + long count = 0; + for (Monitor monitor : monitorRegistry.getRegisteredMonitors()) { + count += countMetrics(monitor); + } + return count; + } + + private static long countMetrics(Monitor monitor) { + if (monitor instanceof CompositeMonitor) { + long count = 0; + for (Monitor nestedMonitor : ((CompositeMonitor) monitor).getMonitors()) { + count += countMetrics(nestedMonitor); + } + return count; + } + return 1; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricServices.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricServices.java new file mode 100644 index 00000000..2a163c10 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricServices.java @@ -0,0 +1,145 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.servo; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.GaugeService; + +import com.netflix.servo.MonitorRegistry; +import com.netflix.servo.monitor.BasicCounter; +import com.netflix.servo.monitor.BasicDistributionSummary; +import com.netflix.servo.monitor.BasicTimer; +import com.netflix.servo.monitor.DoubleGauge; +import com.netflix.servo.monitor.LongGauge; +import com.netflix.servo.monitor.MonitorConfig; + +/** + * Provides a CounterService and GaugeService implementation + * backed by Servo. + * + * @author Jon Schneider + */ +public class ServoMetricServices implements CounterService, GaugeService { + private final MonitorRegistry registry; + + private final ConcurrentMap counters = new ConcurrentHashMap<>(); + private final ConcurrentMap longGauges = new ConcurrentHashMap<>(); + private final ConcurrentMap doubleGauges = new ConcurrentHashMap<>(); + private final ConcurrentMap distributionSummaries = new ConcurrentHashMap<>(); + private final ConcurrentMap timers = new ConcurrentHashMap<>(); + + public ServoMetricServices(MonitorRegistry registry) { + this.registry = registry; + } + + protected static String stripMetricName(String metricName) { + return metricName.replaceFirst("^(timer|histogram|meter)\\.", ""); + } + + @Override + public void increment(String name) { + incrementInternal(name, 1L); + } + + @Override + public void decrement(String name) { + incrementInternal(name, -1L); + } + + private void incrementInternal(String name, long value) { + String strippedName = stripMetricName(name); + + if (name.startsWith("status.")) { + // drop this metric since we are capturing it already with + // ServoHandlerInterceptor, + // and we are able to glean more information like exceptionType from that + // mechanism than what + // boot provides us + } + else if (name.startsWith("meter.")) { + BasicCounter counter = counters.get(strippedName); + if (counter == null) { + counter = new BasicCounter(MonitorConfig.builder(strippedName).build()); + counters.put(strippedName, counter); + registry.register(counter); + } + counter.increment(value); + } + else { + LongGauge gauge = longGauges.get(strippedName); + if (gauge == null) { + gauge = new LongGauge(MonitorConfig.builder(strippedName).build()); + longGauges.put(strippedName, gauge); + registry.register(gauge); + } + gauge.set(value); + } + } + + @Override + public void reset(String name) { + String strippedName = stripMetricName(name); + BasicCounter counter = counters.remove(strippedName); + if (counter != null) + registry.unregister(counter); + + LongGauge gauge = longGauges.remove(strippedName); + if (gauge != null) + registry.unregister(gauge); + + BasicDistributionSummary distributionSummary = distributionSummaries + .remove(strippedName); + if (distributionSummary != null) + registry.unregister(distributionSummary); + } + + @Override + public void submit(String name, double dValue) { + long value = ((Double) dValue).longValue(); + String strippedName = stripMetricName(name); + if (name.startsWith("histogram.")) { + BasicDistributionSummary distributionSummary = distributionSummaries + .get(strippedName); + if (distributionSummary == null) { + distributionSummary = new BasicDistributionSummary(MonitorConfig.builder( + strippedName).build()); + distributionSummaries.put(strippedName, distributionSummary); + registry.register(distributionSummary); + } + distributionSummary.record(value); + } + else if (name.startsWith("timer.")) { + BasicTimer timer = timers.get(strippedName); + if (timer == null) { + timer = new BasicTimer(MonitorConfig.builder(strippedName).build()); + timers.put(strippedName, timer); + registry.register(timer); + } + timer.record(value, TimeUnit.MILLISECONDS); + } + else { + DoubleGauge gauge = doubleGauges.get(strippedName); + if (gauge == null) { + gauge = new DoubleGauge(MonitorConfig.builder(strippedName).build()); + doubleGauges.put(strippedName, gauge); + registry.register(gauge); + } + gauge.set(dValue); + } + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricsAutoConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricsAutoConfiguration.java new file mode 100644 index 00000000..6bd709f0 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricsAutoConfiguration.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013-2014 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +package org.springframework.cloud.netflix.metrics.servo; + +import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; +import org.springframework.boot.actuate.endpoint.MetricReaderPublicMetrics; +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.GaugeService; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.cloud.netflix.metrics.DefaultMetricsTagProvider; +import org.springframework.cloud.netflix.metrics.MetricsInterceptorConfiguration; +import org.springframework.cloud.netflix.metrics.MetricsTagProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import com.netflix.servo.DefaultMonitorRegistry; +import com.netflix.servo.MonitorRegistry; +import com.netflix.servo.monitor.Monitors; + +/** + * Auto configuration to configure Servo support. + * + * @author Dave Syer + * @author Christian Dupuis + * @author Jon Schneider + */ +@Configuration +@ConditionalOnClass({ Monitors.class, MetricReader.class }) +@ConditionalOnMissingClass("com.netflix.spectator.api.Registry") +@AutoConfigureBefore(EndpointAutoConfiguration.class) +@Import(MetricsInterceptorConfiguration.class) +public class ServoMetricsAutoConfiguration { + @Bean + @ConditionalOnMissingBean + public ServoMetricsConfigBean servoMetricsConfig() { + return new ServoMetricsConfigBean(); + } + + @Bean + @ConditionalOnMissingBean + public ServoMetricNaming servoMetricNaming() { + return new HierarchicalServoMetricNaming(); + } + + @Bean + @ConditionalOnMissingBean + public MonitorRegistry monitorRegistry() { + System.setProperty(DefaultMonitorRegistry.class.getCanonicalName() + ".registryClass", servoMetricsConfig() + .getRegistryClass()); + return DefaultMonitorRegistry.getInstance(); + } + + @Bean + public MetricReaderPublicMetrics servoPublicMetrics(MonitorRegistry monitorRegistry, ServoMetricNaming servoMetricNaming) { + ServoMetricReader reader = new ServoMetricReader(monitorRegistry, servoMetricNaming); + return new MetricReaderPublicMetrics(reader); + } + + @Bean + @ConditionalOnMissingBean({ ServoMetricServices.class, CounterService.class, GaugeService.class }) + public ServoMetricServices spectatorMetricServices(MonitorRegistry monitorRegistry) { + return new ServoMetricServices(monitorRegistry); + } + + @Bean + public MetricsTagProvider defaultMetricsTagProvider() { + return new DefaultMetricsTagProvider(); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricsConfigBean.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricsConfigBean.java new file mode 100644 index 00000000..f5eac395 --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricsConfigBean.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.servo; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties to configure Servo support. + * + * @author Jon Schneider + */ +@ConfigurationProperties("netflix.metrics.servo") +public class ServoMetricsConfigBean { + String registryClass = "com.netflix.servo.BasicMonitorRegistry"; + + public String getRegistryClass() { + return registryClass; + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMonitorCache.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMonitorCache.java new file mode 100644 index 00000000..ca91e01e --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/metrics/servo/ServoMonitorCache.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.servo; + +import java.util.HashMap; +import java.util.Map; + +import com.netflix.servo.DefaultMonitorRegistry; +import com.netflix.servo.MonitorRegistry; +import com.netflix.servo.monitor.BasicTimer; +import com.netflix.servo.monitor.Monitor; +import com.netflix.servo.monitor.MonitorConfig; + +/** + * Servo does not provide a mechanism to retrieve an existing monitor by name + tags. + * + * @author Jon Schneider + */ +public class ServoMonitorCache { + private static final Map timerCache = new HashMap<>(); + + /** + * @param config contains the name and tags that uniquely identify a timer + * @return an already registered timer if it exists, otherwise create/register one and + * return it. + */ + public synchronized static BasicTimer getTimer(MonitorConfig config) { + BasicTimer t = timerCache.get(config); + if (t != null) + return t; + + t = new BasicTimer(config); + timerCache.put(config, t); + DefaultMonitorRegistry.getInstance().register(t); + return t; + } + + /** + * Useful for tests to clear the monitor registry between runs + */ + public static void unregisterAll() { + MonitorRegistry registry = DefaultMonitorRegistry.getInstance(); + for (Monitor monitor : registry.getRegisteredMonitors()) { + registry.unregister(monitor); + } + timerCache.clear(); + } +} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/ServoMetricCollector.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/ServoMetricCollector.java deleted file mode 100644 index 9234edd8..00000000 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/ServoMetricCollector.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2013-2014 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.netflix.servo; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; - -import lombok.extern.apachecommons.CommonsLog; - -import org.springframework.beans.factory.DisposableBean; -import org.springframework.boot.actuate.metrics.reader.MetricReader; -import org.springframework.boot.actuate.metrics.writer.MetricWriter; - -import com.netflix.servo.publish.BasicMetricFilter; -import com.netflix.servo.publish.MetricObserver; -import com.netflix.servo.publish.MonitorRegistryMetricPoller; -import com.netflix.servo.publish.PollRunnable; -import com.netflix.servo.publish.PollScheduler; - -/** - * {@link MetricReader} implementation that registers a {@link MetricObserver} with the - * Netflix Servo library and exposes Servo metrics to the /metric endpoint. - * - * @author Dave Syer - * @author Christian Dupuis - */ -@CommonsLog -public class ServoMetricCollector implements DisposableBean { - - public ServoMetricCollector(MetricWriter metrics, ServoMetricNaming naming) { - List observers = new ArrayList<>(); - observers.add(new ServoMetricObserver(metrics, naming)); - PollRunnable task = new PollRunnable(new MonitorRegistryMetricPoller(), - BasicMetricFilter.MATCH_ALL, true, observers); - - if (!PollScheduler.getInstance().isStarted()) { - try { - PollScheduler.getInstance().start(); - } - catch (Exception e) { - // Can fail due to race condition with evil singletons (if more than one - // app in same JVM) - log.error("Could not start servo metrics poller", e); - return; - } - } - // TODO Make poll interval configurable - PollScheduler.getInstance().addPoller(task, 5, TimeUnit.SECONDS); - } - - @Override - public void destroy() throws Exception { - log.info("Stopping Servo PollScheduler"); - if (PollScheduler.getInstance().isStarted()) { - try { - PollScheduler.getInstance().stop(); - } - catch (Exception e) { - log.error("Could not stop servo metrics poller", e); - } - } - } - -} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/ServoMetricObserver.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/ServoMetricObserver.java deleted file mode 100644 index edb5dad1..00000000 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/ServoMetricObserver.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2013-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 org.springframework.cloud.netflix.servo; - -import com.netflix.servo.Metric; -import com.netflix.servo.publish.BaseMetricObserver; -import org.springframework.boot.actuate.metrics.writer.MetricWriter; - -import java.util.Date; -import java.util.List; - -/** - * {@link com.netflix.servo.publish.MetricObserver} to convert Servo metrics into Spring Boot {@link org.springframework.boot.actuate.metrics.Metric} - * instances. - */ -final class ServoMetricObserver extends BaseMetricObserver { - - private final MetricWriter metrics; - private final ServoMetricNaming naming; - - public ServoMetricObserver(MetricWriter metrics, ServoMetricNaming naming) { - super("spring-boot"); - this.metrics = metrics; - this.naming = naming; - } - - @Override - public void updateImpl(List servoMetrics) { - for (Metric servoMetric : servoMetrics) { - String key = naming.getName(servoMetric); - - if (servoMetric.hasNumberValue()) { - this.metrics.set(new org.springframework.boot.actuate.metrics.Metric<>(key, - servoMetric.getNumberValue(), new Date(servoMetric - .getTimestamp()))); - } - } - } - -} diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/ServoMetricsAutoConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/ServoMetricsAutoConfiguration.java deleted file mode 100644 index e60ac1af..00000000 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/servo/ServoMetricsAutoConfiguration.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2013-2014 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.netflix.servo; - -import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; -import org.springframework.boot.actuate.autoconfigure.MetricRepositoryAutoConfiguration; -import org.springframework.boot.actuate.metrics.GaugeService; -import org.springframework.boot.actuate.metrics.Metric; -import org.springframework.boot.actuate.metrics.reader.MetricReader; -import org.springframework.boot.actuate.metrics.writer.Delta; -import org.springframework.boot.actuate.metrics.writer.MetricWriter; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import com.netflix.servo.monitor.Monitors; - -/** - * Auto configuration to configure Servo support. - * - * @author Dave Syer - * @author Christian Dupuis - */ -@Configuration -@ConditionalOnClass({ Monitors.class, MetricReader.class }) -@ConditionalOnBean(GaugeService.class) -@AutoConfigureBefore(EndpointAutoConfiguration.class) -@AutoConfigureAfter({ MetricRepositoryAutoConfiguration.class }) -public class ServoMetricsAutoConfiguration { - - @Bean - @ConditionalOnMissingBean - public ServoMetricNaming servoMetricNaming() { - return new DefaultServoMetricNaming(); - } - - @Bean - @ConditionalOnMissingBean - public ServoMetricCollector servoMetricCollector(GaugeService gauges, - ServoMetricNaming naming) { - return new ServoMetricCollector(new ActuatorMetricWriter(gauges), naming); - } - - // TODO: refactor this when Spring Boot 1.3 is mandatory - private static class ActuatorMetricWriter implements MetricWriter { - - private GaugeService gauges; - - public ActuatorMetricWriter(GaugeService gauges) { - this.gauges = gauges; - } - - @Override - public void increment(Delta delta) { - } - - @Override - public void set(Metric value) { - gauges.submit(value.getName(), value.getValue().doubleValue()); - } - - @Override - public void reset(String metricName) { - gauges.submit(metricName, 0.); - } - - } - -} diff --git a/spring-cloud-netflix-core/src/main/resources/META-INF/spring.factories b/spring-cloud-netflix-core/src/main/resources/META-INF/spring.factories index c1dc0641..afffba57 100644 --- a/spring-cloud-netflix-core/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-netflix-core/src/main/resources/META-INF/spring.factories @@ -11,7 +11,7 @@ org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\ org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration,\ org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\ org.springframework.cloud.netflix.rx.RxJavaAutoConfiguration,\ -org.springframework.cloud.netflix.servo.ServoMetricsAutoConfiguration +org.springframework.cloud.netflix.metrics.servo.ServoMetricsAutoConfiguration org.springframework.cloud.bootstrap.BootstrapConfiguration=\ org.springframework.cloud.netflix.config.DiscoveryClientConfigServiceBootstrapConfiguration @@ -20,4 +20,4 @@ org.springframework.cloud.client.discovery.EnableDiscoveryClient=\ org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker=\ -org.springframework.cloud.netflix.hystrix.HystrixCircuitBreakerConfiguration \ No newline at end of file +org.springframework.cloud.netflix.hystrix.HystrixCircuitBreakerConfiguration diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/AbstractMetricsTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/AbstractMetricsTests.java new file mode 100644 index 00000000..f9c9d9d8 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/AbstractMetricsTests.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics; + +import org.junit.Before; +import org.springframework.cloud.netflix.metrics.servo.ServoMonitorCache; + +/** + * @author Jon Schneider + */ +public class AbstractMetricsTests { + @Before + public void setup() { + ServoMonitorCache.unregisterAll(); + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/MetricsClientHttpRequestInterceptorTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/MetricsClientHttpRequestInterceptorTests.java new file mode 100644 index 00000000..e0b85649 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/MetricsClientHttpRequestInterceptorTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration; +import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration; +import org.springframework.cloud.netflix.metrics.servo.ServoMetricsAutoConfiguration; +import org.springframework.cloud.netflix.metrics.servo.ServoMonitorCache; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.client.match.MockRestRequestMatchers; +import org.springframework.test.web.client.response.MockRestResponseCreators; +import org.springframework.web.client.RestTemplate; + +import com.netflix.servo.MonitorRegistry; +import com.netflix.servo.monitor.BasicTimer; +import com.netflix.servo.monitor.MonitorConfig; + +/** + * @author Jon Schneider + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = { MetricsRestTemplateRestTemplateConfig.class, + MetricsRestTemplateTestConfig.class }) +@TestPropertySource(properties = { "netflix.metrics.restClient.metricName=metricName", + "spring.aop.proxy-target-class=true" }) +public class MetricsClientHttpRequestInterceptorTests extends AbstractMetricsTests { + @Autowired + MonitorRegistry registry; + + @Autowired + RestTemplate restTemplate; + + @Test + public void metricsGatheredWhenSuccessful() { + MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate); + mockServer.expect(MockRestRequestMatchers.requestTo("/test/123")) + .andExpect(MockRestRequestMatchers.method(HttpMethod.GET)) + .andRespond(MockRestResponseCreators.withSuccess("{\"status\" : \"OK\"}", MediaType.APPLICATION_JSON)); + restTemplate.getForObject("/test/{id}", String.class, 123); + + MonitorConfig.Builder builder = new MonitorConfig.Builder("metricName") + .withTag("method", "GET") + .withTag("uri", "_test_-id-") + .withTag("status", "200") + .withTag("clientName", "none"); + + BasicTimer timer = ServoMonitorCache.getTimer(builder.build()); + + Assert.assertEquals(1L, (long) timer.getCount()); + mockServer.verify(); + } +} + +@Configuration +@ImportAutoConfiguration({ ServoMetricsAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, AopAutoConfiguration.class }) +class MetricsRestTemplateTestConfig { +} + +@Configuration +class MetricsRestTemplateRestTemplateConfig { + @Bean + RestTemplate restTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/MetricsHandlerInterceptorIntegrationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/MetricsHandlerInterceptorIntegrationTests.java new file mode 100644 index 00000000..b055fe40 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/MetricsHandlerInterceptorIntegrationTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration; +import org.springframework.cloud.netflix.metrics.servo.ServoMetricsAutoConfiguration; +import org.springframework.cloud.netflix.metrics.servo.ServoMonitorCache; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.netflix.servo.MonitorRegistry; +import com.netflix.servo.monitor.BasicTimer; +import com.netflix.servo.monitor.MonitorConfig; + +import static org.junit.Assert.assertFalse; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Jon Schneider + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = MetricsTestConfig.class) +@WebAppConfiguration +@TestPropertySource(properties = "netflix.metrics.rest.metricName=metricName") +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class MetricsHandlerInterceptorIntegrationTests extends AbstractMetricsTests { + @Autowired + WebApplicationContext webAppContext; + + @Autowired + MonitorRegistry registry; + + MockMvc mvc; + + @Test + public void autoConfigurationWiresTheMetricsInterceptor() { + assertFalse(webAppContext.getBeansOfType(MetricsHandlerInterceptor.class) + .isEmpty()); + } + + @Before + public void setup() { + mvc = MockMvcBuilders.webAppContextSetup(webAppContext).build(); + } + + @Test + public void metricsGatheredWhenSuccess() throws Exception { + mvc.perform(get("/test/some/request/10")).andExpect(status().isOk()); + assertTimer("test_some_request_-id-", null, 200); + } + + @Test + public void metricsGatheredWhenClientRequestBad() throws Exception { + mvc.perform(get("/test/some/request/oops")) + .andExpect(status().is4xxClientError()); + assertTimer("test_some_request_-id-", null, 400); + } + + @Test + public void metricsGatheredWhenUnhandledError() throws Exception { + try { + mvc.perform(get("/test/some/unhandledError/10")).andExpect(status().isOk()); + } + catch (Exception e) { + } + assertTimer("test_some_unhandledError_-id-", "RuntimeException", 200); + } + + @Test + public void metricsGatheredWhenHandledError() throws Exception { + mvc.perform(get("/test/some/error/10")).andExpect(status().is4xxClientError()); + assertTimer("test_some_error_-id-", null, 422); + } + + protected void assertTimer(String uriTag, String exceptionType, Integer status) { + MonitorConfig.Builder builder = new MonitorConfig.Builder("metricName") + .withTag("method", "GET").withTag("uri", uriTag) + .withTag("status", status.toString()); + + if (exceptionType != null) + builder = builder.withTag("exception", exceptionType); + + BasicTimer timer = ServoMonitorCache.getTimer(builder.build()); + Assert.assertEquals(1L, (long) timer.getCount()); + } +} + +@Configuration +@EnableWebMvc +@ImportAutoConfiguration({ ServoMetricsAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) +class MetricsTestConfig { + @Bean + MetricsTestController testController() { + return new MetricsTestController(); + } +} + +@RestController +@RequestMapping("/test/some") +@ControllerAdvice +class MetricsTestController { + @RequestMapping("/request/{id}") + public String testSomeRequest(@PathVariable Long id) { + return id.toString(); + } + + @RequestMapping("/error/{id}") + public String testSomeHandledError(@PathVariable Long id) { + throw new IllegalStateException("Boom on $id!"); + } + + @RequestMapping("/unhandledError/{id}") + public String testSomeUnhandledError(@PathVariable Long id) { + throw new RuntimeException("Boom on $id!"); + } + + @ExceptionHandler(value = IllegalStateException.class) + @ResponseStatus(code = HttpStatus.UNPROCESSABLE_ENTITY) + ModelAndView defaultErrorHandler(HttpServletRequest request, Exception e) { + return new ModelAndView("error"); + } +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/atlas/AtlasMetricObserverTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/atlas/AtlasMetricObserverTests.java new file mode 100644 index 00000000..68bde083 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/atlas/AtlasMetricObserverTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.atlas; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.client.match.MockRestRequestMatchers; +import org.springframework.test.web.client.response.MockRestResponseCreators; +import org.springframework.web.client.RestTemplate; + +import com.netflix.servo.Metric; +import com.netflix.servo.annotations.DataSourceType; +import com.netflix.servo.monitor.MonitorConfig; +import com.netflix.servo.tag.BasicTagList; + +import static com.netflix.servo.annotations.DataSourceType.COUNTER; +import static com.netflix.servo.annotations.DataSourceType.GAUGE; +import static com.netflix.servo.annotations.DataSourceType.INFORMATIONAL; +import static com.netflix.servo.annotations.DataSourceType.KEY; +import static com.netflix.servo.annotations.DataSourceType.NORMALIZED; +import static com.netflix.servo.annotations.DataSourceType.RATE; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * @author Jon Schneider + */ +public class AtlasMetricObserverTests { + @Test + public void normalizeAtlasUri() { + String normalized = "http://localhost:7001/api/v1/publish"; + assertEquals(normalized, AtlasMetricObserver.normalizeAtlasUri("http://localhost:7001")); + assertEquals(normalized, AtlasMetricObserver.normalizeAtlasUri("http://localhost:7001/")); + assertEquals(normalized, AtlasMetricObserver.normalizeAtlasUri("http://localhost:7001/api/v1/publish")); + assertEquals(normalized, AtlasMetricObserver.normalizeAtlasUri("http://localhost:7001/api/v1/publish/")); + } + + @Test + public void checkValidityOfTags() { + assertTrue(AtlasMetricObserver.validTags(BasicTagList.of("foo", "bar"))); + assertFalse(AtlasMetricObserver.validTags(BasicTagList.of("{foo}", "bar"))); + assertFalse(AtlasMetricObserver.validTags(BasicTagList.of("foo", "{bar}"))); + } + + @Test + public void assignTypesToMetrics() { + assertHasAtlasType("counter", metricWithType("foo", COUNTER)); + + assertHasAtlasType("gauge", metricWithType("foo", GAUGE)); + assertHasAtlasType("gauge", metricWithType("foo", NORMALIZED)); + assertHasAtlasType("gauge", metricWithType("foo", RATE)); + + assertHasAtlasType("rate", metricWithType("foo", INFORMATIONAL)); + assertHasAtlasType("rate", new Metric(new MonitorConfig.Builder("foo").build(), + 0, "bar")); + + // already has type + Metric m = new Metric(new MonitorConfig.Builder("foo") + .withTag(KEY, COUNTER.name()).withTag("atlas.dstype", "counter").build(), + 0, "bar"); + assertHasAtlasType("counter", m); + assertEquals(2, m.getConfig().getTags().size()); + } + + private void assertHasAtlasType(String atlasType, Metric m) { + assertEquals(atlasType, + AtlasMetricObserver.addTypeTagsAsNecessary(Collections.singletonList(m)) + .get(0).getConfig().getTags().getValue("atlas.dstype")); + } + + private Metric metricWithType(String key, DataSourceType type) { + return new Metric(new MonitorConfig.Builder(key).withTag(KEY, type.name()) + .build(), 0, 1); + } + + @Test + public void metricsSentInBatches() { + RestTemplate restTemplate = new RestTemplate(); + + AtlasMetricObserverConfigBean config = new AtlasMetricObserverConfigBean(); + config.setBatchSize(2); + config.setUri("atlas"); + + AtlasMetricObserver obs = new AtlasMetricObserver(config, restTemplate, + BasicTagList.EMPTY); + + // batch size is divisible by metric size + MockRestServiceServer mockServer = MockRestServiceServer + .createServer(restTemplate); + expectTotalBatches(mockServer, 2); + obs.update(generateMetrics(4)); + mockServer.verify(); + + // batch size is not divisible by metric size + mockServer = MockRestServiceServer.createServer(restTemplate); + expectTotalBatches(mockServer, 3); + obs.update(generateMetrics(5)); + mockServer.verify(); + + // metric size is less than batch size + mockServer = MockRestServiceServer.createServer(restTemplate); + expectTotalBatches(mockServer, 1); + obs.update(generateMetrics(1)); + mockServer.verify(); + + // no metrics to send + mockServer = MockRestServiceServer.createServer(restTemplate); + expectTotalBatches(mockServer, 0); + obs.update(Collections. emptyList()); + mockServer.verify(); + + // a single non-numeric metric does not result in a post + mockServer = MockRestServiceServer.createServer(restTemplate); + expectTotalBatches(mockServer, 0); + obs.update(Collections.singletonList(new Metric(new MonitorConfig.Builder("foo") + .build(), 0, "nonumber"))); + mockServer.verify(); + } + + private List generateMetrics(int numberOfMetrics) { + List metrics = new ArrayList<>(); + for (int i = 0; i < numberOfMetrics; i++) + metrics.add(metricWithType("foo" + i, DataSourceType.GAUGE)); + return metrics; + } + + private void expectTotalBatches(MockRestServiceServer mockServer, + int totalBatchesExpected) { + for (int i = 0; i < totalBatchesExpected; i++) { + mockServer + .expect(MockRestRequestMatchers.requestTo("atlas/api/v1/publish")) + .andExpect(MockRestRequestMatchers.method(HttpMethod.POST)) + .andRespond( + MockRestResponseCreators.withSuccess("{\"status\" : \"OK\"}", + MediaType.APPLICATION_JSON)); + } + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/servo/DimensionalServoMetricNamingTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/servo/DimensionalServoMetricNamingTests.java new file mode 100644 index 00000000..8372afce --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/servo/DimensionalServoMetricNamingTests.java @@ -0,0 +1,78 @@ +package org.springframework.cloud.netflix.metrics.servo; + +import org.junit.Test; + +import com.netflix.servo.annotations.DataSourceType; +import com.netflix.servo.monitor.AbstractMonitor; +import com.netflix.servo.monitor.MonitorConfig; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +public class DimensionalServoMetricNamingTests { + private DimensionalServoMetricNaming naming = new DimensionalServoMetricNaming(); + + @Test + public void vanillaServoMetricWorks() { + MonitorConfig config = MonitorConfig.builder("testMetric").build(); + String name = naming.asHierarchicalName(new FixedValueMonitor<>(config, 0)); + assertThat(name, is(equalTo("testMetric()"))); + } + + @Test + public void nameWithPeriodWorks() { + MonitorConfig config = MonitorConfig.builder("test.Metric").build(); + String name = naming.asHierarchicalName(new FixedValueMonitor<>(config, 0)); + assertThat(name, is(equalTo("test.Metric()"))); + } + + @Test + public void typeTagWorks() { + MonitorConfig config = MonitorConfig.builder("testMetric") + .withTag(DataSourceType.KEY, DataSourceType.COUNTER.getValue()).build(); + + String name = naming.asHierarchicalName(new FixedValueMonitor<>(config, 0)); + assertThat(name, is(equalTo("testMetric(type=COUNTER)"))); + } + + @Test + public void instanceTagWorks() { + MonitorConfig config = MonitorConfig.builder("testMetric") + .withTag("instance", "instance0").build(); + String name = naming.asHierarchicalName(new FixedValueMonitor<>(config, 0)); + assertThat(name, is(equalTo("testMetric(instance=instance0)"))); + } + + @Test + public void statisticTagWorks() { + MonitorConfig config = MonitorConfig.builder("testMetric") + .withTag("statistic", "min").build(); + String name = naming.asHierarchicalName(new FixedValueMonitor<>(config, 0)); + assertThat(name, is(equalTo("testMetric(statistic=min)"))); + } + + @Test + public void allTagsWork() { + MonitorConfig config = MonitorConfig.builder("testMetric") + .withTag(DataSourceType.KEY, DataSourceType.COUNTER.getValue()) + .withTag("instance", "instance0").withTag("statistic", "min").build(); + String name = naming.asHierarchicalName(new FixedValueMonitor<>(config, 0)); + assertThat(name, + is(equalTo("testMetric(instance=instance0,statistic=min,type=COUNTER)"))); + } + + private class FixedValueMonitor extends AbstractMonitor { + T value; + + protected FixedValueMonitor(MonitorConfig config, T value) { + super(config); + this.value = value; + } + + @Override + public T getValue(int pollerIndex) { + return value; + } + } +} diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/servo/DefaultServerMetricNamingTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/servo/HierarchicalServoMetricNamingTests.java similarity index 55% rename from spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/servo/DefaultServerMetricNamingTests.java rename to spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/servo/HierarchicalServoMetricNamingTests.java index 616e5ee4..9052a216 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/servo/DefaultServerMetricNamingTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/servo/HierarchicalServoMetricNamingTests.java @@ -14,62 +14,61 @@ * limitations under the License. */ -package org.springframework.cloud.netflix.servo; +package org.springframework.cloud.netflix.metrics.servo; -import static org.junit.Assert.*; -import static org.hamcrest.Matchers.*; +import org.junit.Test; import com.netflix.servo.annotations.DataSourceType; +import com.netflix.servo.monitor.AbstractMonitor; import com.netflix.servo.monitor.MonitorConfig; -import org.junit.Test; -import com.netflix.servo.Metric; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; /** * @author Spencer Gibb */ -public class DefaultServerMetricNamingTests { +public class HierarchicalServoMetricNamingTests { - private DefaultServoMetricNaming naming = new DefaultServoMetricNaming(); + private HierarchicalServoMetricNaming naming = new HierarchicalServoMetricNaming(); @Test public void vanillaServoMetricWorks() { - MonitorConfig config = MonitorConfig.builder("testMetric") .build(); - String name = naming.getName(new Metric(config, System.currentTimeMillis(), 0)); + MonitorConfig config = MonitorConfig.builder("testMetric").build(); + String name = naming.asHierarchicalName(new FixedValueMonitor<>(config, 0)); assertThat(name, is(equalTo("servo.testmetric"))); } @Test public void nameWithPeriodWorks() { - MonitorConfig config = MonitorConfig.builder("test.Metric") .build(); - String name = naming.getName(new Metric(config, System.currentTimeMillis(), 0)); + MonitorConfig config = MonitorConfig.builder("test.Metric").build(); + String name = naming.asHierarchicalName(new FixedValueMonitor<>(config, 0)); assertThat(name, is(equalTo("servo.test.metric"))); } @Test public void typeTagWorks() { MonitorConfig config = MonitorConfig.builder("testMetric") - .withTag(DataSourceType.KEY, DataSourceType.COUNTER.getValue()) - .build(); - String name = naming.getName(new Metric(config, System.currentTimeMillis(), 0)); + .withTag(DataSourceType.KEY, DataSourceType.COUNTER.getValue()).build(); + + String name = naming.asHierarchicalName(new FixedValueMonitor<>(config, 0)); assertThat(name, is(equalTo("counter.servo.testmetric"))); } @Test public void instanceTagWorks() { MonitorConfig config = MonitorConfig.builder("testMetric") - .withTag("instance", "instance0") - .build(); - String name = naming.getName(new Metric(config, System.currentTimeMillis(), 0)); + .withTag("instance", "instance0").build(); + String name = naming.asHierarchicalName(new FixedValueMonitor<>(config, 0)); assertThat(name, is(equalTo("servo.instance0.testmetric"))); } @Test public void statisticTagWorks() { MonitorConfig config = MonitorConfig.builder("testMetric") - .withTag("statistic", "min") - .build(); - String name = naming.getName(new Metric(config, System.currentTimeMillis(), 0)); + .withTag("statistic", "min").build(); + String name = naming.asHierarchicalName(new FixedValueMonitor<>(config, 0)); assertThat(name, is(equalTo("servo.testmetric.min"))); } @@ -77,10 +76,22 @@ public class DefaultServerMetricNamingTests { public void allTagsWork() { MonitorConfig config = MonitorConfig.builder("testMetric") .withTag(DataSourceType.KEY, DataSourceType.COUNTER.getValue()) - .withTag("instance", "instance0") - .withTag("statistic", "min") - .build(); - String name = naming.getName(new Metric(config, System.currentTimeMillis(), 0)); + .withTag("instance", "instance0").withTag("statistic", "min").build(); + String name = naming.asHierarchicalName(new FixedValueMonitor<>(config, 0)); assertThat(name, is(equalTo("counter.servo.instance0.testmetric.min"))); } -} + + private class FixedValueMonitor extends AbstractMonitor { + T value; + + protected FixedValueMonitor(MonitorConfig config, T value) { + super(config); + this.value = value; + } + + @Override + public T getValue(int pollerIndex) { + return value; + } + } +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricReaderTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricReaderTests.java new file mode 100644 index 00000000..64200f0d --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/metrics/servo/ServoMetricReaderTests.java @@ -0,0 +1,49 @@ +package org.springframework.cloud.netflix.metrics.servo; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.cloud.netflix.metrics.AbstractMetricsTests; + +import com.google.common.collect.Lists; +import com.netflix.servo.DefaultMonitorRegistry; +import com.netflix.servo.MonitorRegistry; +import com.netflix.servo.monitor.BasicTimer; +import com.netflix.servo.monitor.MonitorConfig; + +import static junit.framework.Assert.assertEquals; + +public class ServoMetricReaderTests extends AbstractMetricsTests { + @Test + public void singleCompositeMonitorYieldsMultipleActuatorMetrics() { + MonitorRegistry registry = DefaultMonitorRegistry.getInstance(); + + ServoMetricReader reader = new ServoMetricReader(registry, + new DimensionalServoMetricNaming()); + + MonitorConfig.Builder builder = new MonitorConfig.Builder("metricName"); + + BasicTimer timer = ServoMonitorCache.getTimer(builder.build()); + + List> metrics = Lists.newArrayList(reader.findAll()); + + List metricNames = new ArrayList<>(); + for (Metric metric : metrics) { + metricNames.add(metric.getName()); + } + Collections.sort(metricNames); + + assertEquals(4, metrics.size()); + assertEquals("metricName(statistic=count,type=NORMALIZED,unit=MILLISECONDS)", + metricNames.get(0)); + assertEquals("metricName(statistic=max,type=GAUGE,unit=MILLISECONDS)", + metricNames.get(1)); + assertEquals("metricName(statistic=min,type=GAUGE,unit=MILLISECONDS)", + metricNames.get(2)); + assertEquals("metricName(statistic=totalTime,type=NORMALIZED,unit=MILLISECONDS)", + metricNames.get(3)); + } +} diff --git a/spring-cloud-netflix-spectator/.jdk8 b/spring-cloud-netflix-spectator/.jdk8 new file mode 100644 index 00000000..e69de29b diff --git a/spring-cloud-netflix-spectator/pom.xml b/spring-cloud-netflix-spectator/pom.xml new file mode 100644 index 00000000..47535eae --- /dev/null +++ b/spring-cloud-netflix-spectator/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-netflix + 1.1.0.BUILD-SNAPSHOT + .. + + spring-cloud-netflix-spectator + jar + Spring Cloud Netflix Spectator + Spring Cloud Netflix Spectator + + ${basedir}/.. + 0.30.0 + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.8 + 1.8 + + + + + + + + com.netflix.spectator + spectator-api + ${spectator.version} + + + com.netflix.spectator + spectator-reg-servo + ${spectator.version} + + + com.netflix.servo + servo-core + ${servo.version} + + + + + + org.springframework.boot + spring-boot + + + org.springframework.boot + spring-boot-autoconfigure + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.cloud + spring-cloud-netflix-core + + + org.springframework + spring-webmvc + + + javax.servlet + javax.servlet-api + provided + + + com.netflix.servo + servo-core + + + com.netflix.spectator + spectator-api + + + com.netflix.spectator + spectator-reg-servo + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/spring-cloud-netflix-spectator/src/main/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricReader.java b/spring-cloud-netflix-spectator/src/main/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricReader.java new file mode 100644 index 00000000..38ac0211 --- /dev/null +++ b/spring-cloud-netflix-spectator/src/main/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricReader.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.spectator; + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.boot.actuate.metrics.Metric; +import org.springframework.boot.actuate.metrics.reader.MetricReader; + +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Registry; + +import static java.util.stream.StreamSupport.stream; + +/** + * Reads metrics from Spectator, placing Spectator tags in the form + * name(k1=v1,k2=v2) where name is the metric name and the + * contents of the parentheses are tag key-value pairs. + * + * @author Jon Schneider + */ +public class SpectatorMetricReader implements MetricReader { + private Registry registry; + + public SpectatorMetricReader(Registry registry) { + this.registry = registry; + } + + protected static String asHierarchicalName(Id id) { + List tags = stream(id.tags().spliterator(), false).map( + t -> t.key() + "=" + t.value()).collect(Collectors.toList()); + return id.name() + "(" + String.join(",", tags) + ")"; + } + + @Override + public Metric findOne(String name) { + throw new UnsupportedOperationException( + "cannot construct a tag-based Spectator id from a hierarchical name"); + } + + @Override + public Iterable> findAll() { + return stream(registry.spliterator(), false) + .flatMap( + metric -> stream(metric.measure().spliterator(), false).map( + measure -> new Metric<>(asHierarchicalName(measure.id()), + measure.value()))) + .sorted((m1, m2) -> m1.getName().compareTo(m2.getName())) + .collect(Collectors.toList()); + } + + @Override + public long count() { + return stream(registry.spliterator(), false).flatMap( + m -> stream(m.measure().spliterator(), false)).count(); + } +} diff --git a/spring-cloud-netflix-spectator/src/main/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricServices.java b/spring-cloud-netflix-spectator/src/main/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricServices.java new file mode 100644 index 00000000..fc25e853 --- /dev/null +++ b/spring-cloud-netflix-spectator/src/main/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricServices.java @@ -0,0 +1,134 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.spectator; + +import java.util.Collections; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.boot.actuate.metrics.CounterService; +import org.springframework.boot.actuate.metrics.GaugeService; + +import com.netflix.spectator.api.AbstractMeter; +import com.netflix.spectator.api.Gauge; +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Measurement; +import com.netflix.spectator.api.Registry; +import com.netflix.spectator.impl.AtomicDouble; + +/** + * Provides a CounterService and GaugeService implementation + * backed by Spectator. + * + * @author Jon Schneider + */ +public class SpectatorMetricServices implements CounterService, GaugeService { + private final Registry registry; + + private final ConcurrentMap counters = new ConcurrentHashMap<>(); + private final ConcurrentMap gauges = new ConcurrentHashMap<>(); + + public SpectatorMetricServices(Registry registry) { + this.registry = registry; + } + + protected static String stripMetricName(String metricName) { + return metricName.replaceFirst("^(timer|histogram|meter)\\.", ""); + } + + @Override + public void increment(String name) { + incrementInternal(name, 1L); + } + + @Override + public void decrement(String name) { + incrementInternal(name, -1L); + } + + private void incrementInternal(String name, long value) { + if (name.startsWith("status.")) { + // drop this metric since we are capturing it already with + // SpectatorHandlerInterceptor, + // and we are able to glean more information like exceptionType from that + // mechanism than what + // boot provides us + } + else if (name.startsWith("meter.")) { + registry.counter(stripMetricName(name)).increment(value); + } + else { + final Id id = registry.createId(name); + final AtomicLong gauge = getCounterStorage(id); + gauge.addAndGet(value); + registry.register(new NumericGauge(id, gauge)); + } + } + + @Override + public void reset(String name) { + final Id id = registry.createId(stripMetricName(name)); + counters.remove(id); + gauges.remove(id); + } + + @Override + public void submit(String name, double dValue) { + long value = ((Double) dValue).longValue(); + if (name.startsWith("histogram.")) { + registry.distributionSummary(stripMetricName(name)).record(value); + } + else if (name.startsWith("timer.")) { + registry.timer(stripMetricName(name)).record(value, TimeUnit.MILLISECONDS); + } + else { + final Id id = registry.createId(name); + final AtomicDouble gauge = getGaugeStorage(id); + gauge.set(dValue); + registry.register(new NumericGauge(id, gauge)); + } + } + + private AtomicDouble getGaugeStorage(Id id) { + final AtomicDouble newGauge = new AtomicDouble(0); + final AtomicDouble existingGauge = gauges.putIfAbsent(id, newGauge); + return existingGauge == null ? newGauge : existingGauge; + } + + private AtomicLong getCounterStorage(Id id) { + final AtomicLong newCounter = new AtomicLong(0); + final AtomicLong existingCounter = counters.putIfAbsent(id, newCounter); + return existingCounter == null ? newCounter : existingCounter; + } + + private class NumericGauge extends AbstractMeter implements Gauge { + NumericGauge(Id id, Number val) { + super(registry.clock(), id, val); + } + + @Override + public Iterable measure() { + return Collections.singleton(new Measurement(this.id, this.clock.wallTime(), + this.value())); + } + + @SuppressWarnings("ConstantConditions") + @Override + public double value() { + return this.ref.get().doubleValue(); + } + } +} diff --git a/spring-cloud-netflix-spectator/src/main/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricsAutoConfiguration.java b/spring-cloud-netflix-spectator/src/main/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricsAutoConfiguration.java new file mode 100644 index 00000000..ca6d6f3d --- /dev/null +++ b/spring-cloud-netflix-spectator/src/main/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricsAutoConfiguration.java @@ -0,0 +1,91 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.spectator; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.autoconfigure.EndpointAutoConfiguration; +import org.springframework.boot.actuate.endpoint.MetricReaderPublicMetrics; +import org.springframework.boot.actuate.metrics.reader.MetricReader; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.netflix.metrics.DefaultMetricsTagProvider; +import org.springframework.cloud.netflix.metrics.MetricsInterceptorConfiguration; +import org.springframework.cloud.netflix.metrics.MetricsTagProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +import com.netflix.servo.DefaultMonitorRegistry; +import com.netflix.servo.MonitorRegistry; +import com.netflix.servo.publish.MetricPoller; +import com.netflix.servo.publish.MonitorRegistryMetricPoller; +import com.netflix.spectator.api.Registry; +import com.netflix.spectator.servo.ServoRegistry; + +/** + * Configures a basic Spectator registry that bridges to the legacy Servo API. We use this + * bridge because servo contains an Atlas plugin that allows us to easily send all + * Spectator metrics to Atlas. Servo contains a similar plugin for Graphite. + * + * Conditionally configures both an MVC interceptor and a RestTemplate interceptor that + * records metrics for request handling timings. + * + * @author Jon Schneider + */ +@Configuration +@AutoConfigureBefore(EndpointAutoConfiguration.class) +@ConditionalOnClass({ Registry.class, MetricReader.class }) +@Import(MetricsInterceptorConfiguration.class) +public class SpectatorMetricsAutoConfiguration { + @Value("${netflix.metrics.servo.registryClass:com.netflix.servo.BasicMonitorRegistry}") + String servoRegistryClass; + + @Bean + @ConditionalOnMissingBean + public MonitorRegistry monitorRegistry() { + System.setProperty(DefaultMonitorRegistry.class.getCanonicalName() + + ".registryClass", servoRegistryClass); + return DefaultMonitorRegistry.getInstance(); + } + + @Bean + @ConditionalOnMissingBean(Registry.class) + Registry registry(MonitorRegistry monitorRegistry) { + return new ServoRegistry(); + } + + @Bean + @ConditionalOnMissingBean(MetricPoller.class) + MetricPoller metricPoller() { + return new MonitorRegistryMetricPoller(); + } + + @Bean + @ConditionalOnMissingBean + public SpectatorMetricServices spectatorMetricServices(Registry metricRegistry) { + return new SpectatorMetricServices(metricRegistry); + } + + @Bean + public MetricReaderPublicMetrics spectatorPublicMetrics(Registry metricRegistry) { + SpectatorMetricReader reader = new SpectatorMetricReader(metricRegistry); + return new MetricReaderPublicMetrics(reader); + } + + @Bean + public MetricsTagProvider defaultMetricsTagProvider() { + return new DefaultMetricsTagProvider(); + } +} diff --git a/spring-cloud-netflix-spectator/src/main/resources/META-INF/spring.factories b/spring-cloud-netflix-spectator/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..2f16dbb5 --- /dev/null +++ b/spring-cloud-netflix-spectator/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.netflix.metrics.spectator.SpectatorMetricsAutoConfiguration \ No newline at end of file diff --git a/spring-cloud-netflix-spectator/src/test/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricReaderTests.java b/spring-cloud-netflix-spectator/src/test/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricReaderTests.java new file mode 100644 index 00000000..68e3194e --- /dev/null +++ b/spring-cloud-netflix-spectator/src/test/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricReaderTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.spectator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +import org.junit.Test; + +import com.netflix.spectator.api.Id; +import com.netflix.spectator.api.Tag; + +import static org.junit.Assert.assertEquals; +import static org.springframework.cloud.netflix.metrics.spectator.SpectatorMetricReader.asHierarchicalName; + +/** + * @author Jon Schneider + */ +public class SpectatorMetricReaderTests { + @Test + public void convertSpectatorMetricWithTagsToHierarchicalName() { + Id mWithTags = new SimpleId("m", "t1", "val1", "t2", "val2"); + assertEquals("m(t1=val1,t2=val2)", asHierarchicalName(mWithTags)); + } + + private class SimpleTag implements Tag { + String key; + String value; + + public SimpleTag(String key, String value) { + this.key = key; + this.value = value; + } + + @Override + public String key() { + return key; + } + + @Override + public String value() { + return value; + } + } + + private class SimpleId implements Id { + String name; + Collection tags; + + public SimpleId(String name, String... tagPairs) { + this.name = name; + tags = new ArrayList<>(); + for (int i = 0; i < tagPairs.length; i += 2) + tags.add(new SimpleTag(tagPairs[i], tagPairs[i + 1])); + } + + @Override + public String name() { + return name; + } + + @Override + public Iterable tags() { + return tags; + } + + @Override + public Id withTag(String s, String s1) { + return null; + } + + @Override + public Id withTag(Tag tag) { + return null; + } + + @Override + public Id withTags(Iterable iterable) { + return null; + } + + @Override + public Id withTags(Map map) { + return null; + } + } +} diff --git a/spring-cloud-netflix-spectator/src/test/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricServicesTests.java b/spring-cloud-netflix-spectator/src/test/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricServicesTests.java new file mode 100644 index 00000000..17ee9f2e --- /dev/null +++ b/spring-cloud-netflix-spectator/src/test/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricServicesTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.spectator; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.springframework.cloud.netflix.metrics.spectator.SpectatorMetricServices.stripMetricName; + +public class SpectatorMetricServicesTests { + @Test + public void metricNameWithNoTypeIsLeftUnchanged() { + assertEquals("foo", stripMetricName("foo")); + assertEquals("foo.bar", stripMetricName("foo.bar")); + } + + @Test + public void metricTypeIsStrippedFromMetricName() { + assertEquals("foo", stripMetricName("timer.foo")); + assertEquals("foo", stripMetricName("histogram.foo")); + assertEquals("foo", stripMetricName("meter.foo")); + } + + @Test + public void metricTypeNameEmbeddedInMiddleOfMetricNameIsNotRemoved() { + assertEquals("bar.timer.foo", stripMetricName("bar.timer.foo")); + } +} diff --git a/spring-cloud-netflix-spectator/src/test/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricsHandlerInterceptorIntegrationTests.java b/spring-cloud-netflix-spectator/src/test/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricsHandlerInterceptorIntegrationTests.java new file mode 100644 index 00000000..885bbc5b --- /dev/null +++ b/spring-cloud-netflix-spectator/src/test/java/org/springframework/cloud/netflix/metrics/spectator/SpectatorMetricsHandlerInterceptorIntegrationTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2013-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 org.springframework.cloud.netflix.metrics.spectator; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.test.ImportAutoConfiguration; +import org.springframework.cloud.netflix.metrics.MetricsHandlerInterceptor; +import org.springframework.cloud.netflix.metrics.servo.ServoMonitorCache; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.netflix.servo.MonitorRegistry; +import com.netflix.servo.monitor.BasicTimer; +import com.netflix.servo.monitor.MonitorConfig; + +import static org.junit.Assert.assertFalse; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author Jon Schneider + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = SpectatorMetricsTestConfig.class) +@WebAppConfiguration +@TestPropertySource(properties = "netflix.metrics.rest.metricName=metricName") +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class SpectatorMetricsHandlerInterceptorIntegrationTests { + @Autowired + WebApplicationContext webAppContext; + + @Autowired + MonitorRegistry registry; + + MockMvc mvc; + + @Test + public void autoConfigurationWiresTheMetricsInterceptor() { + assertFalse(webAppContext.getBeansOfType(MetricsHandlerInterceptor.class) + .isEmpty()); + } + + @Before + public void setup() { + mvc = MockMvcBuilders.webAppContextSetup(webAppContext).build(); + } + + @Test + public void metricsGatheredWhenSuccess() throws Exception { + mvc.perform(get("/test/some/request/10")).andExpect(status().isOk()); + assertTimer("test_some_request_-id-", null, 200); + } + + @Test + public void metricsGatheredWhenClientRequestBad() throws Exception { + mvc.perform(get("/test/some/request/oops")) + .andExpect(status().is4xxClientError()); + assertTimer("test_some_request_-id-", null, 400); + } + + @Test + public void metricsGatheredWhenUnhandledError() throws Exception { + try { + mvc.perform(get("/test/some/unhandledError/10")).andExpect(status().isOk()); + } + catch (Exception e) { + } + assertTimer("test_some_unhandledError_-id-", "RuntimeException", 200); + } + + @Test + public void metricsGatheredWhenHandledError() throws Exception { + mvc.perform(get("/test/some/error/10")).andExpect(status().is4xxClientError()); + assertTimer("test_some_error_-id-", null, 422); + } + + protected void assertTimer(String uriTag, String exceptionType, Integer status) { + MonitorConfig.Builder builder = new MonitorConfig.Builder("metricName") + .withTag("method", "GET").withTag("uri", uriTag) + .withTag("status", status.toString()); + + if (exceptionType != null) + builder = builder.withTag("exception", exceptionType); + + BasicTimer timer = ServoMonitorCache.getTimer(builder.build()); + Assert.assertEquals(1L, (long) timer.getCount()); + } +} + +@Configuration +@EnableWebMvc +@ImportAutoConfiguration({ SpectatorMetricsAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class }) +class SpectatorMetricsTestConfig { + @Bean + SpectatorMetricsTestController testController() { + return new SpectatorMetricsTestController(); + } +} + +@RestController +@RequestMapping("/test/some") +@ControllerAdvice +class SpectatorMetricsTestController { + @RequestMapping("/request/{id}") + public String testSomeRequest(@PathVariable Long id) { + return id.toString(); + } + + @RequestMapping("/error/{id}") + public String testSomeHandledError(@PathVariable Long id) { + throw new IllegalStateException("Boom on $id!"); + } + + @RequestMapping("/unhandledError/{id}") + public String testSomeUnhandledError(@PathVariable Long id) { + throw new RuntimeException("Boom on $id!"); + } + + @ExceptionHandler(value = IllegalStateException.class) + @ResponseStatus(code = HttpStatus.UNPROCESSABLE_ENTITY) + ModelAndView defaultErrorHandler(HttpServletRequest request, Exception e) { + return new ModelAndView("error"); + } +} \ No newline at end of file diff --git a/spring-cloud-starter-atlas/pom.xml b/spring-cloud-starter-atlas/pom.xml new file mode 100644 index 00000000..497f68ef --- /dev/null +++ b/spring-cloud-starter-atlas/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-netflix + 1.1.0.BUILD-SNAPSHOT + .. + + spring-cloud-starter-atlas + spring-cloud-starter-atlas + Spring Cloud Starter Atlas + http://projects.spring.io/spring-cloud + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.cloud + spring-cloud-starter + + + org.springframework.cloud + spring-cloud-netflix-core + + + com.netflix.servo + servo-core + + + com.fasterxml.jackson.dataformat + jackson-dataformat-smile + + + diff --git a/spring-cloud-starter-atlas/src/main/resources/META-INF/spring.provides b/spring-cloud-starter-atlas/src/main/resources/META-INF/spring.provides new file mode 100644 index 00000000..ef7cba08 --- /dev/null +++ b/spring-cloud-starter-atlas/src/main/resources/META-INF/spring.provides @@ -0,0 +1 @@ +provides: spring-cloud-starter, spring-cloud-netflix-core, servo-core, jackson-dataformat-smile \ No newline at end of file diff --git a/spring-cloud-starter-spectator/.jdk8 b/spring-cloud-starter-spectator/.jdk8 new file mode 100644 index 00000000..e69de29b diff --git a/spring-cloud-starter-spectator/pom.xml b/spring-cloud-starter-spectator/pom.xml new file mode 100644 index 00000000..fc4b7195 --- /dev/null +++ b/spring-cloud-starter-spectator/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + org.springframework.cloud + spring-cloud-netflix + 1.1.0.BUILD-SNAPSHOT + .. + + spring-cloud-starter-spectator + spring-cloud-starter-spectator + Spring Cloud Starter Spectator + http://projects.spring.io/spring-cloud + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + org.springframework.cloud + spring-cloud-starter + + + org.springframework.cloud + spring-cloud-netflix-spectator + + + diff --git a/spring-cloud-starter-spectator/src/main/resources/META-INF/spring.provides b/spring-cloud-starter-spectator/src/main/resources/META-INF/spring.provides new file mode 100644 index 00000000..b33a3efa --- /dev/null +++ b/spring-cloud-starter-spectator/src/main/resources/META-INF/spring.provides @@ -0,0 +1 @@ +provides: spring-cloud-starter, spring-cloud-netflix-spectator \ No newline at end of file