Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.airlift.openmetrics.MetricsResource Maven / Gradle / Ivy
/*
* Licensed 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 io.airlift.openmetrics;
import com.google.common.collect.ImmutableList;
import com.google.inject.Inject;
import io.airlift.log.Logger;
import io.airlift.node.NodeInfo;
import io.airlift.openmetrics.types.Counter;
import io.airlift.openmetrics.types.Gauge;
import io.airlift.openmetrics.types.Metric;
import io.airlift.openmetrics.types.Summary;
import io.airlift.stats.CounterStat;
import io.airlift.stats.TimeDistribution;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import org.weakref.jmx.MBeanExporter;
import org.weakref.jmx.ManagedClass;
import javax.management.InstanceNotFoundException;
import javax.management.IntrospectionException;
import javax.management.JMException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanInfo;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.ReflectionException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static java.util.Objects.requireNonNull;
@Path("/metrics")
public class MetricsResource
{
private static final Logger log = Logger.get(MetricsResource.class);
private static final String OPENMETRICS_CONTENT_TYPE = "application/openmetrics-text; version=1.0.0; charset=utf-8";
private static final String ATTRIBUTE_SEPARATOR = "_ATTRIBUTE_";
private static final String TYPE_SEPARATOR = "_TYPE_";
private static final String NAME_SEPARATOR = "_NAME_";
private static final Pattern METRIC_NAME_PATTERN = Pattern.compile("[[a-zA-Z]][\\w_]*");
private final MBeanServer mbeanServer;
private final MBeanExporter mbeanExporter;
private final List allMetricsObjectNames;
private final Map labels;
@Inject
public MetricsResource(MBeanServer mbeanServer, MBeanExporter mbeanExporter, MetricsConfig metricsConfig, NodeInfo nodeInfo)
{
this.mbeanServer = requireNonNull(mbeanServer, "mbeanServer is null");
this.mbeanExporter = requireNonNull(mbeanExporter, "mbeanExporter is null");
this.allMetricsObjectNames = metricsConfig.getJmxObjectNames();
this.labels = nodeInfo.getAnnotations();
}
@GET
@Produces(OPENMETRICS_CONTENT_TYPE)
public String getMetrics(@QueryParam("name[]") List filter)
{
StringBuilder body = new StringBuilder();
if (filter != null && !filter.isEmpty()) {
for (String metricName : filter) {
toMetricExposition(metricName).ifPresent(exposition -> body.append(exposition));
}
}
else {
body.append(managedMetricExpositions());
for (ObjectName metricObjectNames : allMetricsObjectNames) {
body.append(jmxMetricExpositions(metricObjectNames));
}
}
body.append("# EOF\n");
return body.toString();
}
private Set objectNamesFromMetricName(String metricName)
throws MalformedObjectNameException
{
int nameStart = metricName.indexOf(NAME_SEPARATOR);
int typeStart = metricName.indexOf(TYPE_SEPARATOR);
int attributeStart = metricName.indexOf(ATTRIBUTE_SEPARATOR);
String domain;
if (nameStart != -1) {
domain = metricName.substring(0, nameStart).replace("_", ".");
}
else if (typeStart != -1) {
domain = metricName.substring(0, typeStart).replace("_", ".");
}
else {
domain = metricName.substring(0, attributeStart).replace("_", ".");
}
StringBuilder objectNameBuilder = new StringBuilder(domain).append(":");
if (nameStart != -1) {
objectNameBuilder.append("name=")
.append(metricName, nameStart + NAME_SEPARATOR.length(), typeStart == -1 ? attributeStart : typeStart)
.append(",");
}
if (typeStart != -1) {
objectNameBuilder.append("type=")
.append(metricName.substring(typeStart + TYPE_SEPARATOR.length(), attributeStart).replace("_", "$"))
.append(",");
}
return mbeanServer.queryNames(ObjectName.getInstance(objectNameBuilder.append("*").toString()), null);
}
private String attributeNameFromMetricName(String metricName)
{
int attributeNameStart = metricName.indexOf(ATTRIBUTE_SEPARATOR);
if (attributeNameStart == -1) {
throw new RuntimeException("Metric name invalid, no attribute separator %s".formatted(metricName));
}
return metricName.substring(attributeNameStart + ATTRIBUTE_SEPARATOR.length()).replace("_", ".");
}
private String mBeanNameToMetricName(ObjectName objectName, String attributeName)
{
if (objectName.getDomain().contains("_")) {
log.warn("Unable to expose JMX metric with domain name %s, package names with underscores are unsupported.", objectName.getDomain());
throw new RuntimeException("Bad domain name %s".formatted(objectName.getDomain()));
}
StringBuilder metricNameBuilder = new StringBuilder("JMX_")
.append(objectName.getDomain().replace(".", "_"));
if (objectName.getKeyProperty("name") != null) {
metricNameBuilder.append(NAME_SEPARATOR)
.append(objectName.getKeyProperty("name"));
}
if (objectName.getKeyProperty("type") != null) {
metricNameBuilder.append(TYPE_SEPARATOR)
.append(objectName.getKeyProperty("type").replace("$", "_"));
}
metricNameBuilder.append(ATTRIBUTE_SEPARATOR)
.append(attributeName.replace(".", "_"));
String metricName = metricNameBuilder.toString();
if (!METRIC_NAME_PATTERN.matcher(metricName).matches()) {
log.warn("Calculated metric name has invalid characters %s skipping", metricName);
}
return metricName;
}
private Optional toMetricExposition(String metricName)
{
if (metricName.startsWith("JMX_")) {
final String jmxMetricName = metricName.substring(4);
try {
String attributeName = attributeNameFromMetricName(jmxMetricName);
return objectNamesFromMetricName(jmxMetricName).stream()
.map(objectName -> getMetric(objectName, attributeName, jmxMetricName, ""))
.flatMap(Optional::stream)
.map(Metric::getMetricExposition)
.findFirst();
}
catch (MalformedObjectNameException e) {
log.warn(e, "Unable to retrieve metric %s.", metricName);
return Optional.empty();
}
}
else {
Stream metricStream = getManagedMetricsStream();
return metricStream
.filter(metric -> metric.metricName().equals(metricName))
.findFirst()
.map(Metric::getMetricExposition);
}
}
private Optional getMetric(ObjectName objectName, String attributeName, String metricName, String description)
{
try {
Object attributeValue = mbeanServer.getAttribute(objectName, attributeName);
if (attributeValue == null) {
return Optional.empty();
}
if (attributeValue instanceof Number) {
return Optional.of(Gauge.from(metricName, (Number) attributeValue, labels, description));
}
return Optional.empty();
}
catch (JMException ex) {
log.debug(ex, "Unable to get metric for ObjectName %s and Attribute %s.", objectName.getCanonicalName(), attributeName);
return Optional.empty();
}
}
private String inferAttributesForObjectName(ObjectName objectName)
{
StringBuilder expositions = new StringBuilder();
try {
MBeanInfo mbeanInfo = mbeanServer.getMBeanInfo(objectName);
for (MBeanAttributeInfo mBeanAttributeInfo : mbeanInfo.getAttributes()) {
String attributeName = mBeanAttributeInfo.getName();
String description = mBeanAttributeInfo.getDescription();
try {
getMetric(objectName, attributeName, mBeanNameToMetricName(objectName, attributeName), description)
.ifPresent(value -> expositions.append(value.getMetricExposition()));
}
catch (RuntimeException e) {
log.debug(e, "Unable to get Metric for ObjectName %s and Attribute %s, skipping", objectName.getCanonicalName(), attributeName);
}
}
}
catch (InstanceNotFoundException | IntrospectionException | ReflectionException e) {
log.debug(e, "Unable to get MBeanInfo for object %s, skipping", objectName.getCanonicalName());
}
return expositions.toString();
}
private String sanitizeMetricName(String name)
{
return name.replace(".", "_")
.replace("$", "_")
.replace(":", "_")
.replace("=", "_")
.replace(",", "_")
.replace("-", "_");
}
private List getMetricsRecursively(String prefix, ManagedClass managedClass)
{
String metricName = sanitizeMetricName(prefix);
ImmutableList.Builder metrics = ImmutableList.builder();
for (String attributeName : managedClass.getAttributeNames()) {
try {
String metricAndAttribute = managedClass.isAttributeFlatten(attributeName) ? metricName : metricName + "_" + attributeName;
String attributeDescription = managedClass.getAttributeDescription(attributeName);
ManagedClass child = managedClass.getChildren().get(attributeName);
if (child != null) {
// The managed class is directly translatable to an openmetrics type, don't recurse any further
Optional metricFromTarget = getMetricFromTarget(child, metricAndAttribute, attributeDescription);
if (metricFromTarget.isPresent()) {
metrics.add(metricFromTarget.get());
}
else {
// Recurse this nested child
metrics.addAll(getMetricsRecursively(metricAndAttribute, child));
}
}
else {
// Attempt to infer a numeric gauge
Object attributeValue = managedClass.invokeAttribute(attributeName);
if (attributeValue instanceof Number) {
metrics.add(Gauge.from(metricAndAttribute, (Number) attributeValue, labels, attributeDescription));
}
}
}
catch (ReflectiveOperationException e) {
log.debug("Unable to invoke getter for managed attribute : " + attributeName);
}
}
return metrics.build();
}
private Optional getMetricFromTarget(ManagedClass managedClass, String metricName, String description)
{
Object target;
try {
target = managedClass.getTarget();
}
catch (IllegalStateException ignored) {
return Optional.empty();
}
if (target instanceof CounterStat counterStat) {
return Optional.of(Counter.from(metricName, counterStat, labels, description));
}
if (target instanceof TimeDistribution timeDistribution) {
return Optional.of(Summary.from(metricName, timeDistribution, labels, description));
}
return Optional.empty();
}
private String jmxMetricExpositions(ObjectName initialObjectName)
{
StringBuilder stringBuilder = new StringBuilder();
mbeanServer.queryNames(initialObjectName, null).forEach(objectName -> stringBuilder.append(inferAttributesForObjectName(objectName)));
return stringBuilder.toString();
}
private Stream getManagedMetricsStream()
{
Map managedClasses = this.mbeanExporter.getManagedClasses();
return managedClasses.keySet().stream()
.map(objectName -> getMetricsRecursively(objectName, managedClasses.get(objectName)))
.flatMap(List::stream);
}
private String managedMetricExpositions()
{
StringBuilder builder = new StringBuilder();
getManagedMetricsStream().forEach(metric -> builder.append(metric.getMetricExposition()));
return builder.toString();
}
}