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

org.perf4j.helpers.StatisticsExposingMBean Maven / Gradle / Ivy

Go to download

Performance statistics logging and monitoring toolkit extension to log4j and the java.util.logging framework.

There is a newer version: 0.9.16
Show newest version
/* Copyright (c) 2008-2009 HomeAway, Inc.
 * All rights reserved.  http://www.perf4j.org
 *
 * 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 org.perf4j.helpers;

import org.perf4j.GroupedTimingStatistics;
import org.perf4j.TimingStatistics;

import javax.management.*;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class provides a wrapper around GroupedTimingStatistics data so that this performance information can be
 * exposed through JMX.
 *
 * @author Alex Devine
 */
public class StatisticsExposingMBean extends NotificationBroadcasterSupport implements DynamicMBean {
    /**
     * Logging classes use this as the default ObjectName of this MBean when registering it with an MBeanServer.
     */
    public static final String DEFAULT_MBEAN_NAME = "org.perf4j:type=StatisticsExposingMBean,name=Perf4J";
    /**
     * The type of the Notifications sent when a statistics value is outside of the acceptable range.
     */
    public static final String OUT_OF_RANGE_NOTIFICATION_TYPE = "org.perf4j.threshold.exceeded";

    /**
     * The name under which this MBean is registered in the MBean server.
     */
    protected ObjectName mBeanName;
    /**
     * This MBeanInfo exposes this MBean's management interface to the MBeanServer.
     */
    protected MBeanInfo managementInterface;
    /**
     * The tags whose statistics values are being exposed.
     */
    protected Collection tagsToExpose;
    /**
     * These AcceptableRangeConfigurations force a notification to be sent if a statistic is updated to a value
     * outside the allowable range. This Map maps acceptable ranges to whether or not the LAST check of the attribute
     * value was good or bad. This is used to ensure only a single notification is sent when an attribute crosses the
     * threshold to go out of range.
     */
    protected Map acceptableRanges;
    /**
     * This single thread pool is used to send notifications if any values are outside of the acceptable ranges
     * (this is necessary because the JMX spec states that the sendNotification method may be synchronous). This
     * member variable will be null if no acceptable ranges are specified.
     */
    protected ExecutorService outOfRangeNotifierThread;
    /**
     * This sequence number is required by the JMX Notification API.
     */
    protected long outOfRangeNotificationSeqNo;
    /**
     * The current underlying timing statistics whose values are exposed as MBean attributes.
     */
    protected GroupedTimingStatistics currentTimingStatistics;
    /**
     * Pattern used to parse requested attribute names into the tag name and the statistic name
     */
    protected Pattern attributeNamePattern = Pattern.compile("(.*)(Mean|StdDev|Min|Max|Count|TPS)");

    /**
     * Creates a new StatisticsExposingMBean whose management interface exposes performance attributes for the tags
     * specified, and that sends notifications if attributes are outside of the acceptable ranges.
     *
     * @param mBeanName        The name under which this MBean is registered in the MBean server
     * @param tagsToExpose     The names of the tags whose statistics should exposed. For each tag specified there will
     *                         be 6 attributes whose getters are exposed: tagNameMean, tagNameStdDev, tagNameMin,
     *                         tagNameMax, and tagNameCount and tagNameTPS
     * @param acceptableRanges These acceptable ranges are used to send notifications if any of the monitored
     *                         attributes go outside of the range.
     */
    public StatisticsExposingMBean(String mBeanName,
                                   Collection tagsToExpose,
                                   Collection acceptableRanges) {
        //set mBeanName
        if (mBeanName == null) {
            mBeanName = DEFAULT_MBEAN_NAME;
        }
        try {
            this.mBeanName = new ObjectName(mBeanName);
        } catch (MalformedObjectNameException mone) {
            throw new IllegalArgumentException(mone);
        }

        //set acceptableRanges
        if (acceptableRanges == null || acceptableRanges.isEmpty()) {
            this.acceptableRanges = Collections.emptyMap();
        } else {
            this.acceptableRanges = new LinkedHashMap();
            // initialize the last known value of the attribute as good
            for (AcceptableRangeConfiguration acceptableRange : acceptableRanges) {
                this.acceptableRanges.put(acceptableRange, Boolean.TRUE);
                //ensure the attributeName on the range is valid
                if (!attributeNamePattern.matcher(acceptableRange.getAttributeName()).matches()) {
                    throw new IllegalArgumentException(
                            "Acceptable range attribute name " + acceptableRange.getAttributeName()
                            + " invalid - must match pattern " + attributeNamePattern.pattern()
                    );
                }
            }
            this.outOfRangeNotifierThread = Executors.newSingleThreadExecutor();
        }

        this.tagsToExpose = new ArrayList(tagsToExpose);

        this.managementInterface = createMBeanInfoFromTagNames(tagsToExpose);

        this.currentTimingStatistics = new GroupedTimingStatistics(); //just set empty so it's never null
    }

    /**
     * This method should be called to update the underlying timing statistics, which will correspondingly change the
     * values of the exposed attributes.
     *
     * @param currentTimingStatistics The TimingStatistics to set, may not be null
     */
    public synchronized void updateCurrentTimingStatistics(GroupedTimingStatistics currentTimingStatistics) {
        if (currentTimingStatistics == null) {
            throw new IllegalArgumentException("timing statistics may not be null");
        }
        this.currentTimingStatistics = currentTimingStatistics;

        sendNotificationsIfValuesNotAcceptable();
    }

    /**
     * This MBean operation method allows the caller to add a tag whose statistics should be exposed as attributes
     * at runtime.
     *
     * @param tagName The name of the tag whose statistics should be exposed.
     */
    public void exposeTag(String tagName) {
        this.tagsToExpose.add(tagName);
        this.managementInterface = createMBeanInfoFromTagNames(this.tagsToExpose);
    }

    /**
     * This MBean operation method allows the caller to remove, at runtime, a tag whose statistics are exposed.
     *
     * @param tagName The name of the tag whose statistics should be removed as attributes from this MBean.
     * @return Whether or not the specified tag was previously exposed on this MBean.
     */
    public boolean removeTag(String tagName) {
        boolean retVal = this.tagsToExpose.remove(tagName);
        this.managementInterface = createMBeanInfoFromTagNames(this.tagsToExpose);
        return retVal;
    }

    public synchronized Object getAttribute(String attribute)
            throws AttributeNotFoundException, MBeanException, ReflectionException {
        Matcher matcher = attributeNamePattern.matcher(attribute);
        if (matcher.matches()) {
            String tagName = matcher.group(1);
            String statisticName = matcher.group(2);

            TimingStatistics timingStats = currentTimingStatistics.getStatisticsByTag().get(tagName);
            long windowLength = currentTimingStatistics.getStopTime() - currentTimingStatistics.getStartTime();

            return getStatsValueRetrievers().get(statisticName).getStatsValue(timingStats, windowLength);
        } else {
            throw new AttributeNotFoundException("No attribute named " + attribute);
        }

    }

    public void setAttribute(Attribute attribute)
            throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException {
        throw new AttributeNotFoundException("Statistics attributes are not writable");
    }

    public synchronized AttributeList getAttributes(String[] attributeNames) {
        AttributeList retVal = new AttributeList();
        for (String attributeName : attributeNames) {
            try {
                retVal.add(new Attribute(attributeName, getAttribute(attributeName)));
            } catch (Exception e) {
                //ignore - the absence of the attribute in the return list indicates there was an error
            }
        }
        return retVal;
    }

    public AttributeList setAttributes(AttributeList attributes) {
        //we don't support setting, so just return an empty list
        return new AttributeList();
    }

    public Object invoke(String actionName, Object[] params, String[] signature)
            throws MBeanException, ReflectionException {
        if ("exposeTag".equals(actionName)) {
            exposeTag(params[0].toString());
            return null;
        } else if ("removeTag".equals(actionName)) {
            return removeTag(params[0].toString());
        } else {
            throw new UnsupportedOperationException("Unsupported operation: " + actionName);
        }
    }

    public MBeanInfo getMBeanInfo() {
        return managementInterface;
    }

    public MBeanNotificationInfo[] getNotificationInfo() {
        return managementInterface.getNotifications();
    }

    /**
     * Overridable helper method gets the Map of statistic name to StatsValueRetriever.
     *
     * @return The StatsValueRetriever Map.
     */
    protected Map getStatsValueRetrievers() {
        return StatsValueRetriever.DEFAULT_RETRIEVERS;
    }

    /**
     * Helper method creates an MBeanInfo object that contains 6 read only attributes for each tag name, each
     * attribute representing a different statistic.
     *
     * @param tagNames The name of the tags whose statistics should be exposed as MBeanAttributes.
     * @return The MBeanInfo that represents the management interface for this MBean.
     */
    protected MBeanInfo createMBeanInfoFromTagNames(Collection tagNames) {
        MBeanAttributeInfo[] attributes = new MBeanAttributeInfo[tagNames.size() * getStatsValueRetrievers().size()];

        int i = 0;
        for (String tagName : tagNames) {
            for (Map.Entry statNameAndValueRetriever :
                    getStatsValueRetrievers().entrySet()) {
                String statName = statNameAndValueRetriever.getKey();
                StatsValueRetriever statsValueRetriever = statNameAndValueRetriever.getValue();

                attributes[i++] = new MBeanAttributeInfo(tagName + statName,
                                                         statsValueRetriever.getValueClass().getName(),
                                                         "Returns " + statName + " for tag " + tagName,
                                                         true /* readable */,
                                                         false /* not writable */,
                                                         false /* not "is" getter */);
            }
        }

        MBeanOperationInfo[] operations = new MBeanOperationInfo[2]; //exposeTag and removeTag
        operations[0] = new MBeanOperationInfo("exposeTag",
                                               "Allows the caller to add a monitored tag at runtime",
                                               new MBeanParameterInfo[]{
                                                       new MBeanParameterInfo("tagName",
                                                                              String.class.getName(),
                                                                              "The name of the tag to expose")
                                               },
                                               "void",
                                               MBeanOperationInfo.ACTION);
        operations[1] = new MBeanOperationInfo("removeTag",
                                               "Allows the caller to remove a monitored tag at runtime",
                                               new MBeanParameterInfo[]{
                                                       new MBeanParameterInfo("tagName",
                                                                              String.class.getName(),
                                                                              "The name of the tag to remove")
                                               },
                                               "boolean",
                                               MBeanOperationInfo.ACTION);

        MBeanNotificationInfo[] notificationInfos;
        if (acceptableRanges.isEmpty()) {
            //then we don't send any out-of-range notifications
            notificationInfos = new MBeanNotificationInfo[0];
        } else {
            notificationInfos = new MBeanNotificationInfo[]{
                    new MBeanNotificationInfo(
                            new String[]{OUT_OF_RANGE_NOTIFICATION_TYPE},
                            Notification.class.getName(),
                            "Notification sent if any statistics move outside of the specified acceptable ranges"
                    )
            };
        }

        return new MBeanInfo(getClass().getName(),
                             "Timing Statistics",
                             attributes,
                             null /* no constructors */,
                             operations,
                             notificationInfos);
    }

    /**
     * This helper method sends notifications if any of the acceptable ranges detects an attribute value that is
     * outside of the specified range. This method should only be called when the lock on this object's monitor is held.
     */
    protected void sendNotificationsIfValuesNotAcceptable() {
        //send notifications if any values are outside the acceptable range, but only if the LAST check was good
        for (Map.Entry acceptableRangeAndWasGood : acceptableRanges.entrySet()) {
            AcceptableRangeConfiguration acceptableRange = acceptableRangeAndWasGood.getKey();
            boolean lastCheckWasGood = acceptableRangeAndWasGood.getValue();

            double attributeValue;
            try {
                attributeValue = ((Number) getAttribute(acceptableRange.getAttributeName())).doubleValue();
            } catch (Exception e) {
                //shouldn't happen
                continue;
            }

            boolean isValueInRange = acceptableRange.isInRange(attributeValue);

            //update the lastCheckGood value and send the notification
            acceptableRangeAndWasGood.setValue(isValueInRange);

            if (lastCheckWasGood && !isValueInRange) {
                sendOutOfRangeNotification(attributeValue, acceptableRange);
            }
        }
    }

    /**
     * Helper method is used to send the JMX notification because the attribute value doesn't fall within the
     * acceptable range. This method should only be called when the lock on this object's monitor is held.
     *
     * @param attributeValue  The attribute value that falls outside the threshold
     * @param acceptableRange The AcceptableRangeConfiguration used to constrain the acceptable value
     */
    protected void sendOutOfRangeNotification(final double attributeValue,
                                              final AcceptableRangeConfiguration acceptableRange) {
        outOfRangeNotifierThread.execute(new Runnable() {
            public void run() {
                String errorMessage = "Attribute value " + attributeValue + " not in range " + acceptableRange;
                sendNotification(new Notification(OUT_OF_RANGE_NOTIFICATION_TYPE,
                                                  mBeanName,
                                                  ++outOfRangeNotificationSeqNo,
                                                  System.currentTimeMillis(),
                                                  errorMessage));
            }
        });
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy