org.helios.netty.jmx.MetricCollector.java Source code

Java tutorial

Introduction

Here is the source code for org.helios.netty.jmx.MetricCollector.java

Source

/**
 * 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);
    }

}