com.palantir.opensource.sysmon.linux.LinuxVMStatJMXWrapper.java Source code

Java tutorial

Introduction

Here is the source code for com.palantir.opensource.sysmon.linux.LinuxVMStatJMXWrapper.java

Source

//   Copyright 2011 Palantir Technologies
//
//   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.palantir.opensource.sysmon.linux;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.management.JMException;

import org.apache.commons.io.IOUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import com.palantir.opensource.sysmon.Monitor;
import com.palantir.opensource.sysmon.util.InterruptTimerTask;
import com.palantir.opensource.sysmon.util.JMXUtils;
import com.palantir.opensource.sysmon.util.PropertiesUtils;

/**
 * <p>
 * Monitors system performance statistics as generated by
 * <a href='http://linux.die.net/man/1/iostat'>vmstat</a>.</p>
 * <p>
 * <code>vmstat</code> reports information about processes, memory, paging, block IO,
 * traps, and cpu activity.
 *
 * <h3>JMX Data Path</h3>
 * <code>sysmon.linux.beanpath:type=linux-vmstat</code>
 * <p></p>
 *
 * <h3>Configuration parameters</h3>
 * <em>Note that any value not set in the config file will use the default value.</em>
 * <table cellspacing=5 cellpadding=5><tr><th>Config Key</th><th>Description</th><th>Default Value</th><th>Constant</th></tr>
 * <tr><td>sysmon.linux.vmstat.path</td>
 * <td>path to <code>vmstat</code> binary</td>
 * <td><code>vmstat</code></td>
 * <td>{@link #CONFIG_KEY_VMSTAT_PATH}</td></tr>
 * <tr><td>sysmon.linux.vmstat.opts</td>
 * <td>options passed to <code>vmstat</code></td>
 * <td><code>-n</code></td>
 * <td>{@link #CONFIG_KEY_VMSTAT_OPTIONS}</td></tr>
 * <tr><td>sysmon.linux.vmstat.period</td>
 * <td>period in seconds between <code>vmstat</code> data runs</td>
 * <td><code>60</code></td>
 * <td>{@link #CONFIG_KEY_VMSTAT_PERIOD}</td></tr>
 * </tr></table>
 *
 * @see Monitor Lifecycle documentation
 * @see <a href='http://linux.die.net/man/1/iostat'>vmstat(8) man page</a>
 */
public class LinuxVMStatJMXWrapper extends Thread implements Monitor {

    static final String CONFIG_KEY_PREFIX = LinuxMonitor.CONFIG_KEY_PREFIX + ".vmstat";
    /**
     * Path to vmstat executable. Defaults to "vmstat" (uses $PATH to find executable).
     * Set this config value in the to override where to find vmstat.
     *
     * Config key: {@value}
     * @see LinuxVMStatJMXWrapper#DEFAULT_VMSTAT_PATH default value for this config parameter
     * @see <a href='http://linux.die.net/man/1/iostat'>vmstat(8) on your local linux box</a>
     */
    public static final String CONFIG_KEY_VMSTAT_PATH = CONFIG_KEY_PREFIX + ".path";

    /**
     * Options passed to vmstat (other than period argument).
     * Set this key in config file to override defaults options.
     * Config key: {@value}
     * @see LinuxVMStatJMXWrapper#DEFAULT_VMSTAT_OPTIONS
     * @see <a href='http://linux.die.net/man/1/iostat'>vmstat(8) on your local linux box</a>
     *
     */
    public static final String CONFIG_KEY_VMSTAT_OPTIONS = CONFIG_KEY_PREFIX + ".opts";

    /**
     * Period for vmstat. Set this config value to override how often vmstat is outputting values.
     *
     * Config key: {@value}
     * @see LinuxVMStatJMXWrapper#DEFAULT_VMSTAT_PERIOD
     * @see <a href='http://linux.die.net/man/1/iostat'>vmstat(8) on your local linux box</a>
     */
    public static final String CONFIG_KEY_VMSTAT_PERIOD = CONFIG_KEY_PREFIX + ".period";

    /**
     * Defaults to using a bare path to allow $PATH to find the vmstat executable.
     * Default value: {@value}
     * @see LinuxVMStatJMXWrapper#CONFIG_KEY_VMSTAT_PATH how to override the default
     */
    public static final String DEFAULT_VMSTAT_PATH = "vmstat"; // let the shell figure it out
    /**
     * Default options for vmstat.
     * Default value: {@value}
     * @see LinuxVMStatJMXWrapper#CONFIG_KEY_VMSTAT_OPTIONS how to override the default
     *
     */
    public static final String DEFAULT_VMSTAT_OPTIONS = "-n";
    /**
     * Default for vmstat reporting period.
     * Default value: {@value}
     * @see LinuxVMStatJMXWrapper#CONFIG_KEY_VMSTAT_PERIOD how to override the default config value
     */
    public static final Integer DEFAULT_VMSTAT_PERIOD = new Integer(60);

    /**
     * Added to the bean path to find the place to hang the vmstat data.
     * @see LinuxMonitor#DEFAULT_JMX_BEAN_PATH for information on the bean path.
     */
    public static final String OBJECT_NAME = ":type=linux-vmstat";

    static final Logger log = LogManager.getLogger(LinuxVMStatJMXWrapper.class);

    /**
     * This bean will be mounted at the passed path with type of {@value}
     */
    public static final String BEAN_TYPE = ":type=linux-vmstat";

    final LinuxVMStat canonicalBean;
    final String beanPath;
    final String vmstatCmd[];
    final String vmstatPath;
    /**
     * Period, in seconds.
     */
    final Integer vmstatPeriod;
    volatile boolean shutdown = false;
    volatile Process vmstat = null;
    BufferedReader vmstatStdout = null;
    InputStream vmstatStderr = null;
    OutputStream vmstatStdin = null;

    /**
     * Constructs (but does not start) a new instance of this monitor.
     *
     * You must call {@link #startMonitoring()} to start the monitor and start taking data.
     *
     * @param config configuration for this monitor.  Will not be modified.
     * @throws JMException on an error with JMX server operations (usually related to MXBean naming).
     * @throws LinuxMonitoringException on misconfiguration of the monitor itself.
     */
    public LinuxVMStatJMXWrapper(Properties config) throws LinuxMonitoringException {
        super(LinuxVMStatJMXWrapper.class.getSimpleName());
        this.setDaemon(true);

        if (config == null) {
            // blank one to get all the defaults
            config = new Properties();
        }

        // default should already be set, but just to be safe
        final String _beanPath = config.getProperty(LinuxMonitor.CONFIG_KEY_JMX_BEAN_PATH,
                LinuxMonitor.DEFAULT_JMX_BEAN_PATH);
        this.beanPath = _beanPath + OBJECT_NAME;
        this.canonicalBean = new LinuxVMStat();
        try {
            JMXUtils.registerMBean(canonicalBean, beanPath);
        } catch (JMException e) {
            throw new LinuxMonitoringException("Exception while attempting register bean at path " + beanPath, e);
        }

        vmstatPath = config.getProperty(CONFIG_KEY_VMSTAT_PATH, DEFAULT_VMSTAT_PATH);
        try {
            final String vmstatOpts = config.getProperty(CONFIG_KEY_VMSTAT_OPTIONS, DEFAULT_VMSTAT_OPTIONS);
            vmstatPeriod = PropertiesUtils.extractInteger(config, CONFIG_KEY_VMSTAT_PERIOD, DEFAULT_VMSTAT_PERIOD);
            String cmd = vmstatPath + " " + vmstatOpts + " " + vmstatPeriod;
            String vmstatCmd[] = (cmd).split("\\s+");

            this.vmstatCmd = vmstatCmd;
        } catch (NumberFormatException e) {
            throw new LinuxMonitoringException("Invalid config entry for " + CONFIG_KEY_VMSTAT_PERIOD, e);
        }
    }

    public void startMonitoring() throws LinuxMonitoringException {
        startVMStat();
        start();
    }

    static final String FIRST_LINE_RE = "^\\s*procs -*memory-* -*swap-* -*io-* -*system-* -*cpu-*\\s*$";
    static final Pattern FIRST_LINE = Pattern.compile(FIRST_LINE_RE);
    static final String HEADER_LINE_RE = "^\\s*r\\s+b\\s+swpd\\s+free\\s+buff\\s+cache\\s+si\\s+so\\s+bi"
            + "\\s+bo\\s+in\\s+cs\\s+us\\s+sy\\s+id\\s+wa\\s*(st\\s*)?$";
    static final Pattern HEADER_LINE = Pattern.compile(HEADER_LINE_RE);

    static final Pattern VMSTAT_PAT;
    static {
        int numFields = 16;
        StringBuilder regex = new StringBuilder("^\\s*");
        for (int i = 0; i < numFields; i++) {
            regex.append("(\\d+)");
            if (i < numFields - 1) {
                regex.append("\\s+");
            } else {
                regex.append("\\s*");
            }
        }
        regex.append("(\\d+)?\\s*");
        regex.append("$");
        VMSTAT_PAT = Pattern.compile(regex.toString());
    }

    @Override
    public void run() {
        boolean wasInterrupted = Thread.interrupted();
        try {
            do {
                String line = null;
                try {
                    if (vmstatStdout.ready()) {
                        line = vmstatStdout.readLine();
                    }
                } catch (Exception e) {
                    line = null;
                    if (!shutdown) {
                        log.warn("Caught exception while reading line.", e);
                    } else {
                        log.debug("Shutdown caused exception.", e);
                    }
                }
                if (line != null) {
                    try {
                        LinuxVMStat dataBean = processLine(line);
                        canonicalBean.takeValues(dataBean); // updates the JMX bean
                        continue;
                    } catch (LinuxMonitoringException e) {
                        log.error(e);
                    }
                }
                // throttle data reads. That way, if/when things are broken, we don't loop hard
                // on an error condition, log errors at full throttle
                try {
                    if (!shutdown) {
                        Thread.sleep(1000L);
                    }
                } catch (InterruptedException e) {
                    wasInterrupted = true;
                    if (!shutdown) {
                        log.warn("Interrupted while doing throttled sleep.", e);
                    } else {
                        log.debug("Shutdown caused exception.", e);
                    }
                }
            } while (!shutdown);
        } finally {
            if (wasInterrupted)
                Thread.currentThread().interrupt();
            cleanup();
        }
    }

    LinuxVMStat processLine(String line) throws LinuxMonitoringException {
        Matcher m = VMSTAT_PAT.matcher(line);
        if (m.matches()) {
            LinuxVMStat rc = new LinuxVMStat();
            try {
                rc.runningProcesses = Integer.parseInt(m.group(1));
                rc.sleepingProcesses = Integer.parseInt(m.group(2));
                /*
                 * Memory
                 */
                rc.swappedMemory = Integer.parseInt(m.group(3));
                rc.freeMemory = Integer.parseInt(m.group(4));
                rc.buffersMemory = Integer.parseInt(m.group(5));
                rc.cacheMemory = Integer.parseInt(m.group(6));
                rc.swapIn = Integer.parseInt(m.group(7));
                rc.swapOut = Integer.parseInt(m.group(8));
                /*
                 * I/O
                 */
                rc.blocksRead = Integer.parseInt(m.group(9));
                rc.blocksWritten = Integer.parseInt(m.group(10));
                /*
                 * System
                 */
                rc.interrupts = Integer.parseInt(m.group(11));
                rc.contextSwitches = Integer.parseInt(m.group(12));
                /*
                 * CPU
                 */
                rc.userPercentCPU = Integer.parseInt(m.group(13));
                rc.sysPercentCPU = Integer.parseInt(m.group(14));
                rc.idlePercentCPU = Integer.parseInt(m.group(15));
                rc.waitPercentCPU = Integer.parseInt(m.group(16));
                // this may not be there
                if (m.groupCount() == 18) {
                    rc.stolenFromVMCPU = Integer.parseInt(m.group(17));
                }
            } catch (NumberFormatException e) {
                throw new LinuxMonitoringException("Encountered problems parsing integer out of line: " + line, e);
            }

            return rc;
        } else {
            throw new LinuxMonitoringException(
                    "Input line '" + line + "' did not match regex " + VMSTAT_PAT.pattern() + ", can't parse.");
        }
    }

    private void startVMStat() throws LinuxMonitoringException {
        InterruptTimerTask timer = InterruptTimerTask.setInterruptTimer(10000);
        try {

            vmstat = Runtime.getRuntime().exec(vmstatCmd);
            vmstatStdout = new BufferedReader(new InputStreamReader(vmstat.getInputStream()));
            vmstatStderr = vmstat.getErrorStream();
            vmstatStdin = vmstat.getOutputStream();

            // first line is discarded
            String firstLine = vmstatStdout.readLine();
            Matcher m = FIRST_LINE.matcher(firstLine);
            if (!m.matches()) {
                log.warn("vmstat returned unexpected first line: " + firstLine);
            }

            // make sure we're getting the fields we expect
            String headerLine = vmstatStdout.readLine();
            m = HEADER_LINE.matcher(headerLine);
            if (!m.matches()) {
                throw new LinuxMonitoringException("Header line does match expected header!" + "Expected: "
                        + HEADER_LINE + "\n" + "Got: " + headerLine + "\n");
            }

            // skip the average-since-last-reboot-line
            vmstatStdout.readLine();

        } catch (IOException e) {
            cleanup();
            if (e.getMessage().matches("^.*vmstat: not found.*$")) {
                final String errorMsg;
                // first case - absolute path
                if (!vmstatPath.equals(DEFAULT_VMSTAT_PATH)) {
                    errorMsg = "vmstat not found at specified path: " + vmstatPath
                            + ". Perhaps the procps package needs to be installed?";
                } else {
                    errorMsg = "vmstat not found in the executable $PATH for this process."
                            + " Perhaps the procps package needs to be installed?"
                            + " (Try 'yum install procps' as root.)";
                }
                throw new LinuxMonitoringException(errorMsg);
            }
            throw new LinuxMonitoringException("Error initializing vmstat", e);
        } finally {
            timer.cancel();
        }
    }

    private synchronized void cleanup() {
        if (vmstat != null) {
            vmstat.destroy();
        }

        IOUtils.closeQuietly(vmstatStdout);
        vmstatStdout = null;

        IOUtils.closeQuietly(vmstatStderr);
        vmstatStderr = null;

        IOUtils.closeQuietly(vmstatStdin);
        vmstatStdin = null;

        vmstat = null;
    }

    public synchronized void stopMonitoring() throws InterruptedException {
        shutdown = true;
        cleanup();
        join(this.vmstatPeriod * 1000 * 4);
        if (this.isAlive()) {
            log.error("background thread did not close in a reasonable amount of time");
        }
    }
}