Remove spring-boot metrics information

This commit is contained in:
Spencer Gibb
2017-09-25 17:19:08 -04:00
parent 6775e76e5f
commit 8f4ff0e166
28 changed files with 0 additions and 2150 deletions

View File

@@ -1,111 +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.metrics;
import java.io.IOException;
import java.util.Collections;
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.util.StringUtils;
import org.springframework.web.servlet.HandlerMapping;
/**
* @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();
if( host == null ) {
host = "none";
}
String strippedUrlTemplate = urlTemplate.replaceAll("^https?://[^/]+/", "");
Map<String, String> tags = new HashMap<>();
tags.put("method", request.getMethod().name());
tags.put("uri", sanitizeUrlTemplate(strippedUrlTemplate));
tags.put("status", status);
tags.put("clientName", host);
return Collections.unmodifiableMap(tags);
}
@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 = (String) request
.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
if (uri == null) {
uri = request.getPathInfo();
}
if (!StringUtils.hasText(uri)) {
uri = "/";
}
uri = sanitizeUrlTemplate(uri.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
*/
protected String sanitizeUrlTemplate(String urlTemplate) {
String sanitized = urlTemplate
.replaceAll("\\{(\\w+):.+}(?=/|$)", "-$1-") // extract path variable names from regex expressions
.replaceAll("/", "_")
.replaceAll("[{}]", "-");
if (!StringUtils.hasText(sanitized)) {
sanitized = "none";
}
return sanitized;
}
}

View File

@@ -1,84 +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.metrics;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.TimeUnit;
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.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.
*/
private final ServoMonitorCache servoMonitorCache;
private final Collection<MetricsTagProvider> tagProviders;
private final String metricName;
public MetricsClientHttpRequestInterceptor(
Collection<MetricsTagProvider> tagProviders,
ServoMonitorCache servoMonitorCache, String metricName) {
this.tagProviders = tagProviders;
this.servoMonitorCache = servoMonitorCache;
this.metricName = 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);
}
}
}

View File

@@ -1,98 +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.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
ServoMonitorCache servoMonitorCache;
@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);
}
}

View File

@@ -1,131 +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.metrics;
import java.util.ArrayList;
import java.util.Collection;
import javax.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.metrics.reader.MetricReader;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.cloud.netflix.metrics.servo.ServoMonitorCache;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
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.MonitorRegistry;
import com.netflix.servo.monitor.Monitors;
/**
* @author Jon Schneider
*/
@Configuration
@ConditionalOnProperty(value = "spring.cloud.netflix.metrics.enabled", havingValue = "true", matchIfMissing = true)
@ConditionalOnClass({ Monitors.class, MetricReader.class })
public class MetricsInterceptorConfiguration {
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(WebMvcConfigurerAdapter.class)
static class MetricsWebResourceConfiguration extends WebMvcConfigurerAdapter {
@Bean
MetricsHandlerInterceptor servoMonitoringWebResourceInterceptor() {
return new MetricsHandlerInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(servoMonitoringWebResourceInterceptor());
}
}
@Configuration
@ConditionalOnClass({ RestTemplate.class, JoinPoint.class })
@ConditionalOnProperty(value = "spring.aop.enabled", havingValue = "true", matchIfMissing = true)
static class MetricsRestTemplateAspectConfiguration {
@Bean
RestTemplateUrlTemplateCapturingAspect restTemplateUrlTemplateCapturingAspect() {
return new RestTemplateUrlTemplateCapturingAspect();
}
}
@Configuration
@ConditionalOnClass({ RestTemplate.class, HttpServletRequest.class }) // HttpServletRequest implicitly required by MetricsTagProvider
static class MetricsRestTemplateConfiguration {
@Value("${netflix.metrics.restClient.metricName:restclient}")
String metricName;
@Bean
MetricsClientHttpRequestInterceptor spectatorLoggingClientHttpRequestInterceptor(
Collection<MetricsTagProvider> tagProviders,
ServoMonitorCache servoMonitorCache) {
return new MetricsClientHttpRequestInterceptor(tagProviders,
servoMonitorCache, this.metricName);
}
@Bean
BeanPostProcessor spectatorRestTemplateInterceptorPostProcessor() {
return new MetricsInterceptorPostProcessor();
}
private static class MetricsInterceptorPostProcessor
implements BeanPostProcessor, ApplicationContextAware {
private ApplicationContext context;
private MetricsClientHttpRequestInterceptor interceptor;
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof RestTemplate) {
if (this.interceptor == null) {
this.interceptor = this.context
.getBean(MetricsClientHttpRequestInterceptor.class);
}
RestTemplate restTemplate = (RestTemplate) bean;
// create a new list as the old one may be unmodifiable (ie Arrays.asList())
ArrayList<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(interceptor);
interceptors.addAll(restTemplate.getInterceptors());
restTemplate.setInterceptors(interceptors);
}
return bean;
}
@Override
public void setApplicationContext(ApplicationContext context)
throws BeansException {
this.context = context;
}
}
}
}

View File

@@ -1,43 +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.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);
}

View File

@@ -1,39 +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.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();
}
}

View File

@@ -1,39 +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.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.RestOperations+.*(String, ..))")
public Object captureUrlTemplate(ProceedingJoinPoint joinPoint) throws Throwable {
try {
String urlTemplate = (String) joinPoint.getArgs()[0];
RestTemplateUrlTemplateHolder.setRestTemplateUrlTemplate(urlTemplate);
return joinPoint.proceed();
}
finally {
RestTemplateUrlTemplateHolder.clear();
}
}
}

View File

@@ -1,39 +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.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();
}
}

View File

@@ -1,68 +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.metrics;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.util.ClassUtils;
/**
* @author Dave Syer
*/
public class ServoEnvironmentPostProcessor implements EnvironmentPostProcessor {
private static final Log log = LogFactory.getLog(ServoEnvironmentPostProcessor.class);
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
if (ClassUtils.isPresent("com.netflix.servo.monitor.Monitors", null)) {
// Make spring AOP default to target class so RestTemplates can be customized
log.debug("Setting 'spring.aop.proxyTargetClass=true' to make spring AOP default to target class so RestTemplates can be customized");
addDefaultProperty(environment, "spring.aop.proxyTargetClass", "true");
}
}
private void addDefaultProperty(ConfigurableEnvironment environment, String name,
String value) {
MutablePropertySources sources = environment.getPropertySources();
Map<String, Object> map = null;
if (sources.contains("defaultProperties")) {
PropertySource<?> source = sources.get("defaultProperties");
if (source instanceof MapPropertySource) {
map = ((MapPropertySource) source).getSource();
}
}
else {
map = new LinkedHashMap<>();
sources.addLast(new MapPropertySource("defaultProperties", map));
}
if (map != null) {
map.put(name, value);
}
}
}

View File

@@ -1,75 +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.metrics.atlas;
import java.util.Collection;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
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.cloud.netflix.metrics.servo.ServoMetricsAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.client.RestTemplate;
import com.netflix.servo.MonitorRegistry;
import com.netflix.servo.publish.MonitorRegistryMetricPoller;
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)
@Import(ServoMetricsAutoConfiguration.class)
public class AtlasConfiguration {
@Autowired(required = false)
private Collection<AtlasTagProvider> tagProviders;
@Autowired(required = false)
@Qualifier("atlasRestTemplate")
private RestTemplate restTemplate = new RestTemplate();
@Bean
public AtlasMetricObserverConfigBean atlasObserverConfig() {
return new AtlasMetricObserverConfigBean();
}
@Bean
@ConditionalOnMissingBean
public AtlasMetricObserver atlasObserver(AtlasMetricObserverConfigBean atlasObserverConfig) {
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, MonitorRegistry monitorRegistry) {
return new AtlasExporter(observer, new MonitorRegistryMetricPoller(monitorRegistry));
}
}

View File

@@ -1,37 +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.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));
}
}

View File

@@ -1,273 +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.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.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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.monitor.MonitorConfig;
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.debug("Invalid tag key " + tag.getKey());
return false;
}
if (!validAtlasTag.matcher(tag.getValue()).matches()) {
logger.debug("Invalid tag value " + tag.getValue());
return false;
}
}
return true;
}
static String normalizeAtlasUri(String uri) {
if (uri != null) {
Matcher matcher = Pattern.compile("(.+?)(/api/v1/publish)?/?").matcher(uri);
if (matcher.matches())
return matcher.group(1) + "/api/v1/publish";
else
throw new IllegalStateException("netflix.atlas.uri is not a valid uri");
}
throw new IllegalStateException("netflix.atlas.uri was not found in your properties and is required to communicate with Atlas");
}
@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 = sanitizeTags(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);
}
}
enum PublishMetricsBatchStatus {
NothingToDo, Success, PartialSuccess, Failure
}
PublishMetricsBatchStatus 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 PublishMetricsBatchStatus.NothingToDo; // 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 {
ResponseEntity<Map> response = restTemplate.exchange(uri, HttpMethod.POST, entity, Map.class);
if(response.getStatusCode() == HttpStatus.ACCEPTED) {
// partial success processing the metrics batch
List<String> messages = (List<String>) response.getBody().get("message");
if(messages != null) {
for (String message : messages) {
logger.error("Failed to write metric to atlas: " + message);
}
}
return PublishMetricsBatchStatus.PartialSuccess;
}
}
catch (HttpClientErrorException e) {
logger.error("Failed to write metrics to atlas: " + e.getResponseBodyAsString());
return PublishMetricsBatchStatus.Failure;
}
catch (RestClientException e) {
logger.error("Failed to write metrics to atlas", e);
return PublishMetricsBatchStatus.Failure;
}
}
catch (IOException e) {
return PublishMetricsBatchStatus.Failure;
}
return PublishMetricsBatchStatus.Success;
}
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;
}
static List<Metric> sanitizeTags(List<Metric> metrics) {
List<Metric> sanitized = new ArrayList<>(metrics.size());
for (Metric m : metrics) {
MonitorConfig.Builder config = MonitorConfig.builder(toValidCharset(m.getConfig().getName()));
for (Tag tag : m.getConfig().getTags()) {
config.withTag(toValidCharset(tag.getKey()), toValidCharset(tag.getValue()));
}
config.withPublishingPolicy(m.getConfig().getPublishingPolicy());
sanitized.add(new Metric(config.build(), m.getTimestamp(), m.getValue()));
}
return sanitized;
}
private static String toValidCharset(String name) {
return name.replaceAll("[^\\.\\-\\w]", "_");
}
static List<Metric> addTypeTagsAsNecessary(List<Metric> metrics) {
List<Metric> typedMetrics = new ArrayList<>(metrics.size());
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;
}
}

View File

@@ -1,50 +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.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;
}
}

View File

@@ -1,26 +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.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();
}

View File

@@ -1,34 +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.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(AtlasConfiguration.class)
public @interface EnableAtlas {
}

View File

@@ -1,38 +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.metrics.servo;
import java.util.ArrayList;
import java.util.List;
import org.springframework.util.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.collectionToCommaDelimitedString(tags) + ")";
}
}

View File

@@ -1,110 +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.metrics.servo;
import com.netflix.servo.annotations.DataSourceType;
import com.netflix.servo.monitor.Monitor;
import com.netflix.servo.monitor.MonitorConfig;
import com.netflix.servo.tag.Tag;
import com.netflix.servo.tag.TagList;
/**
* @author Spencer Gibb
*/
public class HierarchicalServoMetricNaming implements ServoMetricNaming {
private static final String JMX_DOMAIN_KEY = "JmxDomain";
public static final String SERVO = "servo.";
@Override
public String asHierarchicalName(Monitor<?> monitor) {
MonitorConfig config = monitor.getConfig();
TagList tags = config.getTags();
Tag domainTag = tags.getTag(JMX_DOMAIN_KEY);
String name;
if (domainTag != null) { // jmx metric
name = handleJmxMetric(config, tags);
} else {
name = handleMetric(config, tags);
}
return name.toLowerCase();
}
private String handleMetric(MonitorConfig config, TagList tags) {
String type = cleanValue(tags.getTag(DataSourceType.KEY), false);
String instanceName = cleanValue(tags.getTag("instance"), false);
String name = cleanupIllegalCharacters(config.getName(), true);
String statistic = cleanValue(tags.getTag("statistic"), false);
StringBuilder nameBuilder = new StringBuilder();
if (type != null) {
nameBuilder.append(type).append(".");
}
nameBuilder.append(SERVO);
if (instanceName != null) {
nameBuilder.append(instanceName).append(".");
}
if (name != null) {
nameBuilder.append(name).append(".");
}
if (statistic != null) {
nameBuilder.append(statistic).append(".");
}
// remove trailing "."
nameBuilder.deleteCharAt(nameBuilder.lastIndexOf("."));
return nameBuilder.toString();
}
private String handleJmxMetric(MonitorConfig config, TagList tags) {
String domain = cleanValue(tags.getTag(JMX_DOMAIN_KEY), true);
String type = cleanValue(tags.getTag("Jmx.type"), false);
String instanceName = cleanValue(tags.getTag("Jmx.instance"), false);
String name = cleanValue(tags.getTag("Jmx.name"), true);
String fieldName = cleanupIllegalCharacters(config.getName(), false);
StringBuilder nameBuilder = new StringBuilder();
nameBuilder.append(domain).append(".");
if (type != null) {
nameBuilder.append(type).append(".");
}
nameBuilder.append(SERVO);
if (instanceName != null) {
nameBuilder.append(instanceName).append(".");
}
if (name != null) {
nameBuilder.append(name).append(".");
}
if (fieldName != null) {
nameBuilder.append(fieldName).append(".");
}
// remove trailing "."
nameBuilder.deleteCharAt(nameBuilder.lastIndexOf("."));
return nameBuilder.toString();
}
private String cleanValue(Tag tag, boolean allowPeriodsInName) {
if (tag == null) {
return null;
}
return cleanupIllegalCharacters(tag.getValue(), allowPeriodsInName);
}
private String cleanupIllegalCharacters(String s, boolean allowPeriodsInName) {
if (!allowPeriodsInName) {
s = s.replace(".", "_");
}
return s.replace(" ", "_");
}
}

View File

@@ -1,32 +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.metrics.servo;
import com.netflix.servo.monitor.Monitor;
/**
* @author Spencer Gibb
*/
public interface ServoMetricNaming {
/**
* @param monitor a monitor representing a single statistic (not a CompositeMonitor)
* @return a hierarchical name representing a single statistic for a servo monitor;
* note that this method will be called once for each statistic on a composite servo
* Monitor like a Timer.
*/
String asHierarchicalName(Monitor<?> monitor);
}

View File

@@ -1,87 +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.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;
}
}

View File

@@ -1,145 +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.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);
}
}
}

View File

@@ -1,106 +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.metrics.servo;
import org.springframework.boot.actuate.autoconfigure.metrics.ExportMetricReader;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricRepositoryAutoConfiguration;
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.boot.autoconfigure.condition.ConditionalOnProperty;
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(MetricRepositoryAutoConfiguration.class)
@Import(MetricsInterceptorConfiguration.class)
@ConditionalOnProperty(name = "spring.metrics.servo.enabled", matchIfMissing = true)
public class ServoMetricsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ServoMetricsConfigBean servoMetricsConfig() {
return new ServoMetricsConfigBean();
}
@Bean
@ConditionalOnMissingBean
public ServoMetricNaming servoMetricNaming() {
return new HierarchicalServoMetricNaming();
}
@Bean
@ConditionalOnMissingBean
public MonitorRegistry monitorRegistry(ServoMetricsConfigBean servoMetricsConfig) {
System.setProperty(
DefaultMonitorRegistry.class.getCanonicalName() + ".registryClass",
servoMetricsConfig.getRegistryClass());
return DefaultMonitorRegistry.getInstance();
}
@Bean
public ServoMonitorCache monitorCache(MonitorRegistry monitorRegistry, ServoMetricsConfigBean servoMetricsConfig) {
return new ServoMonitorCache(monitorRegistry, servoMetricsConfig);
}
@Bean
@ExportMetricReader
public ServoMetricReader servoMetricReader(MonitorRegistry monitorRegistry,
ServoMetricNaming servoMetricNaming) {
ServoMetricReader reader = new ServoMetricReader(monitorRegistry,
servoMetricNaming);
return reader;
}
@Bean
public MetricReaderPublicMetrics servoPublicMetrics(ServoMetricReader reader) {
return new MetricReaderPublicMetrics(reader);
}
@Bean
@ConditionalOnMissingBean({ CounterService.class, GaugeService.class })
public ServoMetricServices servoMetricServices(MonitorRegistry monitorRegistry) {
return new ServoMetricServices(monitorRegistry);
}
@Configuration
@ConditionalOnClass(name = "javax.servlet.http.HttpServletRequest")
protected static class MetricsTagConfiguration {
@Bean
public MetricsTagProvider defaultMetricsTagProvider() {
return new DefaultMetricsTagProvider();
}
}
}

View File

@@ -1,66 +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.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 {
/**
* Enable the Netflix Servo metrics services. If this flag is off Servo can still be
* used by Netflix OSS components, but the Spring Boot metrics collection will be done
* with the default services.
*/
boolean enabled = true;
/**
* Fully qualified class name for monitor registry used by Servo.
*/
String registryClass = "com.netflix.servo.BasicMonitorRegistry";
/**
* When the `ServoMonitorCache` reaches this size, a warning is logged.
* This will be useful if you are using string concatenation in RestTemplate urls.
*/
int cacheWarningThreshold = 1000;
public boolean getEnabled() {
return this.enabled;
}
public void isEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getRegistryClass() {
return this.registryClass;
}
public void setRegistryClass(String registryClass) {
this.registryClass = registryClass;
}
public int getCacheWarningThreshold() {
return cacheWarningThreshold;
}
public void setCacheWarningThreshold(int cacheWarningThreshold) {
this.cacheWarningThreshold = cacheWarningThreshold;
}
}

View File

@@ -1,63 +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.metrics.servo;
import java.util.HashMap;
import java.util.Map;
import com.netflix.servo.MonitorRegistry;
import com.netflix.servo.monitor.BasicTimer;
import com.netflix.servo.monitor.MonitorConfig;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Servo does not provide a mechanism to retrieve an existing monitor by name + tags.
*
* @author Jon Schneider
*/
public class ServoMonitorCache {
private static final Log log = LogFactory.getLog(ServoMonitorCache.class);
private final Map<MonitorConfig, BasicTimer> timerCache = new HashMap<>();
private final MonitorRegistry monitorRegistry;
private final ServoMetricsConfigBean config;
public ServoMonitorCache(MonitorRegistry monitorRegistry, ServoMetricsConfigBean config) {
this.monitorRegistry = monitorRegistry;
this.config = config;
}
/**
* @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 BasicTimer getTimer(MonitorConfig config) {
BasicTimer t = this.timerCache.get(config);
if (t != null)
return t;
t = new BasicTimer(config);
this.timerCache.put(config, t);
if (this.timerCache.size() > this.config.getCacheWarningThreshold()) {
log.warn("timerCache is above the warning threshold of " + this.config.getCacheWarningThreshold() + " with size " + this.timerCache.size() + ".");
}
this.monitorRegistry.register(t);
return t;
}
}

View File

@@ -1,85 +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.metrics.atlas;
import java.util.Properties;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
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.monitor.DynamicCounter;
/**
* @author Jon Schneider
*/
@SpringBootTest(classes = AtlasExporterConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class AtlasExporterTests {
@Autowired
private RestTemplate restTemplate;
@Autowired
private AtlasExporter atlasExporter;
@Test
public void exportMetricsAtPeriodicIntervals() {
MockRestServiceServer mockServer = MockRestServiceServer
.createServer(this.restTemplate);
mockServer.expect(MockRestRequestMatchers.requestTo("/atlas/api/v1/publish"))
.andExpect(MockRestRequestMatchers.method(HttpMethod.POST))
.andRespond(MockRestResponseCreators.withSuccess("{\"status\" : \"OK\"}",
MediaType.APPLICATION_JSON));
DynamicCounter.increment("counterThatWillBeSentToAtlas");
this.atlasExporter.export();
mockServer.verify();
}
}
@EnableAutoConfiguration
@Configuration
@EnableAtlas
class AtlasExporterConfiguration {
@Qualifier("atlasRestTemplate")
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
@Bean
public static PropertySourcesPlaceholderConfigurer properties() throws Exception {
final PropertySourcesPlaceholderConfigurer config = new PropertySourcesPlaceholderConfigurer();
Properties properties = new Properties();
properties.setProperty("netflix.atlas.uri", "atlas");
config.setProperties(properties);
return config;
}
}

View File

@@ -1,240 +0,0 @@
/*
* Copyright 2013-2017 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.HttpStatus;
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 com.netflix.servo.tag.Tag;
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.hamcrest.Matchers.equalTo;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* @author Jon Schneider
*/
public class AtlasMetricObserverTests {
@Test
public void normalizeAtlasUri() {
String normalized = "http://localhost:7001/api/v1/publish";
assertThat(AtlasMetricObserver.normalizeAtlasUri("http://localhost:7001"), is(equalTo(normalized)));
assertThat(AtlasMetricObserver.normalizeAtlasUri("http://localhost:7001/"), is(equalTo(normalized)));
assertThat(AtlasMetricObserver.normalizeAtlasUri("http://localhost:7001/api/v1/publish"), is(equalTo(normalized)));
assertThat(AtlasMetricObserver.normalizeAtlasUri("http://localhost:7001/api/v1/publish/"), is(equalTo(normalized)));
}
@Test(expected = IllegalStateException.class)
public void emptyAtlasUriThrowsException() {
AtlasMetricObserver.normalizeAtlasUri("");
}
@Test(expected = IllegalStateException.class)
public void missingAtlasUriThrowsException() {
AtlasMetricObserver.normalizeAtlasUri(null);
}
@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(),
System.currentTimeMillis(), "bar"));
// already has type
Metric m = new Metric(new MonitorConfig.Builder("foo")
.withTag(KEY, COUNTER.name()).withTag("atlas.dstype", "counter").build(),
System.currentTimeMillis(), "bar");
assertHasAtlasType("counter", m);
assertThat(m.getConfig().getTags().size(), is(equalTo(2)));
}
private void assertHasAtlasType(String atlasType, Metric m) {
assertThat(AtlasMetricObserver.addTypeTagsAsNecessary(Collections.singletonList(m))
.get(0).getConfig().getTags().getValue("atlas.dstype"), is(equalTo(atlasType)));
}
private Metric metricWithType(String key, DataSourceType type) {
return new Metric(new MonitorConfig.Builder(key).withTag(KEY, type.name())
.build(), System.currentTimeMillis(), 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();
}
/**
* If ALL of the metrics in a batch fail, Atlas will return a 400 with a String body indicating why.
*/
@Test
public void failingMetricsBatch() {
RestTemplate restTemplate = new RestTemplate();
AtlasMetricObserverConfigBean config = new AtlasMetricObserverConfigBean();
config.setBatchSize(1);
config.setUri("atlas");
MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);
mockServer
.expect(MockRestRequestMatchers.requestTo("/atlas/api/v1/publish"))
.andExpect(MockRestRequestMatchers.method(HttpMethod.POST))
.andRespond(MockRestResponseCreators.withBadRequest().body("foo0 is bad for some reason"));
AtlasMetricObserver obs = new AtlasMetricObserver(config, restTemplate, BasicTagList.EMPTY);
assertThat(obs.sendMetricsBatch(generateMetrics(1)),
is(equalTo(AtlasMetricObserver.PublishMetricsBatchStatus.Failure)));
}
/**
* If SOME metrics in a batch fail, Atlas will return a 202 with a JSON body with a message for each
* failing metric.
*/
@Test
public void partialSuccessMetricsBatch() {
RestTemplate restTemplate = new RestTemplate();
AtlasMetricObserverConfigBean config = new AtlasMetricObserverConfigBean();
config.setBatchSize(2);
config.setUri("atlas");
MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate);
mockServer
.expect(MockRestRequestMatchers.requestTo("/atlas/api/v1/publish"))
.andExpect(MockRestRequestMatchers.method(HttpMethod.POST))
.andRespond(
MockRestResponseCreators.withStatus(HttpStatus.ACCEPTED)
.body("{\"message\" : [\"foo1 is bad for some reason\"]}")
.contentType(MediaType.APPLICATION_JSON));
AtlasMetricObserver obs = new AtlasMetricObserver(config, restTemplate, BasicTagList.EMPTY);
assertThat(obs.sendMetricsBatch(generateMetrics(2)),
is(equalTo(AtlasMetricObserver.PublishMetricsBatchStatus.PartialSuccess)));
}
@Test
public void sanitizeMetrics() {
String mixtureOfValidAndInvalidChars = "a_1.2-Z/ A";
Metric m = new Metric(new MonitorConfig.Builder(mixtureOfValidAndInvalidChars)
.withTag(mixtureOfValidAndInvalidChars, mixtureOfValidAndInvalidChars).build(), 0, 1);
Metric sanitizedMetric = AtlasMetricObserver.sanitizeTags(Collections.singletonList(m)).get(0);
String valid = "a_1.2-Z__A";
assertThat(sanitizedMetric.getConfig().getName(), is(equalTo(valid)));
Tag tag = sanitizedMetric.getConfig().getTags().iterator().next();
assertThat(tag.getKey(), is(equalTo(valid)));
assertThat(tag.getValue(), is(equalTo(valid)));
}
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));
}
}
}

View File

@@ -1,30 +0,0 @@
<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-starter-netflix</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-cloud-starter-netflix-spectator</artifactId>
<name>Spring Cloud Starter Netflix Spectator</name>
<description>Spring Cloud Starter Netflix Spectator</description>
<url>https://projects.spring.io/spring-cloud</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>https://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>

View File

@@ -1 +0,0 @@
provides: spring-cloud-starter, spring-cloud-netflix-spectator