001/*
002 * Copyright 2012-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.boot.actuate.metrics;
018
019import java.util.Collection;
020import java.util.Collections;
021import java.util.HashMap;
022import java.util.HashSet;
023import java.util.LinkedHashMap;
024import java.util.LinkedHashSet;
025import java.util.List;
026import java.util.Map;
027import java.util.Set;
028import java.util.function.BiFunction;
029import java.util.stream.Collectors;
030
031import io.micrometer.core.instrument.Meter;
032import io.micrometer.core.instrument.MeterRegistry;
033import io.micrometer.core.instrument.Statistic;
034import io.micrometer.core.instrument.Tag;
035import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
036
037import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
038import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
039import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
040import org.springframework.boot.actuate.endpoint.annotation.Selector;
041import org.springframework.lang.Nullable;
042
043/**
044 * An {@link Endpoint} for exposing the metrics held by a {@link MeterRegistry}.
045 *
046 * @author Jon Schneider
047 * @author Phillip Webb
048 * @since 2.0.0
049 */
050@Endpoint(id = "metrics")
051public class MetricsEndpoint {
052
053        private final MeterRegistry registry;
054
055        public MetricsEndpoint(MeterRegistry registry) {
056                this.registry = registry;
057        }
058
059        @ReadOperation
060        public ListNamesResponse listNames() {
061                Set<String> names = new LinkedHashSet<>();
062                collectNames(names, this.registry);
063                return new ListNamesResponse(names);
064        }
065
066        private void collectNames(Set<String> names, MeterRegistry registry) {
067                if (registry instanceof CompositeMeterRegistry) {
068                        ((CompositeMeterRegistry) registry).getRegistries()
069                                        .forEach((member) -> collectNames(names, member));
070                }
071                else {
072                        registry.getMeters().stream().map(this::getName).forEach(names::add);
073                }
074        }
075
076        private String getName(Meter meter) {
077                return meter.getId().getName();
078        }
079
080        @ReadOperation
081        public MetricResponse metric(@Selector String requiredMetricName,
082                        @Nullable List<String> tag) {
083                List<Tag> tags = parseTags(tag);
084                Collection<Meter> meters = findFirstMatchingMeters(this.registry,
085                                requiredMetricName, tags);
086                if (meters.isEmpty()) {
087                        return null;
088                }
089                Map<Statistic, Double> samples = getSamples(meters);
090                Map<String, Set<String>> availableTags = getAvailableTags(meters);
091                tags.forEach((t) -> availableTags.remove(t.getKey()));
092                Meter.Id meterId = meters.iterator().next().getId();
093                return new MetricResponse(requiredMetricName, meterId.getDescription(),
094                                meterId.getBaseUnit(), asList(samples, Sample::new),
095                                asList(availableTags, AvailableTag::new));
096        }
097
098        private List<Tag> parseTags(List<String> tags) {
099                if (tags == null) {
100                        return Collections.emptyList();
101                }
102                return tags.stream().map(this::parseTag).collect(Collectors.toList());
103        }
104
105        private Tag parseTag(String tag) {
106                String[] parts = tag.split(":", 2);
107                if (parts.length != 2) {
108                        throw new InvalidEndpointRequestException(
109                                        "Each tag parameter must be in the form 'key:value' but was: " + tag,
110                                        "Each tag parameter must be in the form 'key:value'");
111                }
112                return Tag.of(parts[0], parts[1]);
113        }
114
115        private Collection<Meter> findFirstMatchingMeters(MeterRegistry registry, String name,
116                        Iterable<Tag> tags) {
117                if (registry instanceof CompositeMeterRegistry) {
118                        return findFirstMatchingMeters((CompositeMeterRegistry) registry, name, tags);
119                }
120                return registry.find(name).tags(tags).meters();
121        }
122
123        private Collection<Meter> findFirstMatchingMeters(CompositeMeterRegistry composite,
124                        String name, Iterable<Tag> tags) {
125                return composite.getRegistries().stream()
126                                .map((registry) -> findFirstMatchingMeters(registry, name, tags))
127                                .filter((matching) -> !matching.isEmpty()).findFirst()
128                                .orElse(Collections.emptyList());
129        }
130
131        private Map<Statistic, Double> getSamples(Collection<Meter> meters) {
132                Map<Statistic, Double> samples = new LinkedHashMap<>();
133                meters.forEach((meter) -> mergeMeasurements(samples, meter));
134                return samples;
135        }
136
137        private void mergeMeasurements(Map<Statistic, Double> samples, Meter meter) {
138                meter.measure().forEach((measurement) -> samples.merge(measurement.getStatistic(),
139                                measurement.getValue(), mergeFunction(measurement.getStatistic())));
140        }
141
142        private BiFunction<Double, Double, Double> mergeFunction(Statistic statistic) {
143                return Statistic.MAX.equals(statistic) ? Double::max : Double::sum;
144        }
145
146        private Map<String, Set<String>> getAvailableTags(Collection<Meter> meters) {
147                Map<String, Set<String>> availableTags = new HashMap<>();
148                meters.forEach((meter) -> mergeAvailableTags(availableTags, meter));
149                return availableTags;
150        }
151
152        private void mergeAvailableTags(Map<String, Set<String>> availableTags, Meter meter) {
153                meter.getId().getTags().forEach((tag) -> {
154                        Set<String> value = Collections.singleton(tag.getValue());
155                        availableTags.merge(tag.getKey(), value, this::merge);
156                });
157        }
158
159        private <T> Set<T> merge(Set<T> set1, Set<T> set2) {
160                Set<T> result = new HashSet<>(set1.size() + set2.size());
161                result.addAll(set1);
162                result.addAll(set2);
163                return result;
164        }
165
166        private <K, V, T> List<T> asList(Map<K, V> map, BiFunction<K, V, T> mapper) {
167                return map.entrySet().stream()
168                                .map((entry) -> mapper.apply(entry.getKey(), entry.getValue()))
169                                .collect(Collectors.toList());
170        }
171
172        /**
173         * Response payload for a metric name listing.
174         */
175        public static final class ListNamesResponse {
176
177                private final Set<String> names;
178
179                ListNamesResponse(Set<String> names) {
180                        this.names = names;
181                }
182
183                public Set<String> getNames() {
184                        return this.names;
185                }
186
187        }
188
189        /**
190         * Response payload for a metric name selector.
191         */
192        public static final class MetricResponse {
193
194                private final String name;
195
196                private final String description;
197
198                private final String baseUnit;
199
200                private final List<Sample> measurements;
201
202                private final List<AvailableTag> availableTags;
203
204                MetricResponse(String name, String description, String baseUnit,
205                                List<Sample> measurements, List<AvailableTag> availableTags) {
206                        this.name = name;
207                        this.description = description;
208                        this.baseUnit = baseUnit;
209                        this.measurements = measurements;
210                        this.availableTags = availableTags;
211                }
212
213                public String getName() {
214                        return this.name;
215                }
216
217                public String getDescription() {
218                        return this.description;
219                }
220
221                public String getBaseUnit() {
222                        return this.baseUnit;
223                }
224
225                public List<Sample> getMeasurements() {
226                        return this.measurements;
227                }
228
229                public List<AvailableTag> getAvailableTags() {
230                        return this.availableTags;
231                }
232
233        }
234
235        /**
236         * A set of tags for further dimensional drilldown and their potential values.
237         */
238        public static final class AvailableTag {
239
240                private final String tag;
241
242                private final Set<String> values;
243
244                AvailableTag(String tag, Set<String> values) {
245                        this.tag = tag;
246                        this.values = values;
247                }
248
249                public String getTag() {
250                        return this.tag;
251                }
252
253                public Set<String> getValues() {
254                        return this.values;
255                }
256
257        }
258
259        /**
260         * A measurement sample combining a {@link Statistic statistic} and a value.
261         */
262        public static final class Sample {
263
264                private final Statistic statistic;
265
266                private final Double value;
267
268                Sample(Statistic statistic, Double value) {
269                        this.statistic = statistic;
270                        this.value = value;
271                }
272
273                public Statistic getStatistic() {
274                        return this.statistic;
275                }
276
277                public Double getValue() {
278                        return this.value;
279                }
280
281                @Override
282                public String toString() {
283                        return "MeasurementSample{" + "statistic=" + this.statistic + ", value="
284                                        + this.value + '}';
285                }
286
287        }
288
289}