Browse Source
* For Java 8 users, Spectator is the recommended metrics library as Servo is now deprecated in favor of Spectator * Servo support now needs to be specifically enabled via @EnableServo, even if servo-core is included on the classpath * Added starter modules for both Spectator and Atlas support * ServoMetricReader defines a convention for mapping tag-based metrics to hiearchical names for rendering by the Actuator /metrics endpointpull/6/head
49 changed files with 2820 additions and 272 deletions
@ -0,0 +1,85 @@
@@ -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<String, String> 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<String, String> httpRequestTags(HttpServletRequest request, |
||||
HttpServletResponse response, Object handler, String caller) { |
||||
Map<String, String> 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("[{}]", "-"); |
||||
} |
||||
} |
@ -0,0 +1,82 @@
@@ -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<MetricsTagProvider> 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<String, String> 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); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,95 @@
@@ -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<MetricsTagProvider> 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<String, String> tags = tagProvider.httpRequestTags(request, response, |
||||
handler, caller); |
||||
for (Map.Entry<String, String> 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); |
||||
} |
||||
} |
@ -0,0 +1,80 @@
@@ -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; |
||||
} |
||||
}; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,43 @@
@@ -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<String, String> 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<String, String> httpRequestTags(HttpServletRequest request, |
||||
HttpServletResponse response, Object handler, String caller); |
||||
} |
@ -0,0 +1,39 @@
@@ -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<String, String> clientHttpRequestTags(HttpRequest request, |
||||
ClientHttpResponse response) { |
||||
return Collections.emptyMap(); |
||||
} |
||||
|
||||
@Override |
||||
public Map<String, String> httpRequestTags(HttpServletRequest request, |
||||
HttpServletResponse response, Object handler, String caller) { |
||||
return Collections.emptyMap(); |
||||
} |
||||
} |
@ -0,0 +1,39 @@
@@ -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(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,39 @@
@@ -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<String> restTemplateUrlTemplateHolder = new NamedThreadLocal<String>( |
||||
"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(); |
||||
} |
||||
} |
@ -0,0 +1,66 @@
@@ -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<AtlasTagProvider> 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<String, String> 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); |
||||
} |
||||
} |
@ -0,0 +1,37 @@
@@ -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)); |
||||
} |
||||
} |
@ -0,0 +1,236 @@
@@ -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<Metric> 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<Metric> metrics = addTypeTagsAsNecessary(rawMetrics); |
||||
|
||||
for (int i = 0; i < metrics.size(); i += config.getBatchSize()) { |
||||
List<Metric> 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<Metric> 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<byte[]> 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<Metric> 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<Metric> addTypeTagsAsNecessary(List<Metric> metrics) { |
||||
List<Metric> 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; |
||||
} |
||||
} |
@ -0,0 +1,50 @@
@@ -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; |
||||
} |
||||
} |
@ -0,0 +1,26 @@
@@ -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<String, String> defaultTags(); |
||||
} |
@ -0,0 +1,34 @@
@@ -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 { |
||||
} |
@ -0,0 +1,38 @@
@@ -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<String> tags = new ArrayList<>(config.getTags().size()); |
||||
for (Tag t : config.getTags()) { |
||||
tags.add(t.getKey() + "=" + t.getValue()); |
||||
} |
||||
return config.getName() + "(" + StringUtils.join(tags, ",") + ")"; |
||||
} |
||||
} |
@ -0,0 +1,87 @@
@@ -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<Metric<?>> findAll() { |
||||
Collection<Metric<?>> metrics = new ArrayList<>(); |
||||
for (Monitor<?> monitor : monitorRegistry.getRegisteredMonitors()) { |
||||
addToMetrics(monitor, metrics); |
||||
} |
||||
return metrics; |
||||
} |
||||
|
||||
private void addToMetrics(Monitor<?> monitor, Collection<Metric<?>> 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; |
||||
} |
||||
} |
@ -0,0 +1,145 @@
@@ -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 <code>CounterService</code> and <code>GaugeService</code> implementation |
||||
* backed by Servo. |
||||
* |
||||
* @author Jon Schneider |
||||
*/ |
||||
public class ServoMetricServices implements CounterService, GaugeService { |
||||
private final MonitorRegistry registry; |
||||
|
||||
private final ConcurrentMap<String, BasicCounter> counters = new ConcurrentHashMap<>(); |
||||
private final ConcurrentMap<String, LongGauge> longGauges = new ConcurrentHashMap<>(); |
||||
private final ConcurrentMap<String, DoubleGauge> doubleGauges = new ConcurrentHashMap<>(); |
||||
private final ConcurrentMap<String, BasicDistributionSummary> distributionSummaries = new ConcurrentHashMap<>(); |
||||
private final ConcurrentMap<String, BasicTimer> 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); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,85 @@
@@ -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(); |
||||
} |
||||
} |
@ -0,0 +1,30 @@
@@ -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; |
||||
} |
||||
} |
@ -0,0 +1,59 @@
@@ -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<MonitorConfig, BasicTimer> 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(); |
||||
} |
||||
} |
@ -1,79 +0,0 @@
@@ -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 <code>/metric</code> endpoint. |
||||
* |
||||
* @author Dave Syer |
||||
* @author Christian Dupuis |
||||
*/ |
||||
@CommonsLog |
||||
public class ServoMetricCollector implements DisposableBean { |
||||
|
||||
public ServoMetricCollector(MetricWriter metrics, ServoMetricNaming naming) { |
||||
List<MetricObserver> 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); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,54 +0,0 @@
@@ -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<Metric> 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()))); |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,87 +0,0 @@
@@ -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.); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,27 @@
@@ -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(); |
||||
} |
||||
} |
@ -0,0 +1,89 @@
@@ -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(); |
||||
} |
||||
} |
@ -0,0 +1,160 @@
@@ -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"); |
||||
} |
||||
} |
@ -0,0 +1,156 @@
@@ -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.<Metric> 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<Metric> generateMetrics(int numberOfMetrics) { |
||||
List<Metric> 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)); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,78 @@
@@ -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<T> extends AbstractMonitor<T> { |
||||
T value; |
||||
|
||||
protected FixedValueMonitor(MonitorConfig config, T value) { |
||||
super(config); |
||||
this.value = value; |
||||
} |
||||
|
||||
@Override |
||||
public T getValue(int pollerIndex) { |
||||
return value; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,49 @@
@@ -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<Metric<?>> metrics = Lists.newArrayList(reader.findAll()); |
||||
|
||||
List<String> 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)); |
||||
} |
||||
} |
@ -0,0 +1,95 @@
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
<parent> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-netflix</artifactId> |
||||
<version>1.1.0.BUILD-SNAPSHOT</version> |
||||
<relativePath>..</relativePath> <!-- lookup parent from repository --> |
||||
</parent> |
||||
<artifactId>spring-cloud-netflix-spectator</artifactId> |
||||
<packaging>jar</packaging> |
||||
<name>Spring Cloud Netflix Spectator</name> |
||||
<description>Spring Cloud Netflix Spectator</description> |
||||
<properties> |
||||
<main.basedir>${basedir}/..</main.basedir> |
||||
<spectator.version>0.30.0</spectator.version> |
||||
</properties> |
||||
<build> |
||||
<plugins> |
||||
<plugin> |
||||
<groupId>org.apache.maven.plugins</groupId> |
||||
<artifactId>maven-compiler-plugin</artifactId> |
||||
<configuration> |
||||
<source>1.8</source> |
||||
<target>1.8</target> |
||||
</configuration> |
||||
</plugin> |
||||
</plugins> |
||||
</build> |
||||
<dependencyManagement> |
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>com.netflix.spectator</groupId> |
||||
<artifactId>spectator-api</artifactId> |
||||
<version>${spectator.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.netflix.spectator</groupId> |
||||
<artifactId>spectator-reg-servo</artifactId> |
||||
<version>${spectator.version}</version> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.netflix.servo</groupId> |
||||
<artifactId>servo-core</artifactId> |
||||
<version>${servo.version}</version> |
||||
</dependency> |
||||
</dependencies> |
||||
</dependencyManagement> |
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-autoconfigure</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-actuator</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-netflix-core</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework</groupId> |
||||
<artifactId>spring-webmvc</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>javax.servlet</groupId> |
||||
<artifactId>javax.servlet-api</artifactId> |
||||
<scope>provided</scope> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.netflix.servo</groupId> |
||||
<artifactId>servo-core</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.netflix.spectator</groupId> |
||||
<artifactId>spectator-api</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.netflix.spectator</groupId> |
||||
<artifactId>spectator-reg-servo</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.boot</groupId> |
||||
<artifactId>spring-boot-starter-test</artifactId> |
||||
<scope>test</scope> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
@ -0,0 +1,69 @@
@@ -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 |
||||
* <code>name(k1=v1,k2=v2)</code> where <code>name</code> 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<String> 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<Metric<?>> 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(); |
||||
} |
||||
} |
@ -0,0 +1,134 @@
@@ -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 <code>CounterService</code> and <code>GaugeService</code> implementation |
||||
* backed by Spectator. |
||||
* |
||||
* @author Jon Schneider |
||||
*/ |
||||
public class SpectatorMetricServices implements CounterService, GaugeService { |
||||
private final Registry registry; |
||||
|
||||
private final ConcurrentMap<Id, AtomicLong> counters = new ConcurrentHashMap<>(); |
||||
private final ConcurrentMap<Id, AtomicDouble> 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<Number> implements Gauge { |
||||
NumericGauge(Id id, Number val) { |
||||
super(registry.clock(), id, val); |
||||
} |
||||
|
||||
@Override |
||||
public Iterable<Measurement> measure() { |
||||
return Collections.singleton(new Measurement(this.id, this.clock.wallTime(), |
||||
this.value())); |
||||
} |
||||
|
||||
@SuppressWarnings("ConstantConditions") |
||||
@Override |
||||
public double value() { |
||||
return this.ref.get().doubleValue(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,91 @@
@@ -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(); |
||||
} |
||||
} |
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ |
||||
org.springframework.cloud.netflix.metrics.spectator.SpectatorMetricsAutoConfiguration |
@ -0,0 +1,99 @@
@@ -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<Tag> 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<Tag> 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<Tag> iterable) { |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public Id withTags(Map<String, String> map) { |
||||
return null; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,39 @@
@@ -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")); |
||||
} |
||||
} |
@ -0,0 +1,160 @@
@@ -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"); |
||||
} |
||||
} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
<parent> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-netflix</artifactId> |
||||
<version>1.1.0.BUILD-SNAPSHOT</version> |
||||
<relativePath>..</relativePath> <!-- lookup parent from repository --> |
||||
</parent> |
||||
<artifactId>spring-cloud-starter-atlas</artifactId> |
||||
<name>spring-cloud-starter-atlas</name> |
||||
<description>Spring Cloud Starter Atlas</description> |
||||
<url>http://projects.spring.io/spring-cloud</url> |
||||
<organization> |
||||
<name>Pivotal Software, Inc.</name> |
||||
<url>http://www.spring.io</url> |
||||
</organization> |
||||
<properties> |
||||
<main.basedir>${basedir}/../..</main.basedir> |
||||
</properties> |
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-starter</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-netflix-core</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.netflix.servo</groupId> |
||||
<artifactId>servo-core</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>com.fasterxml.jackson.dataformat</groupId> |
||||
<artifactId>jackson-dataformat-smile</artifactId> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
provides: spring-cloud-starter, spring-cloud-netflix-core, servo-core, jackson-dataformat-smile |
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?> |
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||
<modelVersion>4.0.0</modelVersion> |
||||
<parent> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-netflix</artifactId> |
||||
<version>1.1.0.BUILD-SNAPSHOT</version> |
||||
<relativePath>..</relativePath> <!-- lookup parent from repository --> |
||||
</parent> |
||||
<artifactId>spring-cloud-starter-spectator</artifactId> |
||||
<name>spring-cloud-starter-spectator</name> |
||||
<description>Spring Cloud Starter Spectator</description> |
||||
<url>http://projects.spring.io/spring-cloud</url> |
||||
<organization> |
||||
<name>Pivotal Software, Inc.</name> |
||||
<url>http://www.spring.io</url> |
||||
</organization> |
||||
<properties> |
||||
<main.basedir>${basedir}/../..</main.basedir> |
||||
</properties> |
||||
<dependencies> |
||||
<dependency> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-starter</artifactId> |
||||
</dependency> |
||||
<dependency> |
||||
<groupId>org.springframework.cloud</groupId> |
||||
<artifactId>spring-cloud-netflix-spectator</artifactId> |
||||
</dependency> |
||||
</dependencies> |
||||
</project> |
Loading…
Reference in new issue