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}