blob: 47f0c402334ca18481aee3609e32098c9cadf4bd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.geronimo.microprofile.metrics.common.jaxrs;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonMap;
import static java.util.Optional.ofNullable;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;
import javax.ws.rs.GET;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.core.UriInfo;
import org.apache.geronimo.microprofile.metrics.common.prometheus.PrometheusFormatter;
import org.eclipse.microprofile.metrics.ConcurrentGauge;
import org.eclipse.microprofile.metrics.Counter;
import org.eclipse.microprofile.metrics.Gauge;
import org.eclipse.microprofile.metrics.Histogram;
import org.eclipse.microprofile.metrics.Metadata;
import org.eclipse.microprofile.metrics.Meter;
import org.eclipse.microprofile.metrics.Metered;
import org.eclipse.microprofile.metrics.Metric;
import org.eclipse.microprofile.metrics.MetricID;
import org.eclipse.microprofile.metrics.MetricRegistry;
import org.eclipse.microprofile.metrics.Snapshot;
import org.eclipse.microprofile.metrics.Timer;
@Path("metrics")
public class MetricsEndpoints {
private MetricRegistry baseRegistry;
private MetricRegistry vendorRegistry;
private MetricRegistry applicationRegistry;
private SecurityValidator securityValidator = new SecurityValidator() {
{
init();
}
};
private PrometheusFormatter prometheus = new PrometheusFormatter().enableOverriding();
public void setBaseRegistry(final MetricRegistry baseRegistry) {
this.baseRegistry = baseRegistry;
}
public void setVendorRegistry(final MetricRegistry vendorRegistry) {
this.vendorRegistry = vendorRegistry;
}
public void setApplicationRegistry(final MetricRegistry applicationRegistry) {
this.applicationRegistry = applicationRegistry;
}
public void setSecurityValidator(final SecurityValidator securityValidator) {
this.securityValidator = securityValidator;
}
public void setPrometheus(final PrometheusFormatter prometheus) {
this.prometheus = prometheus;
}
@GET
@Produces(MediaType.APPLICATION_JSON)
public Object getJson(@Context final SecurityContext securityContext,
@Context final UriInfo uriInfo) {
securityValidator.checkSecurity(securityContext, uriInfo);
return Stream.of(MetricRegistry.Type.values())
.collect(toMap(MetricRegistry.Type::getName, it -> findRegistry(it.getName()).getMetrics().entrySet().stream()
.collect(toMap(this::getKey, m -> toJson(map(m.getValue()), formatTags(m.getKey())), (a, b) -> a))));
}
@GET
@Produces(MediaType.TEXT_PLAIN)
public String getText(@Context final SecurityContext securityContext,
@Context final UriInfo uriInfo) {
securityValidator.checkSecurity(securityContext, uriInfo);
return Stream.of(MetricRegistry.Type.values())
.map(type -> {
final MetricRegistry metricRegistry = findRegistry(type.getName());
return prometheus.toText(metricRegistry, type.getName(), metrics(metricRegistry));
})
.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
.toString();
}
@GET
@Path("{registry}")
@Produces(MediaType.APPLICATION_JSON)
public Object getJson(@PathParam("registry") final String registry,
@Context final SecurityContext securityContext,
@Context final UriInfo uriInfo) {
securityValidator.checkSecurity(securityContext, uriInfo);
return findRegistry(registry).getMetrics().entrySet().stream()
.collect(toMap(this::getKey, it -> toJson(map(it.getValue()), formatTags(it.getKey())), (a, b) -> a));
}
@GET
@Path("{registry}")
@Produces(MediaType.TEXT_PLAIN)
public String getText(@PathParam("registry") final String registry,
@Context final SecurityContext securityContext,
@Context final UriInfo uriInfo) {
securityValidator.checkSecurity(securityContext, uriInfo);
final MetricRegistry metricRegistry = findRegistry(registry);
return prometheus.toText(metricRegistry, registry, metrics(metricRegistry)).toString();
}
@GET
@Path("{registry}/{metric}")
@Produces(MediaType.APPLICATION_JSON)
public Object getJson(@PathParam("registry") final String registry,
@PathParam("metric") final String name,
@Context final SecurityContext securityContext,
@Context final UriInfo uriInfo) {
securityValidator.checkSecurity(securityContext, uriInfo);
return singleEntry(name, findRegistry(registry), this::map);
}
@GET
@Path("{registry}/{metric}")
@Produces(MediaType.TEXT_PLAIN)
public String getText(@PathParam("registry") final String registry,
@PathParam("metric") final String name,
@Context final SecurityContext securityContext,
@Context final UriInfo uriInfo) {
securityValidator.checkSecurity(securityContext, uriInfo);
final MetricRegistry metricRegistry = findRegistry(registry);
return prometheus.toText(
metricRegistry, registry,
singleEntry(name, metricRegistry, identity())).toString();
}
@OPTIONS
@Path("{registry}/{metric}")
@Produces(MediaType.APPLICATION_JSON)
public Object getMetadata(@PathParam("registry") final String registry,
@PathParam("metric") final String name,
@Context final SecurityContext securityContext,
@Context final UriInfo uriInfo) {
securityValidator.checkSecurity(securityContext, uriInfo);
return ofNullable(findRegistry(registry).getMetadata().get(name))
.map(metric -> singletonMap(name, mapMeta(metric, new MetricID(name))))
.orElse(emptyMap());
}
@OPTIONS
@Path("{registry}")
@Produces(MediaType.APPLICATION_JSON)
public Object getMetadata(@PathParam("registry") final String registry,
@Context final SecurityContext securityContext,
@Context final UriInfo uriInfo) {
securityValidator.checkSecurity(securityContext, uriInfo);
return findRegistry(registry).getMetadata().entrySet().stream()
.collect(toMap(Map.Entry::getKey, e -> mapMeta(e.getValue(), new MetricID(e.getKey())), (a, b) -> a));
}
private Map<String, Metric> metrics(final MetricRegistry metricRegistry) {
return metricRegistry.getMetrics().entrySet().stream()
.collect(toMap(this::getKey, Map.Entry::getValue, (a, b) -> a));
}
private <T> Map<String, T> singleEntry(final String id, final MetricRegistry metricRegistry,
final Function<Metric, T> metricMapper) {
return ofNullable(metricRegistry.getMetrics().get(new MetricID(id)))
.map(metric -> singletonMap(id, metricMapper.apply(metric)))
.orElseGet(Collections::emptyMap);
}
private Meta mapMeta(final Metadata value, final MetricID metricID) {
return ofNullable(value).map(v -> new Meta(value, metricID)).orElse(null);
}
private Object map(final Metric metric) {
if (Counter.class.isInstance(metric)) {
return Counter.class.cast(metric).getCount();
}
if (Gauge.class.isInstance(metric)) {
return Gauge.class.cast(metric).getValue();
}
return metric;
}
private String getKey(final Map.Entry<MetricID, Metric> e) {
if (Counter.class.isInstance(e.getValue()) || Gauge.class.isInstance(e.getValue())) {
return e.getKey().getName() + formatTags(e.getKey());
}
return e.getKey().getName();
}
// https://github.com/eclipse/microprofile-metrics/issues/508
private Object toJson(final Object metric, final String nameSuffix) {
if (Timer.class.isInstance(metric)) {
final Timer meter = Timer.class.cast(metric);
final Map<Object, Object> map = new HashMap<>(15);
map.putAll(snapshot(meter.getSnapshot(), nameSuffix));
map.putAll(meter(meter, nameSuffix));
return map;
}
if (Meter.class.isInstance(metric)) {
return meter(Meter.class.cast(metric), nameSuffix);
}
if (Histogram.class.isInstance(metric)) {
final Histogram histogram = Histogram.class.cast(metric);
final Map<Object, Object> map = new HashMap<>(11);
map.putAll(snapshot(histogram.getSnapshot(), nameSuffix));
map.put("count" + nameSuffix, histogram.getCount());
return map;
}
if (ConcurrentGauge.class.isInstance(metric)) {
final ConcurrentGauge concurrentGauge = ConcurrentGauge.class.cast(metric);
final Map<Object, Object> map = new HashMap<>(3);
map.put("min" + nameSuffix, concurrentGauge.getMin());
map.put("current" + nameSuffix, concurrentGauge.getCount());
map.put("max" + nameSuffix, concurrentGauge.getMax());
return map;
}
// counters and gauges are unwrapped so skip it
return metric;
}
private Map<String, Object> meter(final Metered metered, final String nameSuffix) {
final Map<String, Object> map = new HashMap<>(5);
map.put("count" + nameSuffix, metered.getCount());
map.put("meanRate" + nameSuffix, metered.getMeanRate());
map.put("oneMinRate" + nameSuffix, metered.getOneMinuteRate());
map.put("fiveMinRate" + nameSuffix, metered.getFiveMinuteRate());
map.put("fifteenMinRate" + nameSuffix, metered.getFifteenMinuteRate());
return map;
}
private Map<String, Object> snapshot(final Snapshot snapshot, final String nameSuffix) {
final Map<String, Object> map = new HashMap<>(10);
map.put("p50" + nameSuffix, snapshot.getMedian());
map.put("p75" + nameSuffix, snapshot.get75thPercentile());
map.put("p95" + nameSuffix, snapshot.get95thPercentile());
map.put("p98" + nameSuffix, snapshot.get98thPercentile());
map.put("p99" + nameSuffix, snapshot.get99thPercentile());
map.put("p999" + nameSuffix, snapshot.get999thPercentile());
map.put("min" + nameSuffix, snapshot.getMin());
map.put("mean" + nameSuffix, snapshot.getMean());
map.put("max" + nameSuffix, snapshot.getMax());
map.put("stddev" + nameSuffix, snapshot.getStdDev());
return map;
}
private MetricRegistry findRegistry(final String registry) {
switch (Stream.of(MetricRegistry.Type.values()).filter(it -> it.getName().equals(registry)).findFirst()
.orElseThrow(() -> new WebApplicationException(Response.Status.NOT_FOUND))) {
case BASE:
return baseRegistry;
case VENDOR:
return vendorRegistry;
default:
return applicationRegistry;
}
}
private String formatTags(final MetricID id) {
return id.getTags().isEmpty() ? "" : (';' + id.getTags().entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(joining(",")));
}
public static class Meta {
private final Metadata value;
private final MetricID metricID;
private Meta(final Metadata value, final MetricID metricID) {
this.value = value;
this.metricID = metricID;
}
public String getName() {
return value.getName();
}
public String getDisplayName() {
return value.getDisplayName();
}
public String getDescription() {
return value.getDescription().orElse(null);
}
public String getType() {
return value.getType();
}
public String getTypeRaw() {
return value.getTypeRaw().name();
}
public String getUnit() {
return value.getUnit().orElse(null);
}
public boolean isReusable() {
return value.isReusable();
}
public String getTags() { // not sure why tck expect it, sounds worse than native getTags for clients
return metricID.getTags().entrySet().stream().map(e -> e.getKey() + '=' + e.getValue()).collect(joining(","));
}
}
}