Java tutorial
// 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"); } } }