Add redis properties as convenience in spring.metrics.export

The redis export and aggregate use case is a lot nicer with this
shared data between the two component types.

Also made MetricExportProperties itself a Trigger (so the default
delay etc. can be configured via spring.metrics.export.*).
This commit is contained in:
Dave Syer 2015-05-31 12:17:11 +01:00
parent e8e89f3738
commit 80ff92919c
7 changed files with 241 additions and 237 deletions

View File

@ -16,30 +16,31 @@
package org.springframework.boot.actuate.metrics.export;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.PatternMatchUtils;
import org.springframework.util.StringUtils;
/**
* @author Dave Syer
*/
@ConfigurationProperties("spring.metrics.export")
public class MetricExportProperties {
public class MetricExportProperties extends Trigger {
/**
* Flag to disable all metric exports (assuming any MetricWriters are available).
*/
private boolean enabled = true;
private Export export = new Export();
private Map<String, SpecificTrigger> triggers = new LinkedHashMap<String, SpecificTrigger>();
private Map<String, Export> writers = new LinkedHashMap<String, Export>();
private Redis redis = new Redis();
/**
* Default values for trigger configuration for all writers. Can also be set by
@ -47,8 +48,8 @@ public class MetricExportProperties {
*
* @return the default trigger configuration
*/
public Export getDefault() {
return this.export;
public Trigger getDefault() {
return this;
}
/**
@ -58,30 +59,27 @@ public class MetricExportProperties {
*
* @return the writers
*/
public Map<String, Export> getWriters() {
return this.writers;
public Map<String, SpecificTrigger> getTriggers() {
return this.triggers;
}
public Redis getRedis() {
return redis;
}
public void setRedis(Redis redis) {
this.redis = redis;
}
@PostConstruct
public void setUpDefaults() {
Export defaults = null;
for (Entry<String, Export> entry : this.writers.entrySet()) {
Trigger defaults = this;
for (Entry<String, SpecificTrigger> entry : this.triggers.entrySet()) {
String key = entry.getKey();
Export value = entry.getValue();
SpecificTrigger value = entry.getValue();
if (value.getNames() == null || value.getNames().length == 0) {
value.setNames(new String[] { key });
}
if (Arrays.asList(value.getNames()).contains("*")) {
defaults = value;
}
}
if (defaults == null) {
this.export.setNames(new String[] { "*" });
this.writers.put("*", this.export);
defaults = this.export;
}
if (defaults.isIgnoreTimestamps() == null) {
defaults.setIgnoreTimestamps(false);
}
if (defaults.isSendLatest() == null) {
defaults.setSendLatest(true);
@ -89,10 +87,7 @@ public class MetricExportProperties {
if (defaults.getDelayMillis() == null) {
defaults.setDelayMillis(5000);
}
for (Export value : this.writers.values()) {
if (value.isIgnoreTimestamps() == null) {
value.setIgnoreTimestamps(defaults.isIgnoreTimestamps());
}
for (Trigger value : this.triggers.values()) {
if (value.isSendLatest() == null) {
value.setSendLatest(defaults.isSendLatest());
}
@ -110,37 +105,29 @@ public class MetricExportProperties {
this.enabled = enabled;
}
public static class Export {
/**
* Find a matching trigger configuration.
* @param name the bean name to match
* @return a matching configuration if there is one
*/
public Trigger findTrigger(String name) {
for (SpecificTrigger value : this.triggers.values()) {
if (PatternMatchUtils.simpleMatch(value.getNames(), name)) {
return value;
}
}
return this;
}
/**
* Trigger for specific bean names.
*/
public static class SpecificTrigger extends Trigger {
/**
* Names (or patterns) for bean names that this configuration applies to.
*/
private String[] names;
/**
* Delay in milliseconds between export ticks. Metrics are exported to external
* sources on a schedule with this delay.
*/
private Long delayMillis;
/**
* Flag to enable metric export (assuming a MetricWriter is available).
*/
private boolean enabled = true;
/**
* Flag to switch off any available optimizations based on not exporting unchanged
* metric values.
*/
private Boolean sendLatest;
/**
* Flag to ignore timestamps completely. If true, send all metrics all the time,
* including ones that haven't changed since startup.
*/
private Boolean ignoreTimestamps;
private String[] includes;
private String[] excludes;
public String[] getNames() {
return this.names;
@ -150,66 +137,119 @@ public class MetricExportProperties {
this.names = names;
}
public String[] getIncludes() {
return this.includes;
}
public void setIncludes(String[] includes) {
this.includes = includes;
}
public void setExcludes(String[] excludes) {
this.excludes = excludes;
}
public String[] getExcludes() {
return this.excludes;
}
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Long getDelayMillis() {
return this.delayMillis;
}
public void setDelayMillis(long delayMillis) {
this.delayMillis = delayMillis;
}
public Boolean isIgnoreTimestamps() {
return this.ignoreTimestamps;
}
public void setIgnoreTimestamps(boolean ignoreTimestamps) {
this.ignoreTimestamps = ignoreTimestamps;
}
public Boolean isSendLatest() {
return this.sendLatest;
}
public void setSendLatest(boolean sendLatest) {
this.sendLatest = sendLatest;
}
}
public static class Redis {
/**
* Prefix for redis repository if active. Should be unique for this JVM, but most
* useful if it also has the form "x.y.a.b" where "x.y" is globally unique across
* all processes sharing the same repository, "a" is unique to this physical
* process and "b" is unique to this logical process (this application). If you
* set spring.application.name elsewhere, then the default will be in the right
* form.
*/
@Value("spring.metrics.${random.value:0000}.${spring.application.name:application}")
private String prefix = "spring.metrics";
/**
* Key for redis repository export (if active). Should be globally unique for a
* system sharing a redis repository.
*/
private String key = "keys.spring.metrics";
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 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;
}
}
}
class Trigger {
/**
* Find a matching trigger configuration.
* @param name the bean name to match
* @return a matching configuration if there is one
* Delay in milliseconds between export ticks. Metrics are exported to external
* sources on a schedule with this delay.
*/
public Export findExport(String name) {
for (Export value : this.writers.values()) {
if (PatternMatchUtils.simpleMatch(value.getNames(), name)) {
return value;
}
}
return this.export;
private Long delayMillis;
/**
* Flag to enable metric export (assuming a MetricWriter is available).
*/
private boolean enabled = true;
/**
* Flag to switch off any available optimizations based on not exporting unchanged
* metric values.
*/
private Boolean sendLatest;
private String[] includes;
private String[] excludes;
public String[] getIncludes() {
return this.includes;
}
public void setIncludes(String[] includes) {
this.includes = includes;
}
public void setExcludes(String[] excludes) {
this.excludes = excludes;
}
public String[] getExcludes() {
return this.excludes;
}
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Long getDelayMillis() {
return this.delayMillis;
}
public void setDelayMillis(long delayMillis) {
this.delayMillis = delayMillis;
}
public Boolean isSendLatest() {
return this.sendLatest;
}
public void setSendLatest(boolean sendLatest) {
this.sendLatest = sendLatest;
}
}

View File

@ -20,7 +20,6 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.springframework.boot.actuate.metrics.export.MetricExportProperties.Export;
import org.springframework.boot.actuate.metrics.reader.MetricReader;
import org.springframework.boot.actuate.metrics.writer.MetricWriter;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
@ -56,7 +55,7 @@ public class MetricExporters implements SchedulingConfigurer {
for (Entry<String, MetricWriter> entry : this.writers.entrySet()) {
String name = entry.getKey();
Export trigger = this.export.findExport(name);
Trigger trigger = this.export.findTrigger(name);
if (trigger != null) {
@ -67,7 +66,6 @@ public class MetricExporters implements SchedulingConfigurer {
exporter.setIncludes(trigger.getIncludes());
exporter.setExcludes(trigger.getExcludes());
}
exporter.setIgnoreTimestamps(trigger.isIgnoreTimestamps());
exporter.setSendLatest(trigger.isSendLatest());
this.exporters.put(name, exporter);

View File

@ -910,7 +910,7 @@ used by default if you are on Java 8 or if you are using Dropwizard metrics.
[[production-ready-metric-writers]]
=== Metric writers and aggregation
=== Metric writers, exporters and aggregation
Spring Boot provides a couple of implementations of a marker interface called `Exporter`
which can be used to copy metric readings from the in-memory buffers to a place where they
@ -919,11 +919,20 @@ can be analysed and displayed. Indeed, if you provide a `@Bean` that implements
metric updates every 5 seconds (configured via `spring.metrics.export.delayMillis`) via a
`@Scheduled` annotation in `MetricRepositoryAutoConfiguration`.
The default exporter is a `MetricCopyExporter` which tries to optimize itself by not
copying values that haven't changed since it was last called. The optimization can be
switched off using a flag (`spring.metrics.export.ignoreTimestamps`). Note also that the
`MetricRegistry` has no support for timestamps, so the optimization is not available if
you are using Dropwizard metrics (all metrics will be copied on every tick).
The default exporter is a `MetricCopyExporter` which tries to optimize
itself by not copying values that haven't changed since it was last
called (the optimization can be switched off using a flag
`spring.metrics.export.sendLatest`). Note also that the Dropwizard
`MetricRegistry` has no support for timestamps, so the optimization is
not available if you are using Dropwizard metrics (all metrics will be
copied on every tick).
The default values for the export trigger (`delayMillis`, `includes`,
`excludes`, `ignoreTimestamps` and `sendLatest`) can be set as
`spring.metrics.export.*`. Individual values for specific
`MetricWriters` can be set as
`spring.metrics.export.triggers.<name>.*` where `<name>` is a bean
name (or pattern for matching bean names).
@ -942,19 +951,20 @@ Example:
[source,java,indent=0]
----
@Value("metrics.mysystem.${random.value:0000}.${spring.application.name:application}")
private String prefix = "metrics.mysystem";
@Value("${metrics.key:keys.mysystem}")
private String key = "METRICSKEY";
@Bean
MetricWriter metricWriter() {
return new RedisMetricRepository(connectionFactory, prefix, key);
MetricWriter metricWriter(MetricExportProperties export) {
return new RedisMetricRepository(connectionFactory,
export.getRedis().getPrefix(), export.getRedis().getKey());
}
----
.application.properties
[source,properties]
----
spring.metrics.export.redis.prefix: metrics.mysystem.${random.value:0000}.${spring.application.name:application}
spring.metrics.export.redis.key: keys.mysystem
----
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.
@ -967,6 +977,11 @@ start with the master prefix (like `metrics.mysystem.*` in the example above). I
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).
NOTE: the example above uses `MetricExportProperties` to inject and
extract the key and prefix. This is provided to you as a convenience
by Spring Boot, and the defaults for that will be sensible, but there
is nothing to stop you using your own values as long as they follow
the recommendations.
[[production-ready-metric-writers-export-to-open-tdsb]]
@ -1064,25 +1079,31 @@ results to the "/metrics" endpoint. Example:
[source,java,indent=0]
----
@Bean
public PublicMetrics metricsAggregate() {
return new MetricReaderPublicMetrics(aggregates());
}
@Autowired
private MetricExportProperties export;
@Bean
protected MetricReader repository(RedisConnectionFactory connectionFactory) {
RedisMetricRepository repository = new RedisMetricRepository(connectionFactory,
"mysystem", "myorg.keys");
return repository;
}
@Bean
public PublicMetrics metricsAggregate() {
return new MetricReaderPublicMetrics(aggregates());
}
@Bean
protected MetricReader aggregates() {
AggregateMetricReader repository = new AggregateMetricReader(repository());
return repository;
}
@Bean
protected MetricReader repository(RedisConnectionFactory connectionFactory) {
RedisMetricRepository repository = new RedisMetricRepository(connectionFactory,
this.export.getRedis().getAggregatePrefix(), this.export.getRedis().getKey());
return repository;
}
@Bean
protected MetricReader aggregates() {
AggregateMetricReader repository = new AggregateMetricReader(repository());
return repository;
}
----
NOTE: the example above uses `MetricExportProperties` to inject and
extract the key and prefix. This is provided to you as a convenience
by Spring Boot, and the defaults for that will be sensible.
[[production-ready-dropwizard-metrics]]
@ -1134,33 +1155,33 @@ and obtain basic information about the last few requests:
[source,json,indent=0]
----
[{
"timestamp": 1394343677415,
"info": {
"method": "GET",
"path": "/trace",
"headers": {
"request": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Connection": "keep-alive",
"Accept-Encoding": "gzip, deflate",
"User-Agent": "Mozilla/5.0 Gecko/Firefox",
"Accept-Language": "en-US,en;q=0.5",
"Cookie": "_ga=GA1.1.827067509.1390890128; ..."
"Authorization": "Basic ...",
"Host": "localhost:8080"
},
"response": {
"Strict-Transport-Security": "max-age=31536000 ; includeSubDomains",
"X-Application-Context": "application:8080",
"Content-Type": "application/json;charset=UTF-8",
"status": "200"
}
}
}
},{
"timestamp": 1394343684465,
...
[{
"timestamp": 1394343677415,
"info": {
"method": "GET",
"path": "/trace",
"headers": {
"request": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Connection": "keep-alive",
"Accept-Encoding": "gzip, deflate",
"User-Agent": "Mozilla/5.0 Gecko/Firefox",
"Accept-Language": "en-US,en;q=0.5",
"Cookie": "_ga=GA1.1.827067509.1390890128; ..."
"Authorization": "Basic ...",
"Host": "localhost:8080"
},
"response": {
"Strict-Transport-Security": "max-age=31536000 ; includeSubDomains",
"X-Application-Context": "application:8080",
"Content-Type": "application/json;charset=UTF-8",
"status": "200"
}
}
}
},{
"timestamp": 1394343684465,
...
}]
----
@ -1201,9 +1222,9 @@ In `META-INF/spring.factories` file you have to activate the listener(s):
[indent=0]
----
org.springframework.context.ApplicationListener=\
org.springframework.boot.actuate.system.ApplicationPidFileWriter,
org.springframework.boot.actuate.system.EmbeddedServerPortFileWriter
org.springframework.context.ApplicationListener=\
org.springframework.boot.actuate.system.ApplicationPidFileWriter,
org.springframework.boot.actuate.system.EmbeddedServerPortFileWriter
----

View File

@ -20,6 +20,7 @@ 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.export.MetricExportProperties;
import org.springframework.boot.actuate.metrics.reader.MetricReader;
import org.springframework.boot.actuate.metrics.repository.redis.RedisMetricRepository;
import org.springframework.context.annotation.Bean;
@ -33,7 +34,7 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
public class AggregateMetricsConfiguration {
@Autowired
private ExportProperties export;
private MetricExportProperties export;
@Autowired
private RedisConnectionFactory connectionFactory;
@ -45,7 +46,7 @@ public class AggregateMetricsConfiguration {
private MetricReader globalMetricsForAggregation() {
return new RedisMetricRepository(this.connectionFactory,
this.export.getAggregatePrefix(), this.export.getKey());
this.export.getRedis().getAggregatePrefix(), this.export.getRedis().getKey());
}
private MetricReader aggregatesMetricReader() {

View File

@ -1,55 +0,0 @@
/*
* 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.boot.context.properties.ConfigurationProperties;
import org.springframework.util.StringUtils;
@ConfigurationProperties("redis.metrics.export")
class ExportProperties {
private String prefix = "spring.metrics";
private String key = "keys.spring.metrics";
public String getPrefix() {
return this.prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getKey() {
return this.key;
}
public void setKey(String key) {
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

@ -19,20 +19,19 @@ package sample.metrics.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.actuate.metrics.export.MetricExportProperties;
import org.springframework.boot.actuate.metrics.jmx.JmxMetricWriter;
import org.springframework.boot.actuate.metrics.repository.redis.RedisMetricRepository;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.jmx.export.MBeanExporter;
@SpringBootApplication
@EnableConfigurationProperties(ExportProperties.class)
public class SampleRedisExportApplication {
@Autowired
private ExportProperties export;
private MetricExportProperties export;
public static void main(String[] args) throws Exception {
SpringApplication.run(SampleRedisExportApplication.class, args);
@ -41,8 +40,8 @@ public class SampleRedisExportApplication {
@Bean
public RedisMetricRepository redisMetricWriter(
RedisConnectionFactory connectionFactory) {
return new RedisMetricRepository(connectionFactory, this.export.getPrefix(),
this.export.getKey());
return new RedisMetricRepository(connectionFactory, this.export.getRedis().getPrefix(),
this.export.getRedis().getKey());
}
@Bean

View File

@ -1,4 +1,4 @@
service.name: Phil
redis.metrics.export.prefix: metrics.sample.${random.value:0000}.${spring.application.name:application}
redis.metrics.export.key: keys.metrics.sample
spring.metrics.export.redis.prefix: metrics.sample.${random.value:0000}.${spring.application.name:application}
spring.metrics.export.redis.key: keys.metrics.sample
spring.jmx.default-domain: org.springframework.boot