ControlHandler.java

/* 
 * 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.esigate.server;

import java.io.IOException;
import java.io.Writer;
import java.lang.management.ManagementFactory;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;

import com.codahale.metrics.Counter;
import com.codahale.metrics.Gauge;
import com.codahale.metrics.Meter;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.Timer;

/**
 * Handle commands to the control port. Work in progress.
 * 
 * <p>
 * Commands
 * <ul>
 * <li>POST /shutdown</li>
 * <li>POST /status</li>
 * </ul>
 * 
 * @author Nicolas Richeton
 * 
 */
public class ControlHandler extends AbstractHandler {
    private static final String PREFIX_CONTEXT = "org.eclipse.jetty.webapp.WebAppContext.main.";
    private static final String PREFIX_THREAD_POOL = "org.eclipse.jetty.util.thread.QueuedThreadPool.esigate.";
    /**
     * Human-readable status
     */
    private static final String URL_STATUS = "/server-status";
    /**
     * Machine-readable status.
     * 
     * <p>
     * Sample :
     * 
     * <pre>
     * Total Accesses: 157678
     * Total kBytes: 176421
     * CPULoad: .0190435
     * Uptime: 2214828
     * ReqPerSec: .071192
     * BytesPerSec: 81.5662
     * BytesPerReq: 1145.72
     * BusyWorkers: 1
     * IdleWorkers: 4
     * </pre>
     */
    private final MetricRegistry registry;

    /**
     * Control handler for administration tasks.
     * 
     * @param registry
     *            metrics registry.
     */
    public ControlHandler(MetricRegistry registry) {
        this.registry = registry;
    }

    private static boolean fromControlConnection(Request serverRequest) {
        return EsigateServer.getControlPort() == serverRequest.getLocalPort();
    }

    /**
     * Perform shutdown.
     * 
     * @param port
     *            control handler port.
     */
    public static void shutdown(int port) {
        Http.doPOST("http://127.0.0.1:" + port + "/shutdown");
    }

    /**
     * Display status.
     * 
     * @param port
     *            control handler port.
     */
    public static void status(int port) {
        Http.doGET("http://127.0.0.1:" + port + URL_STATUS);
    }

    @Override
    public void handle(String target, Request serverRequest, HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {

        if (fromControlConnection(serverRequest)) {
            serverRequest.setHandled(true);

            switch (target) {

            case "/shutdown":
                if ("POST".equals(serverRequest.getMethod())) {
                    response.setStatus(HttpServletResponse.SC_OK);
                    stopServer();
                }
                break;

            case URL_STATUS:
                if ("GET".equals(serverRequest.getMethod())) {

                    if (request.getParameter("auto") != null) {
                        response.setStatus(HttpServletResponse.SC_OK);
                        try (Writer sos = response.getWriter()) {
                            Map<String, Object> status = getServerStatus();
                            for (String key : status.keySet()) {
                                sos.append(key).append(": ").append(String.valueOf(status.get(key))).append("\n");
                            }
                        }

                    } else {
                        response.setStatus(HttpServletResponse.SC_OK);
                        try (Writer sos = response.getWriter()) {
                            sos.append("Esigate Server Status\n");
                            Map<String, Object> status = getServerStatus();
                            for (String key : status.keySet()) {
                                sos.append(key).append(": ").append(String.valueOf(status.get(key))).append("\n");
                            }
                        }
                    }

                }
                break;

            default:
                response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
                break;
            }

        }

    }

    private Map<String, Object> getServerStatus() {
        Map<String, Object> result = new TreeMap<>();

        Map<String, Counter> counters = this.registry.getCounters();
        for (Entry<String, Counter> c : counters.entrySet()) {
            result.put(cleanupStatusKey(c.getKey()), String.valueOf(c.getValue().getCount()));
        }

        Map<String, Meter> meters = this.registry.getMeters();
        for (Entry<String, Meter> c : meters.entrySet()) {
            result.put(cleanupStatusKey(c.getKey()), String.valueOf(c.getValue().getCount()));
            result.put(cleanupStatusKey(c.getKey()) + "PerSec", String.valueOf(c.getValue().getOneMinuteRate()));
        }

        Map<String, Gauge> gauges = this.registry.getGauges();
        for (Entry<String, Gauge> c : gauges.entrySet()) {
            result.put(cleanupStatusKey(c.getKey()), String.valueOf(c.getValue().getValue()));
        }

        Map<String, Timer> timers = this.registry.getTimers();
        for (Entry<String, Timer> c : timers.entrySet()) {
            result.put(cleanupStatusKey(c.getKey()), String.valueOf(c.getValue().getOneMinuteRate()));
        }

        // Get total accesses
        Long accesses =
                meters.get(PREFIX_CONTEXT + "1xx-responses").getCount()
                        + meters.get(PREFIX_CONTEXT + "2xx-responses").getCount()
                        + meters.get(PREFIX_CONTEXT + "3xx-responses").getCount()
                        + meters.get(PREFIX_CONTEXT + "4xx-responses").getCount()
                        + meters.get(PREFIX_CONTEXT + "5xx-responses").getCount();
        result.put("Total Accesses", accesses);

        // Get ReqPerSec
        Double reqPerSec =
                meters.get(PREFIX_CONTEXT + "1xx-responses").getOneMinuteRate()
                        + meters.get(PREFIX_CONTEXT + "2xx-responses").getOneMinuteRate()
                        + meters.get(PREFIX_CONTEXT + "3xx-responses").getOneMinuteRate()
                        + meters.get(PREFIX_CONTEXT + "4xx-responses").getOneMinuteRate()
                        + meters.get(PREFIX_CONTEXT + "5xx-responses").getOneMinuteRate();
        result.put("ReqPerSec", reqPerSec);

        // Get uptime
        result.put("Uptime", ManagementFactory.getRuntimeMXBean().getUptime());

        // Get CPULoad
        Double cpuLoad = ManagementFactory.getOperatingSystemMXBean().getSystemLoadAverage();
        if (cpuLoad >= 0d) {
            result.put("CPULoad", cpuLoad);
        }

        return result;

    }

    /**
     * Remove unnecessary prefix from Metrics meters id.
     * 
     * @param s
     * @return
     */
    private static String cleanupStatusKey(String s) {
        String result = s;
        if (s.startsWith(PREFIX_CONTEXT)) {
            result = s.substring(PREFIX_CONTEXT.length());

        }

        if (s.startsWith(PREFIX_THREAD_POOL)) {
            result = s.substring(PREFIX_THREAD_POOL.length());
        }

        return result;
    }

    /**
     * Start a new thread to shutdown the server
     */
    private void stopServer() {
        // Get current server
        final Server targetServer = this.getServer();

        // Start a new thread in order to escape the destruction of this Handler
        // during the stop process.
        new Thread() {
            @Override
            public void run() {
                try {
                    targetServer.stop();
                } catch (Exception e) {
                    // ignore
                }
            }
        }.start();

    }
}