/* 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 * @author Xu Huisheng */ 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"; /** * When mbean was deployed multi-times, just throw an Exception. */ public static final String COLLISION_DONOTHING = "DONOTHING"; /** * When mbean was deployed multi-times, using the new one to replace the old one. */ public static final String COLLISION_REPLACE = "REPLACE"; /** * When mbean was deployed multi-times, ignore the new one, still using the old one. */ public static final String COLLISION_IGNORE = "IGNORE"; /** * 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<String> 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<AcceptableRangeConfiguration, Boolean> 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<String> tagsToExpose, Collection<AcceptableRangeConfiguration> 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<AcceptableRangeConfiguration, Boolean>(); // 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<String>(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<String, StatsValueRetriever> 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<String> tagNames) { MBeanAttributeInfo[] attributes = new MBeanAttributeInfo[tagNames.size() * getStatsValueRetrievers().size()]; int i = 0; for (String tagName : tagNames) { for (Map.Entry<String, StatsValueRetriever> 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<AcceptableRangeConfiguration, Boolean> 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)); } }); } }