Convert BinderMetricsEmitter to an Exporter
Re-organize and re-name a bit Rename config package Use spring.metrics.export.* to configure the includes and excludes
This commit is contained in:
committed by
Vinicius Carvalho
parent
c812fa0374
commit
07d0328d68
@@ -29,6 +29,11 @@
|
||||
<artifactId>spring-cloud-stream-test-support</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
/*
|
||||
* Copyright 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.stream.config.metrics;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "spring.cloud.stream.metrics")
|
||||
public class StreamMetricsProperties {
|
||||
|
||||
private String prefix;
|
||||
|
||||
@Value("${spring.application.name:${vcap.application.name:${spring.config.name:application}}}")
|
||||
private String key;
|
||||
|
||||
private String metricName;
|
||||
|
||||
private String[] includes = new String[] { "integration**" };
|
||||
|
||||
private String[] excludes;
|
||||
|
||||
private String[] properties;
|
||||
|
||||
private Long delayMillis;
|
||||
|
||||
public String[] getIncludes() {
|
||||
return includes;
|
||||
}
|
||||
|
||||
public void setIncludes(String[] includes) {
|
||||
this.includes = includes;
|
||||
}
|
||||
|
||||
public String[] getExcludes() {
|
||||
return excludes;
|
||||
}
|
||||
|
||||
public void setExcludes(String[] excludes) {
|
||||
this.excludes = excludes;
|
||||
}
|
||||
|
||||
public Long getDelayMillis() {
|
||||
return delayMillis;
|
||||
}
|
||||
|
||||
public void setDelayMillis(Long delayMillis) {
|
||||
this.delayMillis = delayMillis;
|
||||
}
|
||||
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public void setPrefix(String prefix) {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String[] getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
public void setProperties(String[] properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public String getMetricName() {
|
||||
if (this.metricName == null) {
|
||||
this.metricName = resolveMetricName();
|
||||
}
|
||||
return metricName;
|
||||
}
|
||||
|
||||
private String resolveMetricName() {
|
||||
StringBuffer name = new StringBuffer(this.key);
|
||||
if (!StringUtils.isEmpty(this.prefix)) {
|
||||
String prefix = this.prefix;
|
||||
if (prefix.lastIndexOf(".") == -1) {
|
||||
prefix += ".";
|
||||
}
|
||||
name.insert(0, prefix);
|
||||
}
|
||||
return name.toString();
|
||||
}
|
||||
}
|
||||
@@ -38,13 +38,14 @@ public class ApplicationMetrics {
|
||||
|
||||
private int instanceIndex;
|
||||
|
||||
private Collection<Metric> metrics;
|
||||
private Collection<Metric<?>> metrics;
|
||||
|
||||
private Map<String, Object> properties;
|
||||
|
||||
@JsonCreator
|
||||
public ApplicationMetrics(@JsonProperty("name") String name, @JsonProperty("instanceIndex") int instanceIndex,
|
||||
@JsonProperty("metrics") Collection<Metric> metrics) {
|
||||
public ApplicationMetrics(@JsonProperty("name") String name,
|
||||
@JsonProperty("instanceIndex") int instanceIndex,
|
||||
@JsonProperty("metrics") Collection<Metric<?>> metrics) {
|
||||
this.name = name;
|
||||
this.instanceIndex = instanceIndex;
|
||||
this.metrics = metrics;
|
||||
@@ -67,11 +68,11 @@ public class ApplicationMetrics {
|
||||
this.instanceIndex = instanceIndex;
|
||||
}
|
||||
|
||||
public Collection<Metric> getMetrics() {
|
||||
public Collection<Metric<?>> getMetrics() {
|
||||
return metrics;
|
||||
}
|
||||
|
||||
public void setMetrics(Collection<Metric> metrics) {
|
||||
public void setMetrics(Collection<Metric<?>> metrics) {
|
||||
this.metrics = metrics;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 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.stream.metrics;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.MetricsEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.MetricsEndpointMetricReader;
|
||||
import org.springframework.boot.actuate.metrics.Metric;
|
||||
import org.springframework.boot.actuate.metrics.export.Exporter;
|
||||
import org.springframework.boot.actuate.metrics.export.MetricCopyExporter;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
* Component that sends {@link ApplicationMetrics} from
|
||||
* {@link MetricsEndpointMetricReader} downstream via the configured metrics channel.
|
||||
*
|
||||
* It uses the Spring Boot support for {@link Exporter} to periodically emit messages
|
||||
* polled from the endpoint.
|
||||
*
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
public class ApplicationMetricsExporter implements Exporter {
|
||||
|
||||
private MessageChannel source;
|
||||
|
||||
private ApplicationMetricsProperties properties;
|
||||
|
||||
private MetricsEndpointMetricReader metricsReader;
|
||||
|
||||
public ApplicationMetricsExporter(MetricsEndpoint endpoint, MessageChannel source,
|
||||
ApplicationMetricsProperties properties) {
|
||||
this.source = source;
|
||||
this.properties = properties;
|
||||
this.metricsReader = new MetricsEndpointMetricReader(endpoint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void export() {
|
||||
ApplicationMetrics appMetrics = new ApplicationMetrics(
|
||||
this.properties.getMetricName(), this.properties.getInstanceIndex(),
|
||||
filter());
|
||||
appMetrics.setProperties(this.properties.getExportProperties());
|
||||
source.send(MessageBuilder.withPayload(appMetrics).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy of similarly named method in {@link MetricCopyExporter}.
|
||||
*/
|
||||
protected Collection<Metric<?>> filter() {
|
||||
Collection<Metric<?>> result = new ArrayList<>();
|
||||
Iterable<Metric<?>> metrics = metricsReader.findAll();
|
||||
for (Metric<?> metric : metrics) {
|
||||
if (isMatch(metric.getName(), this.properties.getTrigger().getIncludes(),
|
||||
this.properties.getTrigger().getExcludes())) {
|
||||
result.add(metric);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy of similarly named method in {@link MetricCopyExporter}.
|
||||
*/
|
||||
private boolean isMatch(String name, String[] includes, String[] excludes) {
|
||||
if (ObjectUtils.isEmpty(includes)
|
||||
|| PatternMatchUtils.simpleMatch(includes, name)) {
|
||||
return !PatternMatchUtils.simpleMatch(excludes, name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright 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.stream.metrics;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.actuate.metrics.export.MetricExportProperties;
|
||||
import org.springframework.boot.actuate.metrics.export.TriggerProperties;
|
||||
import org.springframework.boot.bind.RelaxedNames;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.core.env.EnumerablePropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "spring.cloud.stream.metrics")
|
||||
public class ApplicationMetricsProperties
|
||||
implements ApplicationListener<ContextRefreshedEvent> {
|
||||
|
||||
private String prefix = "";
|
||||
|
||||
@Value("${spring.application.name:${vcap.application.name:${spring.config.name:application}}}")
|
||||
private String key;
|
||||
|
||||
@Value("${INSTANCE_INDEX:${CF_INSTANCE_INDEX:0}}")
|
||||
private int instanceIndex;
|
||||
|
||||
private String metricName;
|
||||
|
||||
private String[] properties;
|
||||
|
||||
private MetricExportProperties export;
|
||||
|
||||
public TriggerProperties getTrigger() {
|
||||
return export.findTrigger("application");
|
||||
}
|
||||
|
||||
public ApplicationMetricsProperties(MetricExportProperties export) {
|
||||
this.export = export;
|
||||
}
|
||||
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public void setPrefix(String prefix) {
|
||||
if (!prefix.endsWith(".")) {
|
||||
prefix += ".";
|
||||
}
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public int getInstanceIndex() {
|
||||
return instanceIndex;
|
||||
}
|
||||
|
||||
public void setInstanceIndex(int instanceIndex) {
|
||||
this.instanceIndex = instanceIndex;
|
||||
}
|
||||
|
||||
public String[] getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
public void setProperties(String[] properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of properties that are going to be appended to each message. This gets
|
||||
* populate by onApplicationEvent, once the context refreshes to avoid overhead of
|
||||
* doing per message basis.
|
||||
*/
|
||||
private Map<String, Object> exportProperties = new HashMap<>();
|
||||
|
||||
public Map<String, Object> getExportProperties() {
|
||||
return exportProperties;
|
||||
}
|
||||
|
||||
public String getMetricName() {
|
||||
if (this.metricName == null) {
|
||||
this.metricName = resolveMetricName();
|
||||
}
|
||||
return metricName;
|
||||
}
|
||||
|
||||
private String resolveMetricName() {
|
||||
return this.prefix + this.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all property sources from this application context and copies the
|
||||
* ones listed in {@link ApplicationMetricsProperties} includes.
|
||||
*/
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) event
|
||||
.getSource();
|
||||
if (!ObjectUtils.isEmpty(this.properties)) {
|
||||
for (PropertySource<?> source : ctx.getEnvironment().getPropertySources()) {
|
||||
if (source instanceof EnumerablePropertySource) {
|
||||
EnumerablePropertySource<?> e = (EnumerablePropertySource<?>) source;
|
||||
for (String propertyName : e.getPropertyNames()) {
|
||||
RelaxedNames relaxedNames = new RelaxedNames(propertyName);
|
||||
relaxedLoop: for (String relaxedPropertyName : relaxedNames) {
|
||||
if (isMatch(relaxedPropertyName, this.properties, null)) {
|
||||
this.exportProperties.put(
|
||||
RelaxedPropertiesUtils
|
||||
.findCanonicalFormat(relaxedNames),
|
||||
source.getProperty(propertyName));
|
||||
break relaxedLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMatch(String name, String[] includes, String[] excludes) {
|
||||
if (ObjectUtils.isEmpty(includes)
|
||||
|| PatternMatchUtils.simpleMatch(includes, name)) {
|
||||
return !PatternMatchUtils.simpleMatch(excludes, name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
/*
|
||||
* Copyright 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.stream.metrics;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.actuate.endpoint.MetricsEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.MetricsEndpointMetricReader;
|
||||
import org.springframework.boot.actuate.metrics.Metric;
|
||||
import org.springframework.boot.actuate.metrics.export.MetricCopyExporter;
|
||||
import org.springframework.boot.bind.RelaxedNames;
|
||||
import org.springframework.cloud.stream.config.BindingServiceProperties;
|
||||
import org.springframework.cloud.stream.config.metrics.StreamMetricsProperties;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.core.env.EnumerablePropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.integration.support.MessageBuilder;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.PatternMatchUtils;
|
||||
|
||||
/**
|
||||
*
|
||||
* Component that sends metrics from {@link MetricsEndpointMetricReader} downstream via
|
||||
* the configured metrics channel.
|
||||
*
|
||||
* It uses {@link Scheduled} support to periodially emit messages polled from the
|
||||
* endpoint.
|
||||
*
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
public class BinderMetricsEmitter implements ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware {
|
||||
|
||||
@Autowired
|
||||
private Emitter source;
|
||||
|
||||
@Autowired
|
||||
private StreamMetricsProperties properties;
|
||||
|
||||
@Autowired
|
||||
private BindingServiceProperties bindingServiceProperties;
|
||||
|
||||
private MetricsEndpointMetricReader metricsReader;
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
/**
|
||||
* List of properties that are going to be appended to each message. This gets
|
||||
* populate by onApplicationEvent, once the context refreshes to avoid overhead of
|
||||
* doing per message basis.
|
||||
*/
|
||||
private Map<String, Object> whitelistedProperties;
|
||||
|
||||
public BinderMetricsEmitter(MetricsEndpoint endpoint) {
|
||||
this.metricsReader = new MetricsEndpointMetricReader(endpoint);
|
||||
this.whitelistedProperties = new HashMap<>();
|
||||
}
|
||||
|
||||
@Scheduled(fixedRateString = "${spring.cloud.stream.metrics.delay-millis:5000}")
|
||||
public void sendMetrics() {
|
||||
ApplicationMetrics appMetrics = new ApplicationMetrics(this.properties.getMetricName(),
|
||||
this.bindingServiceProperties.getInstanceIndex(), filter());
|
||||
appMetrics.setProperties(whitelistedProperties);
|
||||
source.metrics().send(MessageBuilder.withPayload(appMetrics).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy of similarly named method in {@link MetricCopyExporter}.
|
||||
*/
|
||||
protected Collection<Metric> filter() {
|
||||
Collection<Metric> result = new ArrayList<>();
|
||||
Iterable<Metric<?>> metrics = metricsReader.findAll();
|
||||
for (Metric metric : metrics) {
|
||||
if (isMatch(metric.getName(), this.properties.getIncludes(), this.properties.getExcludes())) {
|
||||
result.add(metric);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy of similarly named method in {@link MetricCopyExporter}.
|
||||
*/
|
||||
private boolean isMatch(String name, String[] includes, String[] excludes) {
|
||||
if (ObjectUtils.isEmpty(includes) || PatternMatchUtils.simpleMatch(includes, name)) {
|
||||
return !PatternMatchUtils.simpleMatch(excludes, name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates over all property sources from this application context and copies the
|
||||
* ones listed in {@link StreamMetricsProperties} includes.
|
||||
*/
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) event.getSource();
|
||||
if (!ObjectUtils.isEmpty(this.properties.getProperties())) {
|
||||
for (PropertySource source : ctx.getEnvironment().getPropertySources()) {
|
||||
if (source instanceof EnumerablePropertySource) {
|
||||
EnumerablePropertySource e = (EnumerablePropertySource) source;
|
||||
for (String propertyName : e.getPropertyNames()) {
|
||||
RelaxedNames relaxedNames = new RelaxedNames(propertyName);
|
||||
relaxedLoop: for (String relaxedPropertyName : relaxedNames) {
|
||||
if (isMatch(relaxedPropertyName, this.properties.getProperties(), null)) {
|
||||
whitelistedProperties.put(RelaxedPropertiesUtils.findCanonicalFormat(relaxedNames),
|
||||
source.getProperty(propertyName));
|
||||
break relaxedLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
}
|
||||
@@ -27,19 +27,19 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
public class RelaxedPropertiesUtils {
|
||||
class RelaxedPropertiesUtils {
|
||||
|
||||
private static final Pattern HYPHEN_LOWER = Pattern.compile("-_|_-|__|\\.-|\\._");
|
||||
|
||||
private static final Pattern SEPARATED_TO_CAMEL_CASE_PATTERN = Pattern.compile("[_\\-.]");
|
||||
private static final Pattern SEPARATED_TO_CAMEL_CASE_PATTERN = Pattern
|
||||
.compile("[_\\-.]");
|
||||
|
||||
private static final char[] SUFFIXES = new char[] { '_', '-', '.' };
|
||||
|
||||
/**
|
||||
* Searches relaxed names and tries to find a best match for a canonical form using
|
||||
* dot notation.
|
||||
* For example, if a new list was built with JAVA_HOME as property, the return of this
|
||||
* method would be {@code java.home}.
|
||||
* dot notation. For example, if a new list was built with JAVA_HOME as property, the
|
||||
* return of this method would be {@code java.home}.
|
||||
*
|
||||
* Relaxed names generate a long list of variations of a property, it can be tricky
|
||||
* trying to infer the correct format, which sometimes may not even exist in dot
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.config.metrics;
|
||||
package org.springframework.cloud.stream.metrics.config;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.MetricsEndpoint;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
@@ -22,32 +22,31 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.stream.annotation.EnableBinding;
|
||||
import org.springframework.cloud.stream.binder.Binder;
|
||||
import org.springframework.cloud.stream.metrics.BinderMetricsEmitter;
|
||||
import org.springframework.cloud.stream.metrics.BootMetricJsonSerializer;
|
||||
import org.springframework.cloud.stream.metrics.Emitter;
|
||||
import org.springframework.cloud.stream.metrics.ApplicationMetricsExporter;
|
||||
import org.springframework.cloud.stream.metrics.ApplicationMetricsProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
@Configuration
|
||||
@ConditionalOnClass(Binder.class)
|
||||
@EnableScheduling
|
||||
@EnableBinding(Emitter.class)
|
||||
@EnableConfigurationProperties(StreamMetricsProperties.class)
|
||||
@ConditionalOnProperty("spring.cloud.stream.bindings." + Emitter.METRICS_CHANNEL_NAME + ".destination")
|
||||
@EnableConfigurationProperties(ApplicationMetricsProperties.class)
|
||||
@ConditionalOnProperty("spring.cloud.stream.bindings." + Emitter.METRICS_CHANNEL_NAME
|
||||
+ ".destination")
|
||||
public class BinderMetricsAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public BinderMetricsEmitter binderMetricsExporter(MetricsEndpoint endpoint) {
|
||||
return new BinderMetricsEmitter(endpoint);
|
||||
public ApplicationMetricsExporter aggregateMetricsExporter(MetricsEndpoint endpoint,
|
||||
Emitter emitter, ApplicationMetricsProperties properties) {
|
||||
return new ApplicationMetricsExporter(endpoint, emitter.metrics(), properties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BootMetricJsonSerializer metricJsonSerializer() {
|
||||
return new BootMetricJsonSerializer();
|
||||
public MetricJsonSerializer metricJsonSerializer() {
|
||||
return new MetricJsonSerializer();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.config.metrics;
|
||||
package org.springframework.cloud.stream.metrics.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -29,10 +29,15 @@ import org.springframework.core.env.MapPropertySource;
|
||||
*/
|
||||
public class BinderMetricsEnvironmentPostProcessor implements EnvironmentPostProcessor {
|
||||
@Override
|
||||
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
|
||||
public void postProcessEnvironment(ConfigurableEnvironment environment,
|
||||
SpringApplication application) {
|
||||
Map<String, Object> propertiesToAdd = new HashMap<>();
|
||||
propertiesToAdd.put("spring.cloud.stream.bindings.streamMetrics.contentType", "application/json");
|
||||
environment.getPropertySources()
|
||||
.addLast(new MapPropertySource("binderMetricsDefaultProperties", propertiesToAdd));
|
||||
propertiesToAdd.put("spring.cloud.stream.bindings.streamMetrics.contentType",
|
||||
"application/json");
|
||||
propertiesToAdd.put("spring.cloud.stream.metrics.instanceIndex",
|
||||
"${spring.cloud.stream.instanceIndex:${INSTANCE_INDEX:${CF_INSTANCE_INDEX:0}}}");
|
||||
propertiesToAdd.put("spring.metrics.export.includes", "integration**");
|
||||
environment.getPropertySources().addLast(
|
||||
new MapPropertySource("binderMetricsDefaultProperties", propertiesToAdd));
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.metrics;
|
||||
package org.springframework.cloud.stream.metrics.config;
|
||||
|
||||
import org.springframework.cloud.stream.annotation.Output;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.stream.metrics;
|
||||
package org.springframework.cloud.stream.metrics.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.DateFormat;
|
||||
@@ -38,15 +38,15 @@ import org.springframework.boot.jackson.JsonComponent;
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
@JsonComponent
|
||||
public class BootMetricJsonSerializer {
|
||||
public class MetricJsonSerializer {
|
||||
|
||||
final static DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
|
||||
|
||||
public static class Serializer extends JsonSerializer<Metric> {
|
||||
public static class Serializer extends JsonSerializer<Metric<?>> {
|
||||
|
||||
@Override
|
||||
public void serialize(Metric metric, JsonGenerator json, SerializerProvider serializerProvider)
|
||||
throws IOException {
|
||||
public void serialize(Metric<?> metric, JsonGenerator json,
|
||||
SerializerProvider serializerProvider) throws IOException {
|
||||
json.writeStartObject();
|
||||
json.writeStringField("name", metric.getName());
|
||||
json.writeNumberField("value", metric.getValue().doubleValue());
|
||||
@@ -55,10 +55,10 @@ public class BootMetricJsonSerializer {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Deserializer extends JsonDeserializer<Metric> {
|
||||
public static class Deserializer extends JsonDeserializer<Metric<?>> {
|
||||
|
||||
@Override
|
||||
public Metric deserialize(JsonParser p, DeserializationContext ctxt)
|
||||
public Metric<?> deserialize(JsonParser p, DeserializationContext ctxt)
|
||||
throws IOException, JsonProcessingException {
|
||||
JsonNode node = p.getCodec().readTree(p);
|
||||
String name = node.get("name").asText();
|
||||
@@ -70,7 +70,7 @@ public class BootMetricJsonSerializer {
|
||||
}
|
||||
catch (ParseException e) {
|
||||
}
|
||||
Metric<Number> metric = new Metric(name, value, timestamp);
|
||||
Metric<Number> metric = new Metric<Number>(name, value, timestamp);
|
||||
|
||||
return metric;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.stream.config.metrics.BinderMetricsAutoConfiguration
|
||||
org.springframework.cloud.stream.metrics.config.BinderMetricsAutoConfiguration
|
||||
org.springframework.boot.env.EnvironmentPostProcessor=\
|
||||
org.springframework.cloud.stream.config.metrics.BinderMetricsEnvironmentPostProcessor
|
||||
org.springframework.cloud.stream.metrics.config.BinderMetricsEnvironmentPostProcessor
|
||||
@@ -20,6 +20,7 @@ import java.util.Collection;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Assert;
|
||||
import org.junit.BeforeClass;
|
||||
@@ -29,6 +30,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.actuate.metrics.Metric;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.cloud.stream.metrics.config.Emitter;
|
||||
import org.springframework.cloud.stream.test.binder.MessageCollector;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.messaging.Message;
|
||||
@@ -37,7 +39,7 @@ import org.springframework.util.CollectionUtils;
|
||||
/**
|
||||
* @author Vinicius Carvalho
|
||||
*/
|
||||
public class BinderMetricsEmitterTests {
|
||||
public class ApplicationMetricsExporterTests {
|
||||
|
||||
@BeforeClass
|
||||
public static void setSystemProps() {
|
||||
@@ -51,8 +53,9 @@ public class BinderMetricsEmitterTests {
|
||||
|
||||
@Test(expected = NoSuchBeanDefinitionException.class)
|
||||
public void checkDisabledConfiguration() throws Exception {
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(BinderExporterApplication.class,
|
||||
"--server.port=0", "--spring.jmx.enabled=false", "--spring.cloud.stream.metrics.delay-millis=500");
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(
|
||||
BinderExporterApplication.class, "--server.port=0",
|
||||
"--spring.jmx.enabled=false");
|
||||
try {
|
||||
applicationContext.getBean(Emitter.class);
|
||||
}
|
||||
@@ -67,17 +70,20 @@ public class BinderMetricsEmitterTests {
|
||||
|
||||
@Test
|
||||
public void defaultIncludes() throws Exception {
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(BinderExporterApplication.class,
|
||||
"--server.port=0", "--spring.jmx.enabled=false", "--spring.cloud.stream.metrics.delay-millis=500",
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(
|
||||
BinderExporterApplication.class, "--server.port=0",
|
||||
"--spring.jmx.enabled=false", "--spring.metrics.export.delay-millis=500",
|
||||
"--spring.cloud.stream.bindings.streamMetrics.destination=foo");
|
||||
Emitter emitterSource = applicationContext.getBean(Emitter.class);
|
||||
MessageCollector collector = applicationContext.getBean(MessageCollector.class);
|
||||
Message message = collector.forChannel(emitterSource.metrics()).poll(1000, TimeUnit.MILLISECONDS);
|
||||
Message<?> message = collector.forChannel(emitterSource.metrics()).poll(1000,
|
||||
TimeUnit.MILLISECONDS);
|
||||
Assert.assertNotNull(message);
|
||||
ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class);
|
||||
ApplicationMetrics applicationMetrics = mapper.readValue((String) message.getPayload(),
|
||||
ApplicationMetrics.class);
|
||||
Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean", applicationMetrics.getMetrics()));
|
||||
ApplicationMetrics applicationMetrics = mapper
|
||||
.readValue((String) message.getPayload(), ApplicationMetrics.class);
|
||||
Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean",
|
||||
applicationMetrics.getMetrics()));
|
||||
Assert.assertFalse(contains("mem", applicationMetrics.getMetrics()));
|
||||
Assert.assertEquals("application", applicationMetrics.getName());
|
||||
Assert.assertEquals(0, applicationMetrics.getInstanceIndex());
|
||||
@@ -87,18 +93,21 @@ public class BinderMetricsEmitterTests {
|
||||
|
||||
@Test
|
||||
public void customAppNameAndIndex() throws Exception {
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(BinderExporterApplication.class,
|
||||
"--server.port=0", "--spring.jmx.enabled=false", "--spring.cloud.stream.metrics.delay-millis=500",
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(
|
||||
BinderExporterApplication.class, "--server.port=0",
|
||||
"--spring.jmx.enabled=false", "--spring.metrics.export.delay-millis=500",
|
||||
"--spring.application.name=foo", "--spring.cloud.stream.instanceIndex=1",
|
||||
"--spring.cloud.stream.bindings.streamMetrics.destination=foo");
|
||||
Emitter emitterSource = applicationContext.getBean(Emitter.class);
|
||||
MessageCollector collector = applicationContext.getBean(MessageCollector.class);
|
||||
Message message = collector.forChannel(emitterSource.metrics()).poll(1000, TimeUnit.MILLISECONDS);
|
||||
Message<?> message = collector.forChannel(emitterSource.metrics()).poll(1000,
|
||||
TimeUnit.MILLISECONDS);
|
||||
Assert.assertNotNull(message);
|
||||
ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class);
|
||||
ApplicationMetrics applicationMetrics = mapper.readValue((String) message.getPayload(),
|
||||
ApplicationMetrics.class);
|
||||
Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean", applicationMetrics.getMetrics()));
|
||||
ApplicationMetrics applicationMetrics = mapper
|
||||
.readValue((String) message.getPayload(), ApplicationMetrics.class);
|
||||
Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean",
|
||||
applicationMetrics.getMetrics()));
|
||||
Assert.assertFalse(contains("mem", applicationMetrics.getMetrics()));
|
||||
Assert.assertEquals("foo", applicationMetrics.getName());
|
||||
Assert.assertEquals(1, applicationMetrics.getInstanceIndex());
|
||||
@@ -108,18 +117,22 @@ public class BinderMetricsEmitterTests {
|
||||
|
||||
@Test
|
||||
public void usingPrefix() throws Exception {
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(BinderExporterApplication.class,
|
||||
"--server.port=0", "--spring.jmx.enabled=false", "--spring.cloud.stream.metrics.delay-millis=500",
|
||||
"--spring.cloud.stream.metrics.prefix=foo", "--spring.cloud.stream.instanceIndex=1",
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(
|
||||
BinderExporterApplication.class, "--server.port=0",
|
||||
"--spring.jmx.enabled=false", "--spring.metrics.export.delay-millis=500",
|
||||
"--spring.cloud.stream.metrics.prefix=foo",
|
||||
"--spring.cloud.stream.instanceIndex=1",
|
||||
"--spring.cloud.stream.bindings.streamMetrics.destination=foo");
|
||||
Emitter emitterSource = applicationContext.getBean(Emitter.class);
|
||||
MessageCollector collector = applicationContext.getBean(MessageCollector.class);
|
||||
Message message = collector.forChannel(emitterSource.metrics()).poll(1000, TimeUnit.MILLISECONDS);
|
||||
Message<?> message = collector.forChannel(emitterSource.metrics()).poll(1000,
|
||||
TimeUnit.MILLISECONDS);
|
||||
Assert.assertNotNull(message);
|
||||
ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class);
|
||||
ApplicationMetrics applicationMetrics = mapper.readValue((String) message.getPayload(),
|
||||
ApplicationMetrics.class);
|
||||
Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean", applicationMetrics.getMetrics()));
|
||||
ApplicationMetrics applicationMetrics = mapper
|
||||
.readValue((String) message.getPayload(), ApplicationMetrics.class);
|
||||
Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean",
|
||||
applicationMetrics.getMetrics()));
|
||||
Assert.assertFalse(contains("mem", applicationMetrics.getMetrics()));
|
||||
Assert.assertEquals("foo.application", applicationMetrics.getName());
|
||||
Assert.assertEquals(1, applicationMetrics.getInstanceIndex());
|
||||
@@ -129,19 +142,22 @@ public class BinderMetricsEmitterTests {
|
||||
|
||||
@Test
|
||||
public void includesExcludes() throws Exception {
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(BinderExporterApplication.class,
|
||||
"--server.port=0", "--spring.jmx.enabled=false", "--spring.cloud.stream.metrics.delay-millis=500",
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(
|
||||
BinderExporterApplication.class, "--server.port=0",
|
||||
"--spring.jmx.enabled=false", "--spring.metrics.export.delay-millis=500",
|
||||
"--spring.cloud.stream.bindings.streamMetrics.destination=foo",
|
||||
"--spring.cloud.stream.metrics.includes=mem**", "--spring.cloud.stream.metrics.excludes=integration**");
|
||||
"--spring.metrics.export.includes=mem**",
|
||||
"--spring.metrics.export.excludes=integration**");
|
||||
Emitter emitterSource = applicationContext.getBean(Emitter.class);
|
||||
MessageCollector collector = applicationContext.getBean(MessageCollector.class);
|
||||
Message message = collector.forChannel(emitterSource.metrics()).poll(1000, TimeUnit.MILLISECONDS);
|
||||
Message<?> message = collector.forChannel(emitterSource.metrics()).poll(1000,
|
||||
TimeUnit.MILLISECONDS);
|
||||
Assert.assertNotNull(message);
|
||||
ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class);
|
||||
ApplicationMetrics applicationMetrics = mapper.readValue((String) message.getPayload(),
|
||||
ApplicationMetrics.class);
|
||||
Assert.assertFalse(
|
||||
contains("integration.channel.errorChannel.errorRate.mean", applicationMetrics.getMetrics()));
|
||||
ApplicationMetrics applicationMetrics = mapper
|
||||
.readValue((String) message.getPayload(), ApplicationMetrics.class);
|
||||
Assert.assertFalse(contains("integration.channel.errorChannel.errorRate.mean",
|
||||
applicationMetrics.getMetrics()));
|
||||
Assert.assertTrue(contains("mem", applicationMetrics.getMetrics()));
|
||||
Assert.assertTrue(CollectionUtils.isEmpty(applicationMetrics.getProperties()));
|
||||
applicationContext.close();
|
||||
@@ -149,50 +165,57 @@ public class BinderMetricsEmitterTests {
|
||||
|
||||
@Test
|
||||
public void includesExcludesWithProperties() throws Exception {
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(BinderExporterApplication.class,
|
||||
"--server.port=0", "--spring.jmx.enabled=false", "--spring.cloud.stream.metrics.delay-millis=500",
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(
|
||||
BinderExporterApplication.class, "--server.port=0",
|
||||
"--spring.jmx.enabled=false", "--spring.metrics.export.delay-millis=500",
|
||||
"--spring.cloud.stream.bindings.streamMetrics.destination=foo",
|
||||
"--spring.cloud.stream.metrics.includes=integration**",
|
||||
"--spring.metrics.export.includes=integration**",
|
||||
"--spring.cloud.stream.metrics.properties=java**,spring.test.env**");
|
||||
Emitter emitterSource = applicationContext.getBean(Emitter.class);
|
||||
MessageCollector collector = applicationContext.getBean(MessageCollector.class);
|
||||
Message message = collector.forChannel(emitterSource.metrics()).poll(1000, TimeUnit.MILLISECONDS);
|
||||
Message<?> message = collector.forChannel(emitterSource.metrics()).poll(1000,
|
||||
TimeUnit.MILLISECONDS);
|
||||
Assert.assertNotNull(message);
|
||||
ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class);
|
||||
ApplicationMetrics applicationMetrics = mapper.readValue((String) message.getPayload(),
|
||||
ApplicationMetrics.class);
|
||||
ApplicationMetrics applicationMetrics = mapper
|
||||
.readValue((String) message.getPayload(), ApplicationMetrics.class);
|
||||
Assert.assertFalse(contains("mem", applicationMetrics.getMetrics()));
|
||||
Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean", applicationMetrics.getMetrics()));
|
||||
Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean",
|
||||
applicationMetrics.getMetrics()));
|
||||
Assert.assertFalse(CollectionUtils.isEmpty(applicationMetrics.getProperties()));
|
||||
Assert.assertTrue(applicationMetrics.getProperties().get("spring.test.env.syntax").equals("testing"));
|
||||
Assert.assertTrue(applicationMetrics.getProperties().get("spring.test.env.syntax")
|
||||
.equals("testing"));
|
||||
applicationContext.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void overrideAppName() throws Exception {
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(BinderExporterApplication.class,
|
||||
"--server.port=0", "--spring.jmx.enabled=false", "--spring.cloud.stream.metrics.delay-millis=500",
|
||||
"--spring.application.name=foo", "--spring.cloud.stream.instanceIndex=1",
|
||||
"--spring.cloud.stream.bindings.streamMetrics.destination=foo",
|
||||
"--spring.cloud.stream.metrics.key=foobarfoo");
|
||||
Emitter emitterSource = applicationContext.getBean(Emitter.class);
|
||||
MessageCollector collector = applicationContext.getBean(MessageCollector.class);
|
||||
Message message = collector.forChannel(emitterSource.metrics()).poll(1000, TimeUnit.MILLISECONDS);
|
||||
Assert.assertNotNull(message);
|
||||
ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class);
|
||||
ApplicationMetrics applicationMetrics = mapper.readValue((String) message.getPayload(),
|
||||
ApplicationMetrics.class);
|
||||
Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean", applicationMetrics.getMetrics()));
|
||||
Assert.assertFalse(contains("mem", applicationMetrics.getMetrics()));
|
||||
Assert.assertEquals("foobarfoo", applicationMetrics.getName());
|
||||
Assert.assertEquals(1, applicationMetrics.getInstanceIndex());
|
||||
Assert.assertTrue(CollectionUtils.isEmpty(applicationMetrics.getProperties()));
|
||||
applicationContext.close();
|
||||
}
|
||||
@Test
|
||||
public void overrideAppName() throws Exception {
|
||||
ConfigurableApplicationContext applicationContext = SpringApplication.run(
|
||||
BinderExporterApplication.class, "--server.port=0",
|
||||
"--spring.jmx.enabled=false", "--spring.metrics.export.delay-millis=500",
|
||||
"--spring.application.name=foo", "--spring.cloud.stream.instanceIndex=1",
|
||||
"--spring.cloud.stream.bindings.streamMetrics.destination=foo",
|
||||
"--spring.cloud.stream.metrics.key=foobarfoo");
|
||||
Emitter emitterSource = applicationContext.getBean(Emitter.class);
|
||||
MessageCollector collector = applicationContext.getBean(MessageCollector.class);
|
||||
Message<?> message = collector.forChannel(emitterSource.metrics()).poll(1000,
|
||||
TimeUnit.MILLISECONDS);
|
||||
Assert.assertNotNull(message);
|
||||
ObjectMapper mapper = applicationContext.getBean(ObjectMapper.class);
|
||||
ApplicationMetrics applicationMetrics = mapper
|
||||
.readValue((String) message.getPayload(), ApplicationMetrics.class);
|
||||
Assert.assertTrue(contains("integration.channel.errorChannel.errorRate.mean",
|
||||
applicationMetrics.getMetrics()));
|
||||
Assert.assertFalse(contains("mem", applicationMetrics.getMetrics()));
|
||||
Assert.assertEquals("foobarfoo", applicationMetrics.getName());
|
||||
Assert.assertEquals(1, applicationMetrics.getInstanceIndex());
|
||||
Assert.assertTrue(CollectionUtils.isEmpty(applicationMetrics.getProperties()));
|
||||
applicationContext.close();
|
||||
}
|
||||
|
||||
private boolean contains(String metric, Collection<Metric> metrics) {
|
||||
private boolean contains(String metric, Collection<Metric<?>> metrics) {
|
||||
boolean contains = false;
|
||||
for (Metric entry : metrics) {
|
||||
for (Metric<?> entry : metrics) {
|
||||
contains = entry.getName().equals(metric);
|
||||
if (contains) {
|
||||
break;
|
||||
Reference in New Issue
Block a user