Support composite registries in MetricsEndpoint

Update `MetricsEndpoint` to deal with `CompositeMeterRegistry`
instances.

Closes gh-10535
This commit is contained in:
Jon Schneider 2017-10-05 22:43:10 -05:00 committed by Phillip Webb
parent b3555fa5c5
commit 37975836f0
2 changed files with 104 additions and 30 deletions

View File

@ -16,11 +16,14 @@
package org.springframework.boot.actuate.metrics; package org.springframework.boot.actuate.metrics;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -29,6 +32,7 @@ import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Statistic; import io.micrometer.core.instrument.Statistic;
import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
@ -52,9 +56,22 @@ public class MetricsEndpoint {
} }
@ReadOperation @ReadOperation
public Map<String, List<String>> listNames() { public ListNamesResponse listNames() {
return Collections.singletonMap("names", this.registry.getMeters().stream() return new ListNamesResponse(recurseListNames(this.registry));
.map(this::getMeterIdName).distinct().collect(Collectors.toList())); }
private Set<String> recurseListNames(MeterRegistry registry) {
Set<String> names = new HashSet<>();
if (registry instanceof CompositeMeterRegistry) {
for (MeterRegistry compositeMember : ((CompositeMeterRegistry) registry)
.getRegistries()) {
names.addAll(recurseListNames(compositeMember));
}
}
else {
registry.getMeters().stream().map(this::getMeterIdName).forEach(names::add);
}
return names;
} }
private String getMeterIdName(Meter meter) { private String getMeterIdName(Meter meter) {
@ -62,13 +79,12 @@ public class MetricsEndpoint {
} }
@ReadOperation @ReadOperation
public Response metric(@Selector String requiredMetricName, public MetricResponse metric(@Selector String requiredMetricName,
@Nullable List<String> tag) { @Nullable List<String> tag) {
Assert.isTrue(tag == null || tag.stream().allMatch((t) -> t.contains(":")), Assert.isTrue(tag == null || tag.stream().allMatch((t) -> t.contains(":")),
"Each tag parameter must be in the form key:value"); "Each tag parameter must be in the form key:value");
List<Tag> tags = parseTags(tag); List<Tag> tags = parseTags(tag);
Collection<Meter> meters = this.registry.find(requiredMetricName).tags(tags) Collection<Meter> meters = recurseFindMeter(this.registry, requiredMetricName, tags);
.meters();
if (meters.isEmpty()) { if (meters.isEmpty()) {
return null; return null;
} }
@ -90,18 +106,32 @@ public class MetricsEndpoint {
tags.forEach((t) -> availableTags.remove(t.getKey())); tags.forEach((t) -> availableTags.remove(t.getKey()));
return new Response(requiredMetricName, return new MetricResponse(requiredMetricName,
samples.entrySet().stream() samples.entrySet().stream()
.map((sample) -> new Response.Sample(sample.getKey(), .map((sample) -> new MetricResponse.Sample(sample.getKey(),
sample.getValue())) sample.getValue()))
.collect( .collect(Collectors.toList()),
Collectors.toList()),
availableTags.entrySet().stream() availableTags.entrySet().stream()
.map((tagValues) -> new Response.AvailableTag(tagValues.getKey(), .map((tagValues) -> new MetricResponse.AvailableTag(
tagValues.getValue())) tagValues.getKey(), tagValues.getValue()))
.collect(Collectors.toList())); .collect(Collectors.toList()));
} }
private Collection<Meter> recurseFindMeter(MeterRegistry registry, String name,
Iterable<Tag> tags) {
Collection<Meter> meters = new ArrayList<>();
if (registry instanceof CompositeMeterRegistry) {
for (MeterRegistry compositeMember : ((CompositeMeterRegistry) registry)
.getRegistries()) {
meters.addAll(recurseFindMeter(compositeMember, name, tags));
}
}
else {
meters.addAll(registry.find(name).tags(tags).meters());
}
return meters;
}
private List<Tag> parseTags(List<String> tags) { private List<Tag> parseTags(List<String> tags) {
return tags == null ? Collections.emptyList() : tags.stream().map((t) -> { return tags == null ? Collections.emptyList() : tags.stream().map((t) -> {
String[] tagParts = t.split(":", 2); String[] tagParts = t.split(":", 2);
@ -110,9 +140,25 @@ public class MetricsEndpoint {
} }
/** /**
* Response payload. * Response payload for a metric name listing.
*/ */
static class Response { static class ListNamesResponse {
private final Set<String> names;
ListNamesResponse(Set<String> names) {
this.names = names;
}
public Set<String> getNames() {
return this.names;
}
}
/**
* Response payload for a metric name selector.
*/
static class MetricResponse {
private final String name; private final String name;
@ -120,7 +166,7 @@ public class MetricsEndpoint {
private final List<AvailableTag> availableTags; private final List<AvailableTag> availableTags;
Response(String name, List<Sample> measurements, MetricResponse(String name, List<Sample> measurements,
List<AvailableTag> availableTags) { List<AvailableTag> availableTags) {
this.name = name; this.name = name;
this.measurements = measurements; this.measurements = measurements;

View File

@ -17,13 +17,12 @@
package org.springframework.boot.actuate.metrics; package org.springframework.boot.actuate.metrics;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Statistic; import io.micrometer.core.instrument.Statistic;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.junit.Test; import org.junit.Test;
@ -43,9 +42,8 @@ public class MetricsEndpointTests {
@Test @Test
public void listNamesHandlesEmptyListOfMeters() { public void listNamesHandlesEmptyListOfMeters() {
Map<String, List<String>> result = this.endpoint.listNames(); MetricsEndpoint.ListNamesResponse result = this.endpoint.listNames();
assertThat(result).containsOnlyKeys("names"); assertThat(result.getNames()).isEmpty();
assertThat(result.get("names")).isEmpty();
} }
@Test @Test
@ -53,18 +51,32 @@ public class MetricsEndpointTests {
this.registry.counter("com.example.foo"); this.registry.counter("com.example.foo");
this.registry.counter("com.example.bar"); this.registry.counter("com.example.bar");
this.registry.counter("com.example.foo"); this.registry.counter("com.example.foo");
Map<String, List<String>> result = this.endpoint.listNames(); MetricsEndpoint.ListNamesResponse result = this.endpoint.listNames();
assertThat(result).containsOnlyKeys("names"); assertThat(result.getNames()).containsOnlyOnce("com.example.foo",
assertThat(result.get("names")).containsOnlyOnce("com.example.foo",
"com.example.bar"); "com.example.bar");
} }
@Test
public void listNamesRecursesOverCompositeRegistries() {
CompositeMeterRegistry composite = new CompositeMeterRegistry();
SimpleMeterRegistry reg1 = new SimpleMeterRegistry();
SimpleMeterRegistry reg2 = new SimpleMeterRegistry();
composite.add(reg1);
composite.add(reg2);
reg1.counter("counter1").increment();
reg2.counter("counter2").increment();
MetricsEndpoint endpoint = new MetricsEndpoint(composite);
assertThat(endpoint.listNames().getNames()).containsOnly("counter1", "counter2");
}
@Test @Test
public void metricValuesAreTheSumOfAllTimeSeriesMatchingTags() { public void metricValuesAreTheSumOfAllTimeSeriesMatchingTags() {
this.registry.counter("cache", "result", "hit", "host", "1").increment(2); this.registry.counter("cache", "result", "hit", "host", "1").increment(2);
this.registry.counter("cache", "result", "miss", "host", "1").increment(2); this.registry.counter("cache", "result", "miss", "host", "1").increment(2);
this.registry.counter("cache", "result", "hit", "host", "2").increment(2); this.registry.counter("cache", "result", "hit", "host", "2").increment(2);
MetricsEndpoint.Response response = this.endpoint.metric("cache", MetricsEndpoint.MetricResponse response = this.endpoint.metric("cache",
Collections.emptyList()); Collections.emptyList());
assertThat(response.getName()).isEqualTo("cache"); assertThat(response.getName()).isEqualTo("cache");
assertThat(availableTagKeys(response)).containsExactly("result", "host"); assertThat(availableTagKeys(response)).containsExactly("result", "host");
@ -77,29 +89,45 @@ public class MetricsEndpointTests {
@Test @Test
public void metricWithSpaceInTagValue() { public void metricWithSpaceInTagValue() {
this.registry.counter("counter", "key", "a space").increment(2); this.registry.counter("counter", "key", "a space").increment(2);
MetricsEndpoint.Response response = this.endpoint.metric("counter", MetricsEndpoint.MetricResponse response = this.endpoint.metric("counter",
Collections.singletonList("key:a space")); Collections.singletonList("key:a space"));
assertThat(response.getName()).isEqualTo("counter"); assertThat(response.getName()).isEqualTo("counter");
assertThat(availableTagKeys(response)).isEmpty(); assertThat(availableTagKeys(response)).isEmpty();
assertThat(getCount(response)).hasValue(2.0); assertThat(getCount(response)).hasValue(2.0);
} }
@Test
public void metricPresentInOneRegistryOfACompositeAndNotAnother() {
CompositeMeterRegistry composite = new CompositeMeterRegistry();
SimpleMeterRegistry reg1 = new SimpleMeterRegistry();
SimpleMeterRegistry reg2 = new SimpleMeterRegistry();
composite.add(reg1);
composite.add(reg2);
reg1.counter("counter1").increment();
reg2.counter("counter2").increment();
MetricsEndpoint endpoint = new MetricsEndpoint(composite);
assertThat(endpoint.metric("counter1", Collections.emptyList())).isNotNull();
assertThat(endpoint.metric("counter2", Collections.emptyList())).isNotNull();
}
@Test @Test
public void nonExistentMetric() { public void nonExistentMetric() {
MetricsEndpoint.Response response = this.endpoint.metric("does.not.exist", MetricsEndpoint.MetricResponse response = this.endpoint.metric("does.not.exist",
Collections.emptyList()); Collections.emptyList());
assertThat(response).isNull(); assertThat(response).isNull();
} }
private Optional<Double> getCount(MetricsEndpoint.Response response) { private Optional<Double> getCount(MetricsEndpoint.MetricResponse response) {
return response.getMeasurements().stream() return response.getMeasurements().stream()
.filter((ms) -> ms.getStatistic().equals(Statistic.Count)).findAny() .filter((ms) -> ms.getStatistic().equals(Statistic.Count)).findAny()
.map(MetricsEndpoint.Response.Sample::getValue); .map(MetricsEndpoint.MetricResponse.Sample::getValue);
} }
private Stream<String> availableTagKeys(MetricsEndpoint.Response response) { private Stream<String> availableTagKeys(MetricsEndpoint.MetricResponse response) {
return response.getAvailableTags().stream() return response.getAvailableTags().stream()
.map(MetricsEndpoint.Response.AvailableTag::getTag); .map(MetricsEndpoint.MetricResponse.AvailableTag::getTag);
} }
} }