Add samples and tweak metrics reader/writers till they work

This commit is contained in:
Dave Syer 2015-05-05 11:50:37 +01:00 committed by Andy Wilkinson
parent 60a4943520
commit 3fda744522
12 changed files with 191 additions and 39 deletions

View File

@ -175,6 +175,9 @@ public class MetricRepositoryAutoConfiguration {
@Autowired(required = false)
private List<MetricWriter> writers;
@Autowired
private MetricsProperties metrics;
@Autowired(required = false)
@Qualifier("actuatorMetricRepository")
private MetricWriter actuatorMetricRepository;
@ -188,13 +191,20 @@ public class MetricRepositoryAutoConfiguration {
&& writers.contains(this.actuatorMetricRepository)) {
writers.remove(this.actuatorMetricRepository);
}
return new MetricCopyExporter(reader, new CompositeMetricWriter(writers)) {
MetricCopyExporter exporter = new MetricCopyExporter(reader,
new CompositeMetricWriter(writers)) {
@Scheduled(fixedDelayString = "${spring.metrics.export.delayMillis:5000}")
@Override
public void export() {
super.export();
}
};
if (this.metrics.getExport().getIncludes() != null
|| this.metrics.getExport().getExcludes() != null) {
exporter.setIncludes(this.metrics.getExport().getIncludes());
exporter.setExcludes(this.metrics.getExport().getExcludes());
}
return exporter;
}
}

View File

@ -43,11 +43,31 @@ public class MetricsProperties {
private boolean enabled = true;
/**
* Flag to switch off an optimization based on not exporting unchanged metric
* values.
* Flag to switch off any available optimizations based on not exporting unchanged
* metric values.
*/
private boolean ignoreTimestamps = false;
private String[] includes;
private String[] excludes;
public String[] getIncludes() {
return this.includes;
}
public void setIncludes(String[] includes) {
this.includes = includes;
}
public String[] getExcludes() {
return this.excludes;
}
public void setExcludes(String[] excludes) {
this.excludes = excludes;
}
public boolean isEnabled() {
return this.enabled;
}

View File

@ -115,7 +115,7 @@ public class AggregateMetricReader implements MetricReader {
if (aggregate == null) {
aggregate = new Metric<Number>(name, metric.getValue(), metric.getTimestamp());
}
else if (key.startsWith("counter")) {
else if (key.contains("counter.")) {
// accumulate all values
aggregate = new Metric<Number>(name, metric.increment(
aggregate.getValue().intValue()).getValue(), metric.getTimestamp());

View File

@ -46,11 +46,15 @@ public class MetricCopyExporter extends AbstractMetricExporter {
}
public void setIncludes(String... includes) {
this.includes = includes;
if (includes != null) {
this.includes = includes;
}
}
public void setExcludes(String... excludes) {
this.excludes = excludes;
if (excludes != null) {
this.excludes = excludes;
}
}
public MetricCopyExporter(MetricReader reader, MetricWriter writer, String prefix) {
@ -61,7 +65,8 @@ public class MetricCopyExporter extends AbstractMetricExporter {
@Override
protected Iterable<Metric<?>> next(String group) {
if (this.includes.length == 0 && this.excludes.length == 0) {
if ((this.includes != null || this.includes.length == 0)
&& (this.excludes != null || this.excludes.length == 0)) {
return this.reader.findAll();
}
return new Iterable<Metric<?>>() {
@ -108,7 +113,8 @@ public class MetricCopyExporter extends AbstractMetricExporter {
boolean matched = false;
while (this.iterator.hasNext() && !matched) {
metric = this.iterator.next();
if (MetricCopyExporter.this.includes.length == 0) {
if (MetricCopyExporter.this.includes == null
|| MetricCopyExporter.this.includes.length == 0) {
matched = true;
}
else {
@ -119,10 +125,12 @@ public class MetricCopyExporter extends AbstractMetricExporter {
}
}
}
for (String pattern : MetricCopyExporter.this.excludes) {
if (PatternMatchUtils.simpleMatch(pattern, metric.getName())) {
matched = false;
break;
if (MetricCopyExporter.this.excludes != null) {
for (String pattern : MetricCopyExporter.this.excludes) {
if (PatternMatchUtils.simpleMatch(pattern, metric.getName())) {
matched = false;
break;
}
}
}
}

View File

@ -24,18 +24,22 @@ import org.springframework.util.ObjectUtils;
/**
* A naming strategy that just passes through the metric name, together with tags from a
* set of static values. Open TSDB requires at least one tag, so one is always added for
* you: the {@value #PREFIX_KEY} key is added with a unique value "spring.X" where X is an
* object hash code ID for this (the naming stategy). In most cases this will be unique
* enough to allow aggregation of the underlying metrics in Open TSDB, but normally it is
* best to provide your own tags, including a prefix if you know one (overwriting the
* default).
* set of static values. Open TSDB requires at least one tag, so tags are always added for
* you: the {@value #DOMAIN_KEY} key is added with a value "spring", and the
* {@value #PROCESS_KEY} key is added with a value equal to the object hash of "this" (the
* naming strategy). The "domain" value is a system identifier - it would be common to all
* processes in the same distributed system. In most cases this will be unique enough to
* allow aggregation of the underlying metrics in Open TSDB, but normally it is best to
* provide your own tags, including a prefix and process identifier if you know one
* (overwriting the default).
*
* @author Dave Syer
*/
public class DefaultOpenTsdbNamingStrategy implements OpenTsdbNamingStrategy {
public static final String PREFIX_KEY = "prefix";
public static final String DOMAIN_KEY = "domain";
public static final String PROCESS_KEY = "process";
/**
* Tags to apply to every metric. Open TSDB requires at least one tag, so a "prefix"
@ -46,8 +50,8 @@ public class DefaultOpenTsdbNamingStrategy implements OpenTsdbNamingStrategy {
private Map<String, OpenTsdbName> cache = new HashMap<String, OpenTsdbName>();
public DefaultOpenTsdbNamingStrategy() {
this.tags.put(PREFIX_KEY,
"spring." + ObjectUtils.getIdentityHexString(this));
this.tags.put(DOMAIN_KEY, "org.springframework.metrics");
this.tags.put(PROCESS_KEY, ObjectUtils.getIdentityHexString(this));
}
public void setTags(Map<String, String> staticTags) {

View File

@ -29,6 +29,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundZSetOperations;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A {@link MetricRepository} implementation for a redis backend. Metric values are stored
@ -92,6 +93,8 @@ public class RedisMetricRepository implements MetricRepository {
public RedisMetricRepository(RedisConnectionFactory redisConnectionFactory,
String prefix, String key) {
Assert.notNull(redisConnectionFactory, "RedisConnectionFactory must not be null");
Assert.state(StringUtils.hasText(prefix), "Prefix must be non-empty");
Assert.state(StringUtils.hasText(key), "Key must be non-empty");
this.redisOperations = RedisUtils.stringTemplate(redisConnectionFactory);
if (!prefix.endsWith(".")) {
prefix = prefix + ".";

View File

@ -943,10 +943,10 @@ Example:
[source,java,indent=0]
----
@Value("${spring.application.name:application}.${random.value:0000}")
private String prefix = "metrics";
@Value("metrics.mysystem.${random.value:0000}.${spring.application.name:application}")
private String prefix = "metrics.mysystem";
@Value("${metrics.key:METRICSKEY}")
@Value("${metrics.key:keys.mysystem}")
private String key = "METRICSKEY";
@Bean
@ -955,35 +955,50 @@ MetricWriter metricWriter() {
}
----
The prefix is constructed with the application name at the end, so it can easily be used
to identify a group of processes with the same logical name later.
NOTE: it's important to set both the key and the prefix. The key is used for all
repository operations, and can be shared by multiple repositories. If multiple
repositories share a key (like in the case where you need to aggregate across them), then
you normally have a read-only "master" repository that has a short, but identifiable,
prefix (like "metrics.mysystem"), and many write-only repositories with prefixes that
start with the master prefix (like `metrics.mysystem.*` in the example above). It is
efficient to read all the keys from a "master" repository like that, but inefficient to
read a subset with a longer prefix (e.g. using one of the writing repositories).
[[production-ready-metric-writers-export-to-open-tdsb]]
==== Example: Export to Open TSDB
If you provide a `@Bean` of type `OpenTsdbHttpMetricWriter` the metrics are exported to
http://opentsdb.net/[Open TSDB] for aggregation. The `OpenTsdbHttpMetricWriter` has a
`url` property that you need to set to the Open TSDB "/put" endpoint, e.g.
`http://localhost:4242/api/put`). It also has a `namingStrategy` that you can customize
or configure to make the metrics match the data structure you need on the server. By
default it just passes through the metric name as an Open TSDB metric name and adds a tag
"prefix" with value "spring.X" where "X" is the object hash of the default naming
strategy. Thus, after running the application and generating some metrics (e.g. by pinging
the home page) you can inspect the metrics in the TDB UI (http://localhost:4242 by
default). Example:
If you provide a `@Bean` of type `OpenTsdbHttpMetricWriter` the metrics are exported to
http://opentsdb.net/[Open TSDB] for aggregation. The `OpenTsdbHttpMetricWriter` has a
`url` property that you need to set to the Open TSDB "/put" endpoint, e.g.
`http://localhost:4242/api/put`). It also has a `namingStrategy` that you can customize or
configure to make the metrics match the data structure you need on the server. By default
it just passes through the metric name as an Open TSDB metric name and adds a tag "domain"
with value "org.springframework.metrics" and another tag "process" with value equals to
the object hash of the naming strategy. Thus, after running the application and generating
some metrics (e.g. by pinging the home page) you can inspect the metrics in the TDB UI
(http://localhost:4242 by default). Example:
[source,indent=0]
----
curl localhost:4242/api/query?start=1h-ago&m=max:counter.status.200.root
[
{
"metric": "counter.status.200.root",
"tags": {
"prefix": "spring.b968a76"
"domain": "org.springframework.metrics",
"process": "b968a76"
},
"aggregateTags": [],
"dps": {
"1430492872": 2,
"1430492875": 6
}
}
}
]
----
@ -1057,7 +1072,7 @@ results to the "/metrics" endpoint. Example:
@Bean
protected MetricReader repository(RedisConnectionFactory connectionFactory) {
RedisMetricRepository repository = new RedisMetricRepository(connectionFactory,
"metrics", "METRICSKEY");
"mysystem", "myorg.keys");
return repository;
}

View File

@ -1 +1,2 @@
service.name: Phil
service.name: Phil
metrics.names.tags.process: ${spring.application.name:application}:${random.value:0000}

View File

@ -22,3 +22,25 @@ the result in Redis, e.g.
2) "4"
----
There is also an `AggregateMetricReader` with public metrics in the application context,
and you can see the result in the "/metrics" (metrics with names in "aggregate.*").
The way the Redis repository was set up (with a random key in the metric names) makes the
aggregates work across restarts of the same application, or across a scaled up application
running in multiple processes. E.g.
[source,indent=0]
----
$ curl localhost:8080/metrics
{
...
"aggregate.application.counter.status.200.metrics": 12,
"aggregate.application.counter.status.200.root": 29,
"aggregate.application.gauge.response.metrics": 43,
"aggregate.application.gauge.response.root": 5,
"counter.status.200.root": 2,
"counter.status.200.metrics": 1,
"gauge.response.metrics": 43,
"gauge.response.root": 5
}
----

View File

@ -0,0 +1,56 @@
/*
* Copyright 2012-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 sample.metrics.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.MetricReaderPublicMetrics;
import org.springframework.boot.actuate.endpoint.PublicMetrics;
import org.springframework.boot.actuate.metrics.aggregate.AggregateMetricReader;
import org.springframework.boot.actuate.metrics.reader.MetricReader;
import org.springframework.boot.actuate.metrics.repository.redis.RedisMetricRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
/**
* @author Dave Syer
*/
@Configuration
public class AggregateMetricsConfiguration {
@Autowired
private ExportProperties export;
@Autowired
private RedisConnectionFactory connectionFactory;
@Bean
public PublicMetrics metricsAggregate() {
return new MetricReaderPublicMetrics(aggregatesMetricReader());
}
private MetricReader globalMetricsForAggregation() {
return new RedisMetricRepository(this.connectionFactory,
this.export.getAggregatePrefix(), this.export.getKey());
}
private MetricReader aggregatesMetricReader() {
AggregateMetricReader repository = new AggregateMetricReader(
globalMetricsForAggregation());
return repository;
}
}

View File

@ -17,6 +17,7 @@
package sample.metrics.redis;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
@ConfigurationProperties("metrics.export")
class ExportProperties {
@ -40,4 +41,15 @@ class ExportProperties {
this.key = key;
}
public String getAggregatePrefix() {
String[] tokens = StringUtils.delimitedListToStringArray(this.prefix, ".");
if (tokens.length > 1) {
if (StringUtils.hasText(tokens[1])) {
// If the prefix has 2 or more non-trivial parts, use the first 1
return tokens[0];
}
}
return this.prefix;
}
}

View File

@ -1,2 +1,3 @@
service.name: Phil
metrics.export.prefix: ${spring.application.name:application}:${random.value:0000}
metrics.export.prefix: metrics.sample.${random.value:0000}.${spring.application.name:application}
metrics.export.key: keys.metrics.sample