Skip to content

Commit 96cb948

Browse files
committed
Merge pull request #10535 from jkschneider/micrometer-rc2-2
* pr/10535: Polish MetricsEndpoint Support composite registries in MetricsEndpoint
2 parents b3555fa + 22a6ee0 commit 96cb948

File tree

2 files changed

+139
-55
lines changed

2 files changed

+139
-55
lines changed

spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/metrics/MetricsEndpoint.java

Lines changed: 100 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,22 @@
1616

1717
package org.springframework.boot.actuate.metrics;
1818

19-
import java.util.Collection;
19+
import java.util.ArrayList;
2020
import java.util.Collections;
2121
import java.util.HashMap;
22+
import java.util.LinkedHashMap;
23+
import java.util.LinkedHashSet;
2224
import java.util.List;
2325
import java.util.Map;
26+
import java.util.Set;
27+
import java.util.function.BiFunction;
2428
import java.util.stream.Collectors;
25-
import java.util.stream.Stream;
2629

27-
import io.micrometer.core.instrument.Measurement;
2830
import io.micrometer.core.instrument.Meter;
2931
import io.micrometer.core.instrument.MeterRegistry;
3032
import io.micrometer.core.instrument.Statistic;
3133
import io.micrometer.core.instrument.Tag;
34+
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
3235

3336
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
3437
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
@@ -40,6 +43,7 @@
4043
* An {@link Endpoint} for exposing the metrics held by a {@link MeterRegistry}.
4144
*
4245
* @author Jon Schneider
46+
* @author Phillip Webb
4347
* @since 2.0.0
4448
*/
4549
@Endpoint(id = "metrics")
@@ -52,54 +56,43 @@ public MetricsEndpoint(MeterRegistry registry) {
5256
}
5357

5458
@ReadOperation
55-
public Map<String, List<String>> listNames() {
56-
return Collections.singletonMap("names", this.registry.getMeters().stream()
57-
.map(this::getMeterIdName).distinct().collect(Collectors.toList()));
59+
public ListNamesResponse listNames() {
60+
Set<String> names = new LinkedHashSet<>();
61+
collectNames(names, this.registry);
62+
return new ListNamesResponse(names);
5863
}
5964

60-
private String getMeterIdName(Meter meter) {
65+
private void collectNames(Set<String> names, MeterRegistry registry) {
66+
if (registry instanceof CompositeMeterRegistry) {
67+
((CompositeMeterRegistry) registry).getRegistries()
68+
.forEach((member) -> collectNames(names, member));
69+
}
70+
else {
71+
registry.getMeters().stream().map(this::getName).forEach(names::add);
72+
}
73+
}
74+
75+
private String getName(Meter meter) {
6176
return meter.getId().getName();
6277
}
6378

6479
@ReadOperation
65-
public Response metric(@Selector String requiredMetricName,
80+
public MetricResponse metric(@Selector String requiredMetricName,
6681
@Nullable List<String> tag) {
6782
Assert.isTrue(tag == null || tag.stream().allMatch((t) -> t.contains(":")),
6883
"Each tag parameter must be in the form key:value");
6984
List<Tag> tags = parseTags(tag);
70-
Collection<Meter> meters = this.registry.find(requiredMetricName).tags(tags)
71-
.meters();
85+
List<Meter> meters = new ArrayList<>();
86+
collectMeters(meters, this.registry, requiredMetricName, tags);
7287
if (meters.isEmpty()) {
7388
return null;
7489
}
75-
76-
Map<Statistic, Double> samples = new HashMap<>();
77-
Map<String, List<String>> availableTags = new HashMap<>();
78-
79-
for (Meter meter : meters) {
80-
for (Measurement ms : meter.measure()) {
81-
samples.merge(ms.getStatistic(), ms.getValue(), Double::sum);
82-
}
83-
for (Tag availableTag : meter.getId().getTags()) {
84-
availableTags.merge(availableTag.getKey(),
85-
Collections.singletonList(availableTag.getValue()),
86-
(t1, t2) -> Stream.concat(t1.stream(), t2.stream())
87-
.collect(Collectors.toList()));
88-
}
89-
}
90-
90+
Map<Statistic, Double> samples = getSamples(meters);
91+
Map<String, List<String>> availableTags = getAvailableTags(meters);
9192
tags.forEach((t) -> availableTags.remove(t.getKey()));
92-
93-
return new Response(requiredMetricName,
94-
samples.entrySet().stream()
95-
.map((sample) -> new Response.Sample(sample.getKey(),
96-
sample.getValue()))
97-
.collect(
98-
Collectors.toList()),
99-
availableTags.entrySet().stream()
100-
.map((tagValues) -> new Response.AvailableTag(tagValues.getKey(),
101-
tagValues.getValue()))
102-
.collect(Collectors.toList()));
93+
return new MetricResponse(requiredMetricName,
94+
asList(samples, MetricResponse.Sample::new),
95+
asList(availableTags, MetricResponse.AvailableTag::new));
10396
}
10497

10598
private List<Tag> parseTags(List<String> tags) {
@@ -109,18 +102,83 @@ private List<Tag> parseTags(List<String> tags) {
109102
}).collect(Collectors.toList());
110103
}
111104

105+
private void collectMeters(List<Meter> meters, MeterRegistry registry, String name,
106+
Iterable<Tag> tags) {
107+
if (registry instanceof CompositeMeterRegistry) {
108+
((CompositeMeterRegistry) registry).getRegistries()
109+
.forEach((member) -> collectMeters(meters, member, name, tags));
110+
}
111+
else {
112+
meters.addAll(registry.find(name).tags(tags).meters());
113+
}
114+
}
115+
116+
private Map<Statistic, Double> getSamples(List<Meter> meters) {
117+
Map<Statistic, Double> samples = new LinkedHashMap<>();
118+
meters.forEach((meter) -> mergeMeasurements(samples, meter));
119+
return samples;
120+
}
121+
122+
private void mergeMeasurements(Map<Statistic, Double> samples, Meter meter) {
123+
meter.measure().forEach((measurement) -> samples.merge(measurement.getStatistic(),
124+
measurement.getValue(), Double::sum));
125+
}
126+
127+
private Map<String, List<String>> getAvailableTags(List<Meter> meters) {
128+
Map<String, List<String>> availableTags = new HashMap<>();
129+
meters.forEach((meter) -> mergeAvailableTags(availableTags, meter));
130+
return availableTags;
131+
}
132+
133+
private void mergeAvailableTags(Map<String, List<String>> availableTags,
134+
Meter meter) {
135+
meter.getId().getTags().forEach((tag) -> {
136+
List<String> value = Collections.singletonList(tag.getValue());
137+
availableTags.merge(tag.getKey(), value, this::merge);
138+
});
139+
}
140+
141+
private <T> List<T> merge(List<T> list1, List<T> list2) {
142+
List<T> result = new ArrayList<>(list1.size() + list2.size());
143+
result.addAll(list1);
144+
result.addAll(list2);
145+
return result;
146+
}
147+
148+
private <K, V, T> List<T> asList(Map<K, V> map, BiFunction<K, V, T> mapper) {
149+
return map.entrySet().stream()
150+
.map((entry) -> mapper.apply(entry.getKey(), entry.getValue()))
151+
.collect(Collectors.toCollection(ArrayList::new));
152+
}
153+
154+
/**
155+
* Response payload for a metric name listing.
156+
*/
157+
static class ListNamesResponse {
158+
159+
private final Set<String> names;
160+
161+
ListNamesResponse(Set<String> names) {
162+
this.names = names;
163+
}
164+
165+
public Set<String> getNames() {
166+
return this.names;
167+
}
168+
}
169+
112170
/**
113-
* Response payload.
171+
* Response payload for a metric name selector.
114172
*/
115-
static class Response {
173+
static class MetricResponse {
116174

117175
private final String name;
118176

119177
private final List<Sample> measurements;
120178

121179
private final List<AvailableTag> availableTags;
122180

123-
Response(String name, List<Sample> measurements,
181+
MetricResponse(String name, List<Sample> measurements,
124182
List<AvailableTag> availableTags) {
125183
this.name = name;
126184
this.measurements = measurements;
@@ -189,6 +247,8 @@ public String toString() {
189247
return "MeasurementSample{" + "statistic=" + this.statistic + ", value="
190248
+ this.value + '}';
191249
}
250+
192251
}
252+
193253
}
194254
}

spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/metrics/MetricsEndpointTests.java

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@
1717
package org.springframework.boot.actuate.metrics;
1818

1919
import java.util.Collections;
20-
import java.util.List;
21-
import java.util.Map;
2220
import java.util.Optional;
2321
import java.util.stream.Stream;
2422

2523
import io.micrometer.core.instrument.MeterRegistry;
2624
import io.micrometer.core.instrument.Statistic;
25+
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
2726
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
2827
import org.junit.Test;
2928

@@ -43,28 +42,39 @@ public class MetricsEndpointTests {
4342

4443
@Test
4544
public void listNamesHandlesEmptyListOfMeters() {
46-
Map<String, List<String>> result = this.endpoint.listNames();
47-
assertThat(result).containsOnlyKeys("names");
48-
assertThat(result.get("names")).isEmpty();
45+
MetricsEndpoint.ListNamesResponse result = this.endpoint.listNames();
46+
assertThat(result.getNames()).isEmpty();
4947
}
5048

5149
@Test
5250
public void listNamesProducesListOfUniqueMeterNames() {
5351
this.registry.counter("com.example.foo");
5452
this.registry.counter("com.example.bar");
5553
this.registry.counter("com.example.foo");
56-
Map<String, List<String>> result = this.endpoint.listNames();
57-
assertThat(result).containsOnlyKeys("names");
58-
assertThat(result.get("names")).containsOnlyOnce("com.example.foo",
54+
MetricsEndpoint.ListNamesResponse result = this.endpoint.listNames();
55+
assertThat(result.getNames()).containsOnlyOnce("com.example.foo",
5956
"com.example.bar");
6057
}
6158

59+
@Test
60+
public void listNamesRecursesOverCompositeRegistries() {
61+
CompositeMeterRegistry composite = new CompositeMeterRegistry();
62+
SimpleMeterRegistry reg1 = new SimpleMeterRegistry();
63+
SimpleMeterRegistry reg2 = new SimpleMeterRegistry();
64+
composite.add(reg1);
65+
composite.add(reg2);
66+
reg1.counter("counter1").increment();
67+
reg2.counter("counter2").increment();
68+
MetricsEndpoint endpoint = new MetricsEndpoint(composite);
69+
assertThat(endpoint.listNames().getNames()).containsOnly("counter1", "counter2");
70+
}
71+
6272
@Test
6373
public void metricValuesAreTheSumOfAllTimeSeriesMatchingTags() {
6474
this.registry.counter("cache", "result", "hit", "host", "1").increment(2);
6575
this.registry.counter("cache", "result", "miss", "host", "1").increment(2);
6676
this.registry.counter("cache", "result", "hit", "host", "2").increment(2);
67-
MetricsEndpoint.Response response = this.endpoint.metric("cache",
77+
MetricsEndpoint.MetricResponse response = this.endpoint.metric("cache",
6878
Collections.emptyList());
6979
assertThat(response.getName()).isEqualTo("cache");
7080
assertThat(availableTagKeys(response)).containsExactly("result", "host");
@@ -77,29 +87,43 @@ public void metricValuesAreTheSumOfAllTimeSeriesMatchingTags() {
7787
@Test
7888
public void metricWithSpaceInTagValue() {
7989
this.registry.counter("counter", "key", "a space").increment(2);
80-
MetricsEndpoint.Response response = this.endpoint.metric("counter",
90+
MetricsEndpoint.MetricResponse response = this.endpoint.metric("counter",
8191
Collections.singletonList("key:a space"));
8292
assertThat(response.getName()).isEqualTo("counter");
8393
assertThat(availableTagKeys(response)).isEmpty();
8494
assertThat(getCount(response)).hasValue(2.0);
8595
}
8696

97+
@Test
98+
public void metricPresentInOneRegistryOfACompositeAndNotAnother() {
99+
CompositeMeterRegistry composite = new CompositeMeterRegistry();
100+
SimpleMeterRegistry reg1 = new SimpleMeterRegistry();
101+
SimpleMeterRegistry reg2 = new SimpleMeterRegistry();
102+
composite.add(reg1);
103+
composite.add(reg2);
104+
reg1.counter("counter1").increment();
105+
reg2.counter("counter2").increment();
106+
MetricsEndpoint endpoint = new MetricsEndpoint(composite);
107+
assertThat(endpoint.metric("counter1", Collections.emptyList())).isNotNull();
108+
assertThat(endpoint.metric("counter2", Collections.emptyList())).isNotNull();
109+
}
110+
87111
@Test
88112
public void nonExistentMetric() {
89-
MetricsEndpoint.Response response = this.endpoint.metric("does.not.exist",
113+
MetricsEndpoint.MetricResponse response = this.endpoint.metric("does.not.exist",
90114
Collections.emptyList());
91115
assertThat(response).isNull();
92116
}
93117

94-
private Optional<Double> getCount(MetricsEndpoint.Response response) {
118+
private Optional<Double> getCount(MetricsEndpoint.MetricResponse response) {
95119
return response.getMeasurements().stream()
96120
.filter((ms) -> ms.getStatistic().equals(Statistic.Count)).findAny()
97-
.map(MetricsEndpoint.Response.Sample::getValue);
121+
.map(MetricsEndpoint.MetricResponse.Sample::getValue);
98122
}
99123

100-
private Stream<String> availableTagKeys(MetricsEndpoint.Response response) {
124+
private Stream<String> availableTagKeys(MetricsEndpoint.MetricResponse response) {
101125
return response.getAvailableTags().stream()
102-
.map(MetricsEndpoint.Response.AvailableTag::getTag);
126+
.map(MetricsEndpoint.MetricResponse.AvailableTag::getTag);
103127
}
104128

105129
}

0 commit comments

Comments
 (0)