diff --git a/pom.xml b/pom.xml
index 30086f24..1fb88167 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,7 +30,7 @@
8.11.01.4.182.1.0
- 0.9.4
+ 0.10.01.1.0-rc.11.0.111.7
@@ -83,6 +83,11 @@
pomimport
+
+ org.springframework.cloud
+ spring-cloud-starter-atlas
+ 1.1.0.BUILD-SNAPSHOT
+ org.springframework.cloudspring-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.cloudspring-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.cloudspring-cloud-netflix-turbine
@@ -221,6 +236,11 @@
servo-core${servo.version}
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-smile
+ ${jackson.version}
+ com.netflix.eurekaeureka-client
@@ -410,15 +430,18 @@
spring-cloud-netflix-hystrix-amqpspring-cloud-netflix-hystrix-streamspring-cloud-netflix-eureka-server
+ spring-cloud-netflix-spectatorspring-cloud-netflix-turbinespring-cloud-netflix-turbine-streamspring-cloud-netflix-sidecar
+ spring-cloud-starter-atlasspring-cloud-starter-eurekaspring-cloud-starter-eureka-serverspring-cloud-starter-feignspring-cloud-starter-hystrixspring-cloud-starter-hystrix-dashboardspring-cloud-starter-ribbon
+ spring-cloud-starter-spectatorspring-cloud-starter-turbinespring-cloud-starter-turbine-amqpspring-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 @@
h2test
+
+ 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