All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.micrometer.core.instrument.binder.commonspool2.CommonsObjectPool2Metrics Maven / Gradle / Ivy

There is a newer version: 1.13.2
Show newest version
/*
 * Copyright 2020 VMware, Inc.
 *
 * 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
 *
 * https://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.micrometer.core.instrument.binder.commonspool2;

import io.micrometer.core.instrument.*;
import io.micrometer.core.instrument.binder.BaseUnits;
import io.micrometer.core.instrument.binder.MeterBinder;
import io.micrometer.core.instrument.util.NamedThreadFactory;
import io.micrometer.core.lang.NonNull;
import io.micrometer.core.lang.Nullable;
import io.micrometer.core.util.internal.logging.InternalLogger;
import io.micrometer.core.util.internal.logging.InternalLoggerFactory;

import javax.management.*;
import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.ToDoubleFunction;

import static java.util.Collections.emptyList;

/**
 * Apache Commons Pool 2.x metrics collected from metrics exposed via the MBeanServer.
 * Metrics are exposed for each object pool.
 *
 * @author Chao Chang
 * @since 1.6.0
 */
public class CommonsObjectPool2Metrics implements MeterBinder, AutoCloseable {

    private static final InternalLogger log = InternalLoggerFactory.getInstance(CommonsObjectPool2Metrics.class);

    private static final String JMX_DOMAIN = "org.apache.commons.pool2";

    private static final String METRIC_NAME_PREFIX = "commons.pool2.";

    private static final String[] TYPES = new String[] { "GenericObjectPool", "GenericKeyedObjectPool" };

    private final ExecutorService executor = Executors
            .newSingleThreadExecutor(new NamedThreadFactory("commons-pool-metrics-updater"));

    private final MBeanServer mBeanServer;

    private final Iterable tags;

    private final List notificationListenerCleanUpRunnables = new CopyOnWriteArrayList<>();

    public CommonsObjectPool2Metrics() {
        this(emptyList());
    }

    public CommonsObjectPool2Metrics(Iterable tags) {
        this(getMBeanServer(), tags);
    }

    public CommonsObjectPool2Metrics(MBeanServer mBeanServer, Iterable tags) {
        this.mBeanServer = mBeanServer;
        this.tags = tags;
    }

    private static MBeanServer getMBeanServer() {
        List mBeanServers = MBeanServerFactory.findMBeanServer(null);
        if (!mBeanServers.isEmpty()) {
            return mBeanServers.get(0);
        }
        return ManagementFactory.getPlatformMBeanServer();
    }

    @Override
    public void bindTo(@NonNull MeterRegistry registry) {
        for (String type : TYPES) {
            registerMetricsEventually(type, (o, tags) -> {
                registerGaugeForObject(registry, o, "NumIdle", "num.idle", tags,
                        "The number of instances currently idle in this pool", BaseUnits.OBJECTS);
                registerGaugeForObject(registry, o, "NumWaiters", "num.waiters", tags,
                        "The estimate of the number of threads currently blocked waiting for an object from the pool",
                        BaseUnits.THREADS);

                registerFunctionCounterForObject(registry, o, "CreatedCount", "created", tags,
                        "The total number of objects created for this pool over the lifetime of the pool",
                        BaseUnits.OBJECTS);
                registerFunctionCounterForObject(registry, o, "BorrowedCount", "borrowed", tags,
                        "The total number of objects successfully borrowed from this pool over the lifetime of the pool",
                        BaseUnits.OBJECTS);
                registerFunctionCounterForObject(registry, o, "ReturnedCount", "returned", tags,
                        "The total number of objects returned to this pool over the lifetime of the pool",
                        BaseUnits.OBJECTS);
                registerFunctionCounterForObject(registry, o, "DestroyedCount", "destroyed", tags,
                        "The total number of objects destroyed by this pool over the lifetime of the pool",
                        BaseUnits.OBJECTS);
                registerFunctionCounterForObject(registry, o, "DestroyedByEvictorCount", "destroyed.by.evictor", tags,
                        "The total number of objects destroyed by the evictor associated with this pool over the lifetime of the pool",
                        BaseUnits.OBJECTS);
                registerFunctionCounterForObject(registry, o, "DestroyedByBorrowValidationCount",
                        "destroyed.by.borrow.validation", tags,
                        "The total number of objects destroyed by this pool as a result of failing validation during borrowObject() over the lifetime of the pool",
                        BaseUnits.OBJECTS);

                registerTimeGaugeForObject(registry, o, "MaxBorrowWaitTimeMillis", "max.borrow.wait", tags,
                        "The maximum time a thread has waited to borrow objects from the pool");
                registerTimeGaugeForObject(registry, o, "MeanActiveTimeMillis", "mean.active", tags,
                        "The mean time objects are active");
                registerTimeGaugeForObject(registry, o, "MeanIdleTimeMillis", "mean.idle", tags,
                        "The mean time objects are idle");
                registerTimeGaugeForObject(registry, o, "MeanBorrowWaitTimeMillis", "mean.borrow.wait", tags,
                        "The mean time threads wait to borrow an object");
            });
        }
    }

    private Iterable nameTag(ObjectName name, String type)
            throws AttributeNotFoundException, MBeanException, ReflectionException, InstanceNotFoundException {
        Tags tags = Tags.of("name", name.getKeyProperty("name"), "type", type);
        if (Objects.equals(type, "GenericObjectPool")) {
            // for GenericObjectPool, we want to include the name and factoryType as tags
            String factoryType = mBeanServer.getAttribute(name, "FactoryType").toString();
            tags = Tags.concat(tags, "factoryType", factoryType);
        }
        return tags;
    }

    private void registerMetricsEventually(String type, BiConsumer perObject) {
        try {
            Set objs = mBeanServer.queryNames(new ObjectName(JMX_DOMAIN + ":type=" + type + ",*"), null);
            for (ObjectName o : objs) {
                Iterable nameTags = emptyList();
                try {
                    nameTags = nameTag(o, type);
                }
                catch (Exception e) {
                    log.error("exception in determining name tag", e);
                }
                perObject.accept(o, Tags.concat(tags, nameTags));
            }
        }
        catch (MalformedObjectNameException e) {
            throw new RuntimeException("Error registering commons pool2 based metrics", e);
        }

        registerNotificationListener(type, perObject);
    }

    /**
     * This notification listener should remain indefinitely since new pools can be added
     * at any time.
     * @param type The pool type to listen for.
     * @param perObject Metric registration handler when a new MBean is created.
     */
    private void registerNotificationListener(String type, BiConsumer perObject) {
        NotificationListener notificationListener =
                // in notification listener, we cannot get attributes for the registered
                // object,
                // so we do it later time in a separate thread.
                (notification, handback) -> {
                    executor.execute(() -> {
                        MBeanServerNotification mbs = (MBeanServerNotification) notification;
                        ObjectName o = mbs.getMBeanName();
                        Iterable nameTags = emptyList();
                        int maxTries = 3;
                        for (int i = 0; i < maxTries; i++) {
                            try {
                                Thread.sleep(1000);
                            }
                            catch (InterruptedException e) {
                                Thread.currentThread().interrupt();
                                throw new RuntimeException(e);
                            }
                            try {
                                nameTags = nameTag(o, type);
                                break;
                            }
                            catch (AttributeNotFoundException | MBeanException | ReflectionException
                                    | InstanceNotFoundException e) {
                                if (i == maxTries - 1) {
                                    log.error("can not set name tag", e);
                                }
                            }
                        }
                        perObject.accept(o, Tags.concat(tags, nameTags));
                    });
                };

        NotificationFilter filter = (NotificationFilter) notification -> {
            if (!MBeanServerNotification.REGISTRATION_NOTIFICATION.equals(notification.getType()))
                return false;
            ObjectName obj = ((MBeanServerNotification) notification).getMBeanName();
            return obj.getDomain().equals(JMX_DOMAIN) && obj.getKeyProperty("type").equals(type);
        };

        try {
            mBeanServer.addNotificationListener(MBeanServerDelegate.DELEGATE_NAME, notificationListener, filter, null);
            notificationListenerCleanUpRunnables.add(() -> {
                try {
                    mBeanServer.removeNotificationListener(MBeanServerDelegate.DELEGATE_NAME, notificationListener);
                }
                catch (InstanceNotFoundException | ListenerNotFoundException ignore) {
                }
            });
        }
        catch (InstanceNotFoundException ignore) {
            // unable to register MBean listener
        }
    }

    @Override
    public void close() {
        notificationListenerCleanUpRunnables.forEach(Runnable::run);
        executor.shutdown();
    }

    private void registerGaugeForObject(MeterRegistry registry, ObjectName o, String jmxMetricName, String meterName,
            Tags allTags, String description, @Nullable String baseUnit) {
        final AtomicReference gauge = new AtomicReference<>();
        gauge.set(Gauge
                .builder(METRIC_NAME_PREFIX + meterName, mBeanServer,
                        getJmxAttribute(registry, gauge, o, jmxMetricName))
                .description(description).baseUnit(baseUnit).tags(allTags).register(registry));
    }

    private void registerFunctionCounterForObject(MeterRegistry registry, ObjectName o, String jmxMetricName,
            String meterName, Tags allTags, String description, @Nullable String baseUnit) {
        final AtomicReference counter = new AtomicReference<>();
        counter.set(FunctionCounter
                .builder(METRIC_NAME_PREFIX + meterName, mBeanServer,
                        getJmxAttribute(registry, counter, o, jmxMetricName))
                .description(description).baseUnit(baseUnit).tags(allTags).register(registry));
    }

    private void registerTimeGaugeForObject(MeterRegistry registry, ObjectName o, String jmxMetricName,
            String meterName, Tags allTags, String description) {
        final AtomicReference timeGauge = new AtomicReference<>();
        timeGauge.set(TimeGauge
                .builder(METRIC_NAME_PREFIX + meterName, mBeanServer, TimeUnit.MILLISECONDS,
                        getJmxAttribute(registry, timeGauge, o, jmxMetricName))
                .description(description).tags(allTags).register(registry));
    }

    private ToDoubleFunction getJmxAttribute(MeterRegistry registry,
            AtomicReference meter, ObjectName o, String jmxMetricName) {
        return s -> safeDouble(() -> {
            if (!s.isRegistered(o)) {
                registry.remove(meter.get());
            }
            return s.getAttribute(o, jmxMetricName);
        });
    }

    private double safeDouble(Callable callable) {
        try {
            return Double.parseDouble(callable.call().toString());
        }
        catch (Exception e) {
            return Double.NaN;
        }
    }

}