/* * Copyright 2015-2017 JKOOL, LLC. * * 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 com.jkoolcloud.tnt4j.stream.jmx.scheduler; import java.io.IOException; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantLock; import javax.management.*; import javax.management.relation.MBeanServerNotificationFilter; import com.jkoolcloud.tnt4j.core.Activity; import com.jkoolcloud.tnt4j.core.PropertySnapshot; import com.jkoolcloud.tnt4j.stream.jmx.conditions.*; import com.jkoolcloud.tnt4j.stream.jmx.core.SampleContext; import com.jkoolcloud.tnt4j.stream.jmx.core.SampleListener; import com.jkoolcloud.tnt4j.stream.jmx.core.UnsupportedAttributeException; import com.jkoolcloud.tnt4j.utils.Utils; /** * <p> * This class provides implementation for handling sample/heart-beats generated by {@link Scheduler} class, which * handles implementation of all metric collection for each sample. * </p> * * @see Scheduler * @see SampleHandler * @see AttributeSample * @see AttributeCondition * * @version $Revision: 1 $ */ public class SampleHandlerImpl implements SampleHandler, NotificationListener { public static String STAT_NOOP_COUNT = "noop.count"; public static String STAT_SAMPLE_COUNT = "sample.count"; public static String STAT_TOTAL_ERROR_COUNT = "total.error.count"; public static String STAT_TOTAL_EXCLUDE_COUNT = "total.exclude.count"; public static String STAT_MBEAN_COUNT = "mbean.count"; public static String STAT_CONDITION_COUNT = "condition.count"; public static String STAT_LISTENER_COUNT = "listener.count"; public static String STAT_TOTAL_ACTION_COUNT = "total.action.count"; public static String STAT_TOTAL_METRIC_COUNT = "total.metric.count"; public static String STAT_LAST_METRIC_COUNT = "last.metric.count"; public static String STAT_SAMPLE_TIME_USEC = "sample.time.usec"; private final ReentrantLock lock = new ReentrantLock(); String mbeanIncFilter, mbeanExcFilter; long sampleCount = 0, totalMetricCount = 0, totalActionCount = 0; long lastMetricCount = 0, lastSampleTimeUsec = 0; long noopCount = 0, excCount = 0, errorCount = 0; MBeanServerConnection mbeanServer; SampleContext context; Throwable lastError; MBeanServerNotificationFilter MBeanFilter; List<ObjectName> iFilters = new ArrayList<>(5), eFilters = new ArrayList<>(5); Map<AttributeCondition, AttributeAction> conditions = new LinkedHashMap<AttributeCondition, AttributeAction>(89); ConcurrentHashMap<ObjectName, MBeanInfo> mbeans = new ConcurrentHashMap<ObjectName, MBeanInfo>(89); final List<SampleListener> listeners = new ArrayList<>(5); /** * Create new instance of {@code SampleHandlerImpl} with a given MBean server and a set of filters. * * @param mServerConn MBean server connection instance * @param incFilter MBean include filters semicolon separated * @param excFilter MBean exclude filters semicolon separated */ public SampleHandlerImpl(MBeanServerConnection mServerConn, String incFilter, String excFilter) { mbeanServer = mServerConn; mbeanIncFilter = incFilter; mbeanExcFilter = excFilter; context = new SampleContextImpl(this); } /** * Tokenize a given set of filters into JMX object names * * @param filter semicolon set of JMX filters * @param filters list of object names * @throws MalformedObjectNameException */ private static void tokenizeFilters(String filter, List<ObjectName> filters) throws MalformedObjectNameException { StringTokenizer itk = new StringTokenizer(filter, ";"); while (itk.hasMoreTokens()) { filters.add(new ObjectName(itk.nextToken())); } } /** * Install MBean add/delete listener * * @throws IOException * @throws InstanceNotFoundException */ private void listenForChanges() throws IOException, InstanceNotFoundException { if (MBeanFilter == null) { MBeanFilter = new MBeanServerNotificationFilter(); MBeanFilter.enableAllObjectNames(); mbeanServer.addNotificationListener(MBeanServerDelegate.DELEGATE_NAME, this, MBeanFilter, null); } } /** * Determine if a given object name matches include/exclusion filters * * @param oname object name * @return true if included, false otherwise */ public boolean isFilterIncluded(ObjectName oname) { for (ObjectName eFilter : eFilters) { if (eFilter.apply(oname)) { return false; } } for (ObjectName incFilter : iFilters) { if (incFilter.apply(oname)) { return true; } } return false; } /** * Load JMX beans based on a configured MBean filter list. All loaded MBeans are stored in {@link HashMap}. */ private void loadMBeans() { try { tokenizeFilters(mbeanIncFilter, iFilters); if (!Utils.isEmpty(mbeanExcFilter)) { tokenizeFilters(mbeanExcFilter, eFilters); } listenForChanges(); // run inclusion for (ObjectName nameFilter : iFilters) { Set<ObjectName> set = mbeanServer.queryNames(nameFilter, nameFilter); if (!eFilters.isEmpty()) { excludeFromSet(set, eFilters); } for (ObjectName oname : set) { mbeans.put(oname, mbeanServer.getMBeanInfo(oname)); runRegister(oname); } } } catch (Exception ex) { lastError = ex; doError(ex); } } /** * Exclude MBeans based on a list of exclude object name patterns * * @param objSet JMX object name set * @param eFilters list of MBean exclusions */ private static void excludeFromSet(Set<ObjectName> objSet, List<ObjectName> eFilters) { for (ObjectName ename : eFilters) { Iterator<ObjectName> it = objSet.iterator(); while (it.hasNext()) { ObjectName name = it.next(); if (ename.apply(name)) { it.remove(); } } } } /** * Sample MBeans based on a configured MBean filter list and store within given activity as snapshots. * * @param activity * instance where sampled MBean attributes are stored * @return number of metrics loaded from all MBeans */ private int sampleMBeans(Activity activity) { int pCount = 0; for (Entry<ObjectName, MBeanInfo> entry : mbeans.entrySet()) { ObjectName name = entry.getKey(); MBeanInfo info = entry.getValue(); MBeanAttributeInfo[] attr = info.getAttributes(); PropertySnapshot snapshot = new PropertySnapshot(name.getDomain(), name.getCanonicalName()); for (MBeanAttributeInfo jinfo : attr) { AttributeSample sample = AttributeSample.newAttributeSample(activity, snapshot, mbeanServer, name, jinfo); try { if (doPre(sample)) { sample.sample(); // obtain a sample doPost(sample); } } catch (Throwable ex) { doError(sample, ex); } finally { if (sample.excludeNext()) { excCount++; } evalAttrConditions(sample); } } if (snapshot.size() > 0) { pCount += snapshot.size(); activity.addSnapshot(snapshot); } } return pCount; } /** * Run and evaluate all registered conditions and invoke associated {@code MBeanAction} instances. * * @param sample MBean sample instance * @see AttributeSample */ protected void evalAttrConditions(AttributeSample sample) { for (Map.Entry<AttributeCondition, AttributeAction> entry : conditions.entrySet()) { if (entry.getKey().evaluate(sample)) { totalActionCount++; entry.getValue().action(context, entry.getKey(), sample); } } } /** * Finish processing of the activity sampling * * @param activity instance * @return snapshot instance containing metrics at the end of each sample */ private PropertySnapshot finish(Activity activity) { PropertySnapshot snapshot = new PropertySnapshot(activity.getName(), "SampleContext"); snapshot.add(STAT_NOOP_COUNT, noopCount); snapshot.add(STAT_SAMPLE_COUNT, sampleCount); snapshot.add(STAT_TOTAL_ERROR_COUNT, errorCount); snapshot.add(STAT_TOTAL_EXCLUDE_COUNT, excCount); snapshot.add(STAT_MBEAN_COUNT, mbeans.size()); snapshot.add(STAT_CONDITION_COUNT, conditions.size()); snapshot.add(STAT_LISTENER_COUNT, listeners.size()); snapshot.add(STAT_TOTAL_ACTION_COUNT, totalActionCount); snapshot.add(STAT_TOTAL_METRIC_COUNT, totalMetricCount); snapshot.add(STAT_LAST_METRIC_COUNT, lastMetricCount); snapshot.add(STAT_SAMPLE_TIME_USEC, lastSampleTimeUsec); // get custom statistics Map<String, Object> stats = new HashMap<String, Object>(); doStats(stats); snapshot.addAll(stats); activity.addSnapshot(snapshot); return snapshot; } @Override public void started(Activity activity) { lock.lock(); try { lastError = null; // reset last sample error runPre(activity); if ((!activity.isNoop()) && (mbeans.isEmpty())) { loadMBeans(); } else if (activity.isNoop()) { noopCount++; } } finally { lock.unlock(); } } @Override public void stopped(Activity activity) { if (!activity.isNoop()) { lock.lock(); try { long started = System.nanoTime(); sampleCount++; lastMetricCount = sampleMBeans(activity); totalMetricCount += lastMetricCount; lastSampleTimeUsec = (System.nanoTime() - started) / 1000; // run post listeners runPost(activity); if (activity.isNoop()) { noopCount++; } // compute sampling statistics finish(activity); } catch (Throwable ex) { doError(ex); } finally { lock.unlock(); } } } /** * Reset all counters maintained by sampling handler * * @return instance to the sampling context */ public SampleContext resetCounters() { lock.lock(); try { sampleCount = 0; totalMetricCount = 0; totalActionCount = 0; lastMetricCount = 0; lastSampleTimeUsec = 0; noopCount = 0; excCount = 0; errorCount = 0; lastError = null; return context; } finally { lock.unlock(); } } /** * Run {@link com.jkoolcloud.tnt4j.stream.jmx.core.SampleListener#register(SampleContext, ObjectName)} for all * registered listeners. * * @param name MBean object name */ private void runRegister(ObjectName name) { synchronized (this.listeners) { for (SampleListener lst : listeners) { lst.register(context, name); } } } /** * Run {@link com.jkoolcloud.tnt4j.stream.jmx.core.SampleListener#unregister(SampleContext, ObjectName)} for all * registered listeners. * * @param name MBean object name */ private void runUnRegister(ObjectName name) { synchronized (this.listeners) { for (SampleListener lst : listeners) { lst.unregister(context, name); } } } /** * Run {@link com.jkoolcloud.tnt4j.stream.jmx.core.SampleListener#post(SampleContext, Activity)} for all registered * listeners. * * @param activity sampling activity instance */ private void runPost(Activity activity) { synchronized (this.listeners) { for (SampleListener lst : listeners) { lst.post(context, activity); } } } /** * Run {@link com.jkoolcloud.tnt4j.stream.jmx.core.SampleListener#pre(SampleContext, Activity)} for all registered * listeners. * * @param activity sampling activity instance */ private void runPre(Activity activity) { synchronized (this.listeners) { for (SampleListener lst : listeners) { lst.pre(context, activity); } } } /** * Run * {@link com.jkoolcloud.tnt4j.stream.jmx.core.SampleListener#pre(com.jkoolcloud.tnt4j.stream.jmx.core.SampleContext, com.jkoolcloud.tnt4j.stream.jmx.conditions.AttributeSample)} * for all registered listeners. * * @param sample current attribute sample instance */ private boolean doPre(AttributeSample sample) { synchronized (this.listeners) { for (SampleListener lst : listeners) { lst.pre(context, sample); } } return !sample.excludeNext(); } /** * Run * {@link com.jkoolcloud.tnt4j.stream.jmx.core.SampleListener#post(com.jkoolcloud.tnt4j.stream.jmx.core.SampleContext, com.jkoolcloud.tnt4j.core.Activity)} * for all registered listeners. * * @param sample current attribute sample instance * @throws UnsupportedAttributeException */ private void doPost(AttributeSample sample) throws UnsupportedAttributeException { synchronized (this.listeners) { for (SampleListener lst : listeners) { lst.post(context, sample); } } } /** * Run {@link com.jkoolcloud.tnt4j.stream.jmx.core.SampleListener#error(SampleContext, AttributeSample)} for all * registered listeners. * * @param sample current attribute sample instance * @param ex exception associated with the error */ private void doError(AttributeSample sample, Throwable ex) { errorCount++; lastError = ex; sample.setError(ex); synchronized (this.listeners) { for (SampleListener lst : listeners) { lst.error(context, sample); } } } /** * Run {@link com.jkoolcloud.tnt4j.stream.jmx.core.SampleListener#error(SampleContext, Throwable)} for all * registered listeners. * * @param ex exception associated with the error */ private void doError(Throwable ex) { errorCount++; lastError = ex; synchronized (this.listeners) { for (SampleListener lst : listeners) { lst.error(context, ex); } } } /** * Run {@link com.jkoolcloud.tnt4j.stream.jmx.core.SampleListener#getStats(SampleContext, Map)} for all registered * listeners. * * @param stats map of key/value statistics */ private void doStats(Map<String, Object> stats) { synchronized (this.listeners) { for (SampleListener lst : listeners) { lst.getStats(context, stats); } } } @Override public SampleHandler register(AttributeCondition cond, AttributeAction action) { conditions.put(cond, (action == null ? NoopAction.NOOP : action)); return this; } @Override public SampleHandler register(AttributeCondition cond) { register(cond, NoopAction.NOOP); return this; } @Override public SampleHandler addListener(SampleListener listener) { listeners.add(listener); return this; } @Override public SampleHandler removeListener(SampleListener listener) { listeners.remove(listener); return this; } @Override public SampleContext getContext() { return context; } @Override public void handleNotification(Notification notification, Object handback) { if (notification instanceof MBeanServerNotification) { MBeanServerNotification mbeanEvent = (MBeanServerNotification) notification; if (mbeanEvent.getType().equalsIgnoreCase(MBeanServerNotification.REGISTRATION_NOTIFICATION)) { try { if (isFilterIncluded(mbeanEvent.getMBeanName())) { mbeans.put(mbeanEvent.getMBeanName(), mbeanServer.getMBeanInfo(mbeanEvent.getMBeanName())); runRegister(mbeanEvent.getMBeanName()); } } catch (Throwable ex) { doError(ex); } } else if (mbeanEvent.getType().equalsIgnoreCase(MBeanServerNotification.UNREGISTRATION_NOTIFICATION)) { mbeans.remove(mbeanEvent.getMBeanName()); runUnRegister(mbeanEvent.getMBeanName()); } } } }