diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricExportAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricExportAutoConfiguration.java
index 4737e3543c..897893fc97 100644
--- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricExportAutoConfiguration.java
+++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/MetricExportAutoConfiguration.java
@@ -85,14 +85,18 @@ public class MetricExportAutoConfiguration {
@Configuration
protected static class MetricExportPropertiesConfiguration {
- @Value("spring.metrics.${random.value:0000}.${spring.application.name:application}")
+ @Value("spring.metrics.${spring.application.name:application}.${random.value:0000}")
private String prefix = "spring.metrics";
+ @Value("d.d.k.d")
+ private String aggregateKeyPattern = "";
+
@Bean(name = "spring.metrics.export.CONFIGURATION_PROPERTIES")
@ConditionalOnMissingBean
public MetricExportProperties metricExportProperties() {
MetricExportProperties export = new MetricExportProperties();
export.getRedis().setPrefix(this.prefix);
+ export.getRedis().setAggregateKeyPattern(this.aggregateKeyPattern);
return export;
}
diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReader.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReader.java
index cb6b7a3f58..6028321e2c 100644
--- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReader.java
+++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReader.java
@@ -42,7 +42,7 @@ public class AggregateMetricReader implements MetricReader {
private MetricReader source;
- private int truncate = 2;
+ private String keyPattern = "d.d";
private String prefix = "aggregate.";
@@ -51,21 +51,32 @@ public class AggregateMetricReader implements MetricReader {
}
/**
- * The number of period-separated keys to remove from the start of the input metric
- * names before aggregating.
- * @param truncate length of source metric prefixes
+ * Pattern that tells the aggregator what to do with the keys from the source
+ * repository. The keys in the source repository are assumed to be period separated,
+ * and the pattern is in the same format, e.g. "d.d.k.d". The pattern segments are
+ * matched against the source keys and a rule is applied:
+ *
+ * - "d" means "discard" this key segment (useful for global prefixes like system
+ * identifiers, or aggregate keys a.k.a. physical identifiers)
+ * - "k" means "keep" it with no change (useful for logical identifiers like app
+ * names)
+ *
+ * Default is "d.d" (we assume there is a global prefix of length 2).
+ *
+ * @param keyPattern the keyPattern to set
*/
- public void setTruncateKeyLength(int truncate) {
- this.truncate = truncate;
+ public void setKeyPattern(String keyPattern) {
+ this.keyPattern = keyPattern;
}
/**
- * Prefix to apply to all output metrics. A period will be appended if no present in
- * the provided value.
- * @param prefix the prefix to use default "aggregator.")
+ * Prefix to apply to all output metrics. A period will be appended if not present in the
+ * provided value.
+ *
+ * @param prefix the prefix to use (default "aggregator.")
*/
public void setPrefix(String prefix) {
- this.prefix = prefix.endsWith(".") ? prefix : prefix + ".";
+ this.prefix = prefix.endsWith(".") ? prefix : (StringUtils.hasText(prefix) ? prefix + "." : "");
}
@Override
@@ -128,12 +139,22 @@ public class AggregateMetricReader implements MetricReader {
private String getSourceKey(String name) {
String[] keys = StringUtils.delimitedListToStringArray(name, ".");
- if (keys.length <= this.truncate) {
- return null;
+ String[] patterns = StringUtils.delimitedListToStringArray(this.keyPattern, ".");
+ StringBuilder builder = new StringBuilder();
+ int index = 0;
+ for (index = 0; index < patterns.length; index++) {
+ if ("k".equals(patterns[index])) {
+ if (builder.length() > 0) {
+ builder.append(".");
+ }
+ builder.append(keys[index]);
+ }
}
- StringBuilder builder = new StringBuilder(keys[this.truncate]);
- for (int i = this.truncate + 1; i < keys.length; i++) {
- builder.append(".").append(keys[i]);
+ for (; index < keys.length; index++) {
+ if (builder.length() > 0) {
+ builder.append(".");
+ }
+ builder.append(keys[index]);
}
return builder.toString();
}
diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricExportProperties.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricExportProperties.java
index b4aff1b066..d51ee7e948 100644
--- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricExportProperties.java
+++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/export/MetricExportProperties.java
@@ -24,7 +24,6 @@ import javax.annotation.PostConstruct;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.PatternMatchUtils;
-import org.springframework.util.StringUtils;
/**
* Configuration properties for metrics export.
@@ -105,8 +104,8 @@ public class MetricExportProperties extends TriggerProperties {
/**
* 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
+ * all processes sharing the same repository, "a" is unique to this logical
+ * process (this application) and "b" is unique to this physical process. If you
* set spring.application.name elsewhere, then the default will be in the right
* form.
*/
@@ -118,6 +117,15 @@ public class MetricExportProperties extends TriggerProperties {
*/
private String key = "keys.spring.metrics";
+ /**
+ * Pattern that tells the aggregator what to do with the keys from the source
+ * repository. The keys in the source repository are assumed to be period
+ * separated, and the pattern is in the same format, e.g. "d.d.k.d". Here "d"
+ * means "discard" and "k" means "keep" the key segment in the corresponding
+ * position in the source.
+ */
+ private String aggregateKeyPattern = "";
+
public String getPrefix() {
return this.prefix;
}
@@ -134,14 +142,22 @@ public class MetricExportProperties extends TriggerProperties {
this.key = key;
}
+ public String getAggregateKeyPattern() {
+ return this.aggregateKeyPattern;
+ }
+
+ public void setAggregateKeyPattern(String keyPattern) {
+ this.aggregateKeyPattern = keyPattern;
+ }
+
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
- // (the aggregator strips a further 2 by default).
- return tokens[0];
- }
+ if (this.key.startsWith("keys.")) {
+ return this.key.substring("keys.".length());
+ }
+ // Something that is safe (not empty) but not the whole prefix (on the
+ // assumption that it contains dimension keys)
+ if (this.prefix.contains(".") && !this.prefix.endsWith(".")) {
+ return this.prefix.substring(this.prefix.indexOf("." + 1));
}
return this.prefix;
}
diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReaderTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReaderTests.java
index 3f87f1fc45..c4412a7ad1 100644
--- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReaderTests.java
+++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/aggregate/AggregateMetricReaderTests.java
@@ -16,6 +16,9 @@
package org.springframework.boot.actuate.metrics.aggregate;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
import java.util.Date;
import org.junit.Test;
@@ -23,9 +26,6 @@ import org.springframework.boot.actuate.metrics.Metric;
import org.springframework.boot.actuate.metrics.repository.InMemoryMetricRepository;
import org.springframework.boot.actuate.metrics.writer.Delta;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
/**
* Tests for {@link AggregateMetricReader}.
*
@@ -44,16 +44,39 @@ public class AggregateMetricReaderTests {
}
@Test
- public void writeAndReadLatestValue() {
- this.source.set(new Metric("foo.bar.spam", 2.3, new Date(100L)));
- this.source.set(new Metric("oof.rab.spam", 2.4, new Date(0L)));
- assertEquals(2.3, this.reader.findOne("aggregate.spam").getValue());
+ public void defaultKeyPattern() {
+ this.source.set(new Metric("foo.bar.spam.bucket.wham", 2.3));
+ assertEquals(2.3, this.reader.findOne("aggregate.spam.bucket.wham").getValue());
+ }
+
+ @Test
+ public void addKeyPattern() {
+ this.source.set(new Metric("foo.bar.spam.bucket.wham", 2.3));
+ this.reader.setKeyPattern("d.d.k.d");
+ assertEquals(2.3, this.reader.findOne("aggregate.spam.wham").getValue());
+ }
+
+ @Test
+ public void addPrefix() {
+ this.source.set(new Metric("foo.bar.spam.bucket.wham", 2.3));
+ this.source.set(new Metric("off.bar.spam.bucket.wham", 2.4));
+ this.reader.setPrefix("www");
+ this.reader.setKeyPattern("k.d.k.d");
+ assertEquals(2.3, this.reader.findOne("www.foo.spam.wham").getValue());
+ assertEquals(2, this.reader.count());
}
@Test
public void writeAndReadExtraLong() {
this.source.set(new Metric("blee.foo.bar.spam", 2.3));
- this.reader.setTruncateKeyLength(3);
+ this.reader.setKeyPattern("d.d.d.k");
+ assertEquals(2.3, this.reader.findOne("aggregate.spam").getValue());
+ }
+
+ @Test
+ public void writeAndReadLatestValue() {
+ this.source.set(new Metric("foo.bar.spam", 2.3, new Date(100L)));
+ this.source.set(new Metric("oof.rab.spam", 2.4, new Date(0L)));
assertEquals(2.3, this.reader.findOne("aggregate.spam").getValue());
}
diff --git a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc
index d63459aa76..f0c678e9aa 100644
--- a/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc
+++ b/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc
@@ -985,11 +985,11 @@ MetricWriter metricWriter(MetricExportProperties export) {
.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
+spring.metrics.export.redis.prefix: metrics.mysystem.${spring.application.name:application}.${random.value:0000}
+spring.metrics.export.redis.key: keys.metrics.mysystem
----
-The prefix is constructed with the application name at the end, so it can easily be used
+The prefix is constructed with the application name and id 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
@@ -1128,7 +1128,8 @@ results to the "/metrics" endpoint. Example:
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.
+by Spring Boot, and the defaults will be sensible. They are set up in
+`MetricExportAutoConfiguration`.
NOTE: the `MetricReaders` above are not `@Beans` and are not marked as
`@ExportMetricReader` because they are just collecting and analysing