package cc.mrbird.febs.monitor.endpoint;
|
|
import cc.mrbird.febs.common.annotation.FebsEndPoint;
|
import io.micrometer.core.instrument.Meter;
|
import io.micrometer.core.instrument.MeterRegistry;
|
import io.micrometer.core.instrument.Statistic;
|
import io.micrometer.core.instrument.Tag;
|
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
|
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
|
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
|
import org.springframework.boot.actuate.endpoint.annotation.Selector;
|
import org.springframework.lang.Nullable;
|
|
import java.util.*;
|
import java.util.function.BiFunction;
|
import java.util.stream.Collectors;
|
|
/**
|
* @author MrBird
|
*/
|
@FebsEndPoint
|
public class FebsMetricsEndpoint {
|
|
private final MeterRegistry registry;
|
|
public FebsMetricsEndpoint(MeterRegistry registry) {
|
this.registry = registry;
|
}
|
|
@ReadOperation
|
public ListNamesResponse listNames() {
|
Set<String> names = new LinkedHashSet<>();
|
this.collectNames(names, this.registry);
|
return new ListNamesResponse(names);
|
}
|
|
private void collectNames(Set<String> names, MeterRegistry registry) {
|
if (registry instanceof CompositeMeterRegistry) {
|
((CompositeMeterRegistry)registry).getRegistries().forEach((member) -> this.collectNames(names, member));
|
} else {
|
registry.getMeters().stream().map(this::getName).forEach(names::add);
|
}
|
|
}
|
|
private String getName(Meter meter) {
|
return meter.getId().getName();
|
}
|
|
@ReadOperation
|
public FebsMetricResponse metric(@Selector String requiredMetricName, @Nullable List<String> tag) {
|
List<Tag> tags = this.parseTags(tag);
|
Collection<Meter> meters = this.findFirstMatchingMeters(this.registry, requiredMetricName, tags);
|
if (meters.isEmpty()) {
|
return null;
|
} else {
|
Map<Statistic, Double> samples = this.getSamples(meters);
|
Map<String, Set<String>> availableTags = this.getAvailableTags(meters);
|
tags.forEach((t) -> availableTags.remove(t.getKey()));
|
Meter.Id meterId = meters.iterator().next().getId();
|
return new FebsMetricResponse(requiredMetricName, meterId.getDescription(), meterId.getBaseUnit(), this.asList(samples, Sample::new), this.asList(availableTags, AvailableTag::new));
|
}
|
}
|
|
private List<Tag> parseTags(List<String> tags) {
|
return tags == null ? Collections.emptyList() : tags.stream().map(this::parseTag).collect(Collectors.toList());
|
}
|
|
private Tag parseTag(String tag) {
|
String[] parts = tag.split(":", 2);
|
if (parts.length != 2) {
|
throw new InvalidEndpointRequestException("Each tag parameter must be in the form 'key:value' but was: " + tag, "Each tag parameter must be in the form 'key:value'");
|
} else {
|
return Tag.of(parts[0], parts[1]);
|
}
|
}
|
|
private Collection<Meter> findFirstMatchingMeters(MeterRegistry registry, String name, Iterable<Tag> tags) {
|
return registry instanceof CompositeMeterRegistry ? this.findFirstMatchingMeters((CompositeMeterRegistry)registry, name, tags) : registry.find(name).tags(tags).meters();
|
}
|
|
private Collection<Meter> findFirstMatchingMeters(CompositeMeterRegistry composite, String name, Iterable<Tag> tags) {
|
return composite.getRegistries().stream().map((registry) -> this.findFirstMatchingMeters(registry, name, tags)).filter((matching) -> !matching.isEmpty()).findFirst().orElse(Collections.emptyList());
|
}
|
|
private Map<Statistic, Double> getSamples(Collection<Meter> meters) {
|
Map<Statistic, Double> samples = new LinkedHashMap<>();
|
meters.forEach((meter) -> this.mergeMeasurements(samples, meter));
|
return samples;
|
}
|
|
private void mergeMeasurements(Map<Statistic, Double> samples, Meter meter) {
|
meter.measure().forEach((measurement) -> samples.merge(measurement.getStatistic(), measurement.getValue(), this.mergeFunction(measurement.getStatistic())));
|
}
|
|
private BiFunction<Double, Double, Double> mergeFunction(Statistic statistic) {
|
return Statistic.MAX.equals(statistic) ? Double::max : Double::sum;
|
}
|
|
private Map<String, Set<String>> getAvailableTags(Collection<Meter> meters) {
|
Map<String, Set<String>> availableTags = new HashMap<>(10);
|
meters.forEach((meter) -> this.mergeAvailableTags(availableTags, meter));
|
return availableTags;
|
}
|
|
private void mergeAvailableTags(Map<String, Set<String>> availableTags, Meter meter) {
|
meter.getId().getTags().forEach((tag) -> {
|
Set<String> value = Collections.singleton(tag.getValue());
|
availableTags.merge(tag.getKey(), value, this::merge);
|
});
|
}
|
|
private <T> Set<T> merge(Set<T> set1, Set<T> set2) {
|
Set<T> result = new HashSet<>(set1.size() + set2.size());
|
result.addAll(set1);
|
result.addAll(set2);
|
return result;
|
}
|
|
private <K, V, T> List<T> asList(Map<K, V> map, BiFunction<K, V, T> mapper) {
|
return map.entrySet().stream().map((entry) -> mapper.apply(entry.getKey(), entry.getValue())).collect(Collectors.toList());
|
}
|
|
public static final class Sample {
|
private final Statistic statistic;
|
private final Double value;
|
|
Sample(Statistic statistic, Double value) {
|
this.statistic = statistic;
|
this.value = value;
|
}
|
|
public Statistic getStatistic() {
|
return this.statistic;
|
}
|
|
public Double getValue() {
|
return this.value;
|
}
|
|
@Override
|
public String toString() {
|
return "MeasurementSample{statistic=" + this.statistic + ", value=" + this.value + '}';
|
}
|
}
|
|
public static final class AvailableTag {
|
private final String tag;
|
private final Set<String> values;
|
|
AvailableTag(String tag, Set<String> values) {
|
this.tag = tag;
|
this.values = values;
|
}
|
|
public String getTag() {
|
return this.tag;
|
}
|
|
public Set<String> getValues() {
|
return this.values;
|
}
|
}
|
|
public static final class FebsMetricResponse {
|
private final String name;
|
private final String description;
|
private final String baseUnit;
|
private final List<Sample> measurements;
|
private final List<AvailableTag> availableTags;
|
|
FebsMetricResponse(String name, String description, String baseUnit, List<Sample> measurements, List<AvailableTag> availableTags) {
|
this.name = name;
|
this.description = description;
|
this.baseUnit = baseUnit;
|
this.measurements = measurements;
|
this.availableTags = availableTags;
|
}
|
|
public String getName() {
|
return this.name;
|
}
|
|
public String getDescription() {
|
return this.description;
|
}
|
|
public String getBaseUnit() {
|
return this.baseUnit;
|
}
|
|
public List<Sample> getMeasurements() {
|
return this.measurements;
|
}
|
|
public List<AvailableTag> getAvailableTags() {
|
return this.availableTags;
|
}
|
}
|
|
public static final class ListNamesResponse {
|
private final Set<String> names;
|
|
ListNamesResponse(Set<String> names) {
|
this.names = names;
|
}
|
|
public Set<String> getNames() {
|
return this.names;
|
}
|
}
|
}
|