/* 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.logback; import java.lang.management.ManagementFactory; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.management.MBeanServer; import javax.management.ObjectName; import ch.qos.logback.core.AppenderBase; import ch.qos.logback.classic.spi.LoggingEvent; import org.perf4j.GroupedTimingStatistics; import org.perf4j.helpers.AcceptableRangeConfiguration; import org.perf4j.helpers.MiscUtils; import org.perf4j.helpers.StatisticsExposingMBean; /** * This appender is designed to be attached to an {@link AsyncCoalescingStatisticsAppender}. It takes the incoming * GroupedTimingStatistics log messages and uses this data to update the value of a JMX MBean. The attributes on this * MBean can then be monitored by external tools. In addition, this class allows you to specify notification thresholds * so that a JMX notification is sent if one of the attributes falls outside an acceptable range (for example, if * the mean time for a specific value is too high). * * @author Alex Devine * @author Xu Huisheng */ public class JmxAttributeStatisticsAppender extends AppenderBase<LoggingEvent> { // --- configuration options --- /** * The object name of the MBean exposed through the JMX server. */ private String mBeanName = StatisticsExposingMBean.DEFAULT_MBEAN_NAME; /** * A comma separated list of the tag names to be exposed as JMX attributes. */ private String tagNamesToExpose; /** * A comma separated list of the notification thresholds, which controls whether JMX notifications are sent * when attribute values fall outside acceptable ranges. */ private String notificationThresholds; /** * When deploy log4j multi-times, default collision resolving behavior is do nothing and throw an Exception. */ private String collision = StatisticsExposingMBean.COLLISION_DONOTHING; // --- state variables --- /** * This is the MBean that is registered with the MBeanServer */ protected StatisticsExposingMBean mBean; // --- options --- /** * The <b>MBeanName</b> option is used to specify the ObjectName under which the StatisticsExposingMBean in the * MBeanServer. If not specified, defaults to org.perf4j:type=StatisticsExposingMBean,name=Perf4J. * * @return The value of the MBeanName option */ public String getMBeanName() { return mBeanName; } /** * Sets the value of the <b>MBeanName</b> option. This must be a valid JMX ObjectName. * * @param mBeanName The new value for the MBeanName option. */ public void setMBeanName(String mBeanName) { this.mBeanName = mBeanName; } /** * The <b>TagNamesToExpose</b> option is a comma-separated list of the tag names whose statistics values (e.g. * mean, min, max, etc.) should be exposed as MBeanAttributes. See the * {@link org.perf4j.helpers.StatisticsExposingMBean} for more details. * * @return The value of the TagNamesToExpose expose */ public String getTagNamesToExpose() { return tagNamesToExpose; } /** * Sets the value of the TagNamesToExpose option. * * @param tagNamesToExpose The new value for the TagNamesToExpose option. */ public void setTagNamesToExpose(String tagNamesToExpose) { this.tagNamesToExpose = tagNamesToExpose; } /** * The <b>NotificationThresholds</b> option is a comma-separated list of <i>acceptable range configurations</i>. * An acceptable range configuration specifies the values for which a particular timing statistic is considered * good. If the statistic falls outside of this range, then a JMX notification will be sent. * <p> * The format of an acceptable range configuration is <tt>tagNameStatName(range)</tt> where range can be one of * <tt><value</tt>, <tt>>value</tt>, or <tt>minValue-maxValue</tt>. For example, suppose the * TagNamesToExpose option was set to "databaseCall,fileWrite". This would cause the generated MBean to * expose the following attributes: * <ul> * <li>databaseCallMean * <li>databaseCallStdDev * <li>databaseCallMin * <li>databaseCallMax * <li>databaseCallCount * <li>databaseCallTPS * <li>fileWriteMean * <li>fileWriteStdDev * <li>fileWriteMin * <li>fileWriteMax * <li>fileWriteCount * <li>fileWriteTPS * </ul> * Suppose you wanted to have a JMX notification sent if the databaseCallMean is ever greater than 100ms, the * databaseCallMax is ever greater than 1000ms, the fileWriteMean is ever less than 5ms or greater than 200ms, * and the fileWriteTPS is ever less than 1 transaction per second. You would specify a NotificationThreshold as: * <pre>databaseCallMean(<100),databaseCallMax(<1000),fileWriteMean(5-200),fileWriteTPS(>1)</pre> * * @return The value of the NotificationThresholds option */ public String getNotificationThresholds() { return notificationThresholds; } /** * Sets the value of the NotificationThresholds option. * * @param notificationThresholds The new value for the NotificationThresholds option. */ public void setNotificationThresholds(String notificationThresholds) { this.notificationThresholds = notificationThresholds; } /** * the way to resolve mbean collision. * * @return DONOTHING, REPLACE, IGNORE */ public String getCollision() { return collision; } /** * the way to resolve mbean collision. * * @param collision DONOTHING, REPLACE, IGNORE */ public void setCollision(String collision) { this.collision = collision; } @Override public void start() { super.start(); if (tagNamesToExpose == null) { throw new RuntimeException("You must set the TagNamesToExpose option before activating this appender"); } //parse the options, create the mBean and register it String[] tagNames = MiscUtils.splitAndTrim(tagNamesToExpose, ","); List<AcceptableRangeConfiguration> rangeConfigs = new ArrayList<AcceptableRangeConfiguration>(); if (notificationThresholds != null) { String[] rangeConfigStrings = MiscUtils.splitAndTrim(notificationThresholds, ","); for (String rangeConfigString : rangeConfigStrings) { rangeConfigs.add(new AcceptableRangeConfiguration(rangeConfigString)); } } this.mBean = new StatisticsExposingMBean(mBeanName, Arrays.asList(tagNames), rangeConfigs); this.checkAndRegisterMBean(); } @Override public void stop() { try { MBeanServer mBeanServer = getMBeanServer(); mBeanServer.unregisterMBean(new ObjectName(mBeanName)); } catch (Exception e) { //fine, if we can't unregister it's not a big deal } super.stop(); } // --- appender interface methods --- @Override protected void append(LoggingEvent event) { if ((event.getArgumentArray() != null) && (event.getArgumentArray().length > 0)) { Object logMessage = event.getArgumentArray()[0]; if (logMessage instanceof GroupedTimingStatistics && (mBean != null)) { mBean.updateCurrentTimingStatistics((GroupedTimingStatistics) logMessage); } } } // --- helper methods --- /** * Gets the MBeanServer that should be used to register the StatisticsExposingMBean. Defaults to the Java Platform * MBeanServer. Subclasses could override this to use a different server. * * @return The MBeanServer to use for registrations. */ protected MBeanServer getMBeanServer() { return ManagementFactory.getPlatformMBeanServer(); } protected void checkAndRegisterMBean() { try { MBeanServer mBeanServer = getMBeanServer(); ObjectName oName = new ObjectName(mBeanName); if (StatisticsExposingMBean.COLLISION_DONOTHING.equals(this.collision)) { // DONOTHING. Dont check whether oName had bean existed. // if there was collision, just throw an Exception. mBeanServer.registerMBean(mBean, oName); } else if (StatisticsExposingMBean.COLLISION_REPLACE.equals(this.collision)) { // REPLACE. using new mBean to replace old one. if (mBeanServer.isRegistered(oName)) { mBeanServer.unregisterMBean(oName); } mBeanServer.registerMBean(mBean, oName); } else if (StatisticsExposingMBean.COLLISION_IGNORE.equals(this.collision)) { // IGNORE. if there was collision, still using old one, and dont throw Exception. if (!mBeanServer.isRegistered(oName)) { mBeanServer.registerMBean(mBean, oName); } } else { throw new RuntimeException("dont know have to handle collision type : [" + this.collision + "]. The valid options are DONOTHING, REPLACE, IGNORE."); } } catch (Exception e) { throw new RuntimeException("Error registering statistics MBean: " + e.getMessage(), e); } } }