Java tutorial
/** * Helios, OpenSource Monitoring * Brought to you by the Helios Development Group * * Copyright 2007, Helios Development Group and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. * */ package org.helios.netty.jmx; import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import static org.jboss.netty.handler.codec.http.HttpResponseStatus.OK; import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1; import java.lang.Thread.State; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.lang.management.MemoryUsage; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; import java.net.SocketAddress; import java.util.Collections; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import javax.management.Attribute; import javax.management.AttributeList; import javax.management.Notification; import javax.management.NotificationBroadcasterSupport; import javax.management.ObjectName; import org.apache.log4j.Logger; import org.helios.netty.ajax.PipelineModifier; import org.helios.netty.ajax.SharedChannelGroup; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelDownstreamHandler; import org.jboss.netty.channel.ChannelEvent; import org.jboss.netty.channel.ChannelFuture; import org.jboss.netty.channel.ChannelFutureListener; import org.jboss.netty.channel.ChannelHandler; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelPipeline; import org.jboss.netty.channel.ChannelUpstreamHandler; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.DownstreamMessageEvent; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.handler.codec.http.DefaultHttpResponse; import org.jboss.netty.handler.codec.http.HttpRequest; import org.jboss.netty.handler.codec.http.HttpResponse; import org.jboss.netty.util.CharsetUtil; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * <p>Title: MetricCollector</p> * <p>Description: Background task processor that periodically collects metrics and sends them to all active channels as a JSON object</p> * <p>Company: Helios Development Group LLC</p> * @author Whitehead (nwhitehead AT heliosdev DOT org) * <p><code>org.helios.netty.jmx.MetricCollector</code></p> */ public class MetricCollector extends NotificationBroadcasterSupport implements MetricCollectorMXBean, Runnable, PipelineModifier, ChannelDownstreamHandler, ChannelUpstreamHandler { /** The memory mx bean */ public static final MemoryMXBean memMxBean = ManagementFactory.getMemoryMXBean(); /** The thread mx bean */ public static final ThreadMXBean threadMxBean = ManagementFactory.getThreadMXBean(); /** The NIO Direct MXBean ObjectName */ public static final ObjectName directNio = JMXHelper.objectName("java.nio:type=BufferPool,name=direct"); /** The NIO attributes we are interested in */ public static final String[] NIO_ATTRS = new String[] { "Count", "MemoryUsed", "TotalCapacity" }; /** Indicates if we have the NIO MXBean */ protected final boolean haveNioMXBean; /** The period between collections */ protected long period = 5000; /** Serial number factory for thread names */ protected final AtomicInteger serial = new AtomicInteger(0); /** Serial number factory for notifications */ protected final AtomicLong tick = new AtomicLong(0); /** The ObjectName for the metric collector */ public static final ObjectName OBJECT_NAME = JMXHelper .objectName("org.helios.netty.jmx:service=MetricCollector"); /** A set of the unique metric names */ protected final Set<String> metricNames = new CopyOnWriteArraySet<String>(); /** Instance logger */ protected final Logger log = Logger.getLogger(getClass()); /** The schedule handle */ protected ScheduledFuture<?> handle = null; /** The scheduler */ protected final ScheduledThreadPoolExecutor scheduler; /** A map of remotely submitted metrics keyed by the address that they came from */ protected final Map<SocketAddress, Map<String, Long>> pendingRemoteMetrics = new ConcurrentHashMap<SocketAddress, Map<String, Long>>(); /** Dropped metric counter */ protected final AtomicLong dropCounter = new AtomicLong(0); /** The singleton instance */ private static volatile MetricCollector instance = null; /** The singleton ctor lock */ private static final Object lock = new Object(); /** * Acquires the singleton instance. First call wins on the period. * @param period The ms. between each metric collect. * @return The metric collector singleton */ public static MetricCollector getInstance(long period) { if (instance == null) { synchronized (lock) { if (instance == null) { instance = new MetricCollector(period); } } } return instance; } /** * Acquires the singleton instance. * @return The metric collector singleton */ public static MetricCollector getInstance() { if (instance == null) { throw new RuntimeException("The metric collector has not been initialized", new Throwable()); } return instance; } /** * Creates a new MetricCollector * @param period The period of collection */ private MetricCollector(long period) { super(); haveNioMXBean = ManagementFactory.getPlatformMBeanServer().isRegistered(directNio); this.period = period; try { ManagementFactory.getPlatformMBeanServer().registerMBean(this, OBJECT_NAME); scheduler = (ScheduledThreadPoolExecutor) Executors.newScheduledThreadPool(2, new ThreadFactory() { public Thread newThread(Runnable r) { Thread t = new Thread(r, OBJECT_NAME.getKeyProperty("service") + "Thread#" + serial.incrementAndGet()); t.setDaemon(true); return t; } }); initMetricNames(); scheduler.schedule(this, period, TimeUnit.MILLISECONDS); log.info("Started MetricCollector with a period of " + period + " ms."); } catch (Exception e) { throw new RuntimeException("Failed to create MetricCollector", e); } } /** * Submits a new metric and value * @param metricName The metric name * @param value The metric value */ public void submitMetric(String metricName, long value) { if (metricNames.add(metricName)) { JSONObject envelope = new JSONObject(); try { envelope.put("metric-names", new JSONArray(new String[] { metricName })); SharedChannelGroup.getInstance().write(envelope); SharedChannelGroup.getInstance() .write(packageMetricUpdate(Collections.singletonMap(metricName, value))); } catch (JSONException e) { log.error("Failed to submit metric", e); } } } /** * Submits a map of metrics from a remote socket submitter * @param clientSocket The address of the submitting socket * @param metrics A map of metric values keyed by metric name */ public void submitMetrics(SocketAddress clientSocket, Map<String, Long> metrics) { submitMetrics(metrics); if (pendingRemoteMetrics.put(clientSocket, metrics) != null) { dropCounter.incrementAndGet(); } } /** * Returns the number of dropped metrics * @return the number of dropped metrics */ public long getDroppedMetricCount() { return dropCounter.get(); } /** * Submits a map of metrics * @param metrics A map of metric values keyed by metric name */ public void submitMetrics(Map<String, Long> metrics) { Set<String> newNames = new HashSet<String>(); for (String s : metrics.keySet()) { if (metricNames.add(s)) { newNames.add(s); } } try { JSONObject envelope = new JSONObject(); envelope.put("metric-names", new JSONArray(newNames.toArray(new String[newNames.size()]))); SharedChannelGroup.getInstance().write(envelope); } catch (JSONException e) { log.error("Failed to submit metric names", e); } try { SharedChannelGroup.getInstance().write(packageMetricUpdate(metrics)); } catch (Exception e) { log.error("Failed to send metrics", e); } } /** * Packages the passed map of metrics into a JSONObject * @param metrics A map of metric values keyed by metric name * @return The JSONObject with the metrics */ protected JSONObject packageMetricUpdate(Map<String, Long> metrics) { try { final JSONObject top = new JSONObject(); final JSONObject envelope = new JSONObject(); envelope.put("metrics", top); for (Map.Entry<String, Long> entry : metrics.entrySet()) { insertMetric(entry.getKey(), entry.getValue(), top); } return envelope; } catch (Exception e) { log.error("Failed to package metric updates", e); return null; } } /** * Inserts the metric name keys and value into the passed JSONObject * @param metricName The metric name * @param value The metric value * @param top The JSONObject to insert into * @throws JSONException */ protected void insertMetric(String metricName, long value, JSONObject top) throws JSONException { String[] frags = metricName.split("\\."); int fc = frags.length; JSONObject current = top; for (int i = 0; i < fc; i++) { if (i == fc - 1) { current.put(frags[i], value); } else { JSONObject tmp = null; if (current.has(frags[i])) { tmp = current.getJSONObject(frags[i]); } else { tmp = new JSONObject(); current.put(frags[i], tmp); } current = tmp; } } } /** * The MetricCollector is not a real MetricProvider, but it needs to supply the names of the metrics * it published, so we add them to the registry here. */ protected void initMetricNames() { String[] memMetrics = new String[] { "[capacity(%)]", "[committed]", "[init]", "[max]", "[used]", "[consumed(%)]" }; for (String m : memMetrics) { metricNames.add("[heap]." + m); metricNames.add("[non-heap]." + m); } if (haveNioMXBean) { for (String s : NIO_ATTRS) { metricNames.add("direct-nio." + s); } } metricNames.add("thread-states*"); } /** * Executes the collection * {@inheritDoc} * @see java.lang.Runnable#run() */ public void run() { try { updateMetricNames(); long channelCount = SharedChannelGroup.getInstance().size(); //log.info("\n\tChanne,l Count:" + channelCount); submitMetric("netty.channels.count", channelCount); Notification notif = new Notification(MetricProvider.METRIC_NOTIFICATION, OBJECT_NAME, tick.incrementAndGet(), System.currentTimeMillis()); final JSONObject json = new JSONObject(); final JSONObject envelope = new JSONObject(); envelope.put("metrics", json); notif.setUserData(json); pushRemoteMetrics(json); json.put("ts", System.currentTimeMillis()); json.put("heap", processMemoryUsage(memMxBean.getHeapMemoryUsage())); json.put("non-heap", processMemoryUsage(memMxBean.getNonHeapMemoryUsage())); json.put("thread-states*", new JSONObject(getThreadStates())); if (haveNioMXBean) { json.put("direct-nio", new JSONObject(getNio())); } sendNotification(notif); SharedChannelGroup.getInstance().write(envelope); } catch (Exception e) { e.printStackTrace(System.err); } finally { scheduler.schedule(this, period, TimeUnit.MILLISECONDS); } } /** * Retrieves the unsubmitted remote metrics and packages them into the passed json object * @param json The JSON Object to package the metrics into */ protected void pushRemoteMetrics(final JSONObject json) { Map<SocketAddress, Map<String, Long>> tmpMap = new HashMap<SocketAddress, Map<String, Long>>( pendingRemoteMetrics); pendingRemoteMetrics.clear(); for (Map.Entry<SocketAddress, Map<String, Long>> entry : tmpMap.entrySet()) { for (Map.Entry<String, Long> mentry : entry.getValue().entrySet()) { try { insertMetric(mentry.getKey(), mentry.getValue(), json); } catch (JSONException e) { } } } try { insertMetric("metriccollector.drops", getDroppedMetricCount(), json); } catch (Exception e) { } tmpMap.clear(); } /** * Collects metric names from participating metric providers * @throws JSONException thrown on any json exception */ protected void updateMetricNames() throws JSONException { Notification notif = new Notification(MetricProvider.METRIC_NAME_NOTIFICATION, OBJECT_NAME, tick.incrementAndGet(), System.currentTimeMillis()); final Set<String> names = new HashSet<String>(); final Set<String> newNames = new HashSet<String>(); notif.setUserData(names); sendNotification(notif); for (String s : names) { if (metricNames.add(s)) { newNames.add(s); } } if (!newNames.isEmpty()) { JSONObject envelope = new JSONObject(); envelope.put("metric-names", new JSONArray(newNames)); SharedChannelGroup.getInstance().write(envelope); } } /** * Generates a {@link JSONObject} representing memory usage, plus the percentage usage of:<ul> * <li>Memory Allocated</li> * <li>Memory Maximum Capacity</li> * </ul> * @param usage The memory usage provided by the {@link MemoryMXBean} * @return A {@link JSONObject} * @throws JSONException I have no idea why this would be thrown */ protected JSONObject processMemoryUsage(MemoryUsage usage) throws JSONException { JSONObject json = new JSONObject(usage); json.put("consumed(%)", calcPercent(usage.getUsed(), usage.getCommitted())); json.put("capacity(%)", calcPercent(usage.getUsed(), usage.getMax())); return json; } /** * Returns a JSONArray of all the registered metric names * @return the metricNames */ public JSONObject getMetricNamesJSON() { JSONArray arr = new JSONArray(metricNames); JSONObject mn = new JSONObject(); try { mn.put("metric-names", arr); } catch (JSONException e) { e.printStackTrace(); } return mn; } /** * Returns a JSON string of all the registered metric names * @return the metricNames */ public String getMetricNames() { JSONArray arr = new JSONArray(metricNames); JSONObject mn = new JSONObject(); try { mn.put("metric-names", arr); } catch (JSONException e) { e.printStackTrace(); } return mn.toString(); } /** * Simple percentage calculator * @param part The part value * @param whole The whole value * @return The percentage that the part is of the whole as a long */ protected long calcPercent(double part, double whole) { if (part < 1 || whole < 1) return 0L; double d = part / whole * 100; return (long) d; } /** * Returns a simple map of NIO metrics * @return a simple map of NIO metrics */ protected Map<String, Long> getNio() { Map<String, Long> map = new HashMap<String, Long>(NIO_ATTRS.length); try { AttributeList attrs = ManagementFactory.getPlatformMBeanServer().getAttributes(directNio, NIO_ATTRS); for (Attribute attr : attrs.asList()) { map.put(attr.getName(), (Long) attr.getValue()); } } catch (Exception e) { e.printStackTrace(System.err); } return map; } /** * Collects the number of threads in each thread state * @return an EnumMap with Thread states as the key and the number of threads in that state as the value */ public EnumMap<Thread.State, AtomicInteger> getThreadStates() { EnumMap<Thread.State, AtomicInteger> map = new EnumMap<State, AtomicInteger>(Thread.State.class); for (ThreadInfo ti : threadMxBean.getThreadInfo(threadMxBean.getAllThreadIds())) { State st = ti.getThreadState(); AtomicInteger ai = map.get(st); if (ai == null) { ai = new AtomicInteger(0); map.put(st, ai); } ai.incrementAndGet(); } return map; } /** * Returns the collection period in ms. * @return the period */ public long getPeriod() { return period; } /** * Sets the collection period in ms. * @param period the period to set */ public void setPeriod(long period) { this.period = period; } /** * {@inheritDoc} * @see org.helios.netty.ajax.PipelineModifier#modifyPipeline(org.jboss.netty.channel.ChannelPipeline) */ @Override public void modifyPipeline(ChannelPipeline pipeline) { if (pipeline.get("metricnames") == null) { pipeline.addLast("metricnames", this); } } /** * {@inheritDoc} * @see org.helios.netty.ajax.PipelineModifier#getName() */ @Override public String getName() { return "metricnames"; } /** * {@inheritDoc} * @see org.helios.netty.ajax.PipelineModifier#getChannelHandler() */ @Override public ChannelHandler getChannelHandler() { return this; } /** * {@inheritDoc} * @see org.jboss.netty.channel.ChannelDownstreamHandler#handleDownstream(org.jboss.netty.channel.ChannelHandlerContext, org.jboss.netty.channel.ChannelEvent) */ @Override public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception { final Channel channel = e.getChannel(); if (!channel.isOpen()) return; if (!(e instanceof MessageEvent)) { ctx.sendDownstream(e); return; } Object message = ((MessageEvent) e).getMessage(); if (!(message instanceof JSONObject) && !(message instanceof CharSequence)) { ctx.sendDownstream(e); return; } HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); response.setContent(ChannelBuffers.copiedBuffer("\n" + message.toString() + "\n", CharsetUtil.UTF_8)); response.setHeader(CONTENT_TYPE, "application/json"); ChannelFuture cf = Channels.future(channel); cf.addListener(new ChannelFutureListener() { public void operationComplete(ChannelFuture f) throws Exception { channel.close(); } }); ctx.sendDownstream(new DownstreamMessageEvent(channel, cf, response, channel.getRemoteAddress())); } /** * {@inheritDoc} * @see org.jboss.netty.channel.ChannelUpstreamHandler#handleUpstream(org.jboss.netty.channel.ChannelHandlerContext, org.jboss.netty.channel.ChannelEvent) */ @Override public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception { if (e instanceof MessageEvent) { Object msg = ((MessageEvent) e).getMessage(); if (msg instanceof HttpRequest) { //ctx.sendDownstream(new DownstreamMessageEvent(e.getChannel(), Channels.future(e.getChannel()), getMetricNamesJSON(), ((MessageEvent) e).getRemoteAddress())); e.getChannel().write(getMetricNamesJSON()); } } ctx.sendUpstream(e); } }