/* The contents of this file are subject to the terms * of the Common Development and Distribution License * (the License). You may not use this file except in * compliance with the License. * * You can obtain a copy of the License at * http://www.sun.com/cddl/cddl.html or * install_dir/legal/LICENSE * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at install_dir/legal/LICENSE. * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * */ package com.sun.tools; import com.sun.faban.common.Command; import com.sun.faban.common.CommandHandle; import com.sun.faban.common.NameValuePair; import com.sun.faban.common.TextTable; import com.sun.faban.harness.ConfigurationException; import com.sun.faban.harness.Context; import com.sun.faban.harness.Configure; import com.sun.faban.harness.Start; import com.sun.faban.harness.Stop; import com.sun.faban.harness.tools.ToolContext; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.net.Socket; import java.text.SimpleDateFormat; import java.text.DecimalFormat; import java.text.FieldPosition; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; /** * MemcacheStats.java * This program implements a tool to collect memcache stats from * a list of servers. The stats are collected and displayed at the * specified interval. Most of the stats are on a per. second basis * allowing easy tabulation and comparison with other tools. * * @author Shanti Subramanyam based on work by Kim LiChong * modified by Sheetal Patil * */ public class MemcacheStats { private static StatsClient cache = null; private static Logger logger = Logger.getLogger(MemcacheStats.class.getName()); TextTable outputTextTable = null; int interval; private SimpleDateFormat df = new SimpleDateFormat("HH:mm:ss"); /** Injected tool context. */ @Context public ToolContext ctx; Command cmd; CommandHandle processRef; ArrayList<String> toolCmd; String toolName; private Timer timer; private PrintWriter out; String[] memCacheServers; /** * This constructor creates a memcache client with a pool of servers. * This is for standalone invocation from main. * * @param servers Name of servers running memcached * @param interval Interval in secs for stats collection * @throws IOException Error communicating with memcached */ public MemcacheStats(String servers[], int interval) throws IOException { this.interval = interval; cache = new StatsClient(servers); } /** * Constructs the memcache stats from the tool agent. A no-arg constructor * is required in this case. Arguments are passed via calling the configure * method. */ public MemcacheStats() { } /* * Columns in which the various stats fields will appear */ private static final int CURTIME = 1; private static final int CUR_ITMS = 2; private static final int BYTES = 3; private static final int CUR_CONNS = 4; private static final int SETS = 5; private static final int GETS = 6; private static final int GET_HITS = 7; private static final int GET_MISSES = 8; private static final int EVICTS = 9; private static final int BYTES_R = 10; private static final int BYTES_W = 11; private static final int NUM_COLS = 11; long[][] previousStats; /** * This method is used for dynamic memcache stats gathering. * We only gather and print out the following stats in a single row: * cur_itms bytes cur_conns sets gets get_hits get_misses evicts bytes_r, bytes_w * Since memcached returns cumulative stats, we do the subtraction to get * the stats for this interval and then figure out stats/sec * @return TextTable - a single row of stats */ public TextTable getStats() { Integer intval = 0; long longval = 0; double dblval = 0; DecimalFormat decval = new DecimalFormat("0.00"); FieldPosition fld = new FieldPosition(DecimalFormat.INTEGER_FIELD); Map memcacheStats = cache.stats(); //cache.stats() will return a Map whose key is the name of the memcache server //and whose value is a Map with the memcache statistics Set<Map.Entry> serverEntries = memcacheStats.entrySet(); //set counter to allow to set number of columns to output int row = 0; //reset the iterator for (Map.Entry serverEntry : serverEntries) { String key = (String) serverEntry.getKey(); if (key == null) break; Map statsMap = (Map) serverEntry.getValue(); // get rid of the ":<port>" String srv = key.substring(0, key.indexOf(":")); if (srv == null) break; if (outputTextTable == null) { // the number of rows is #of servers (for each interval) // One extra column for server name outputTextTable = new TextTable(serverEntries.size(), NUM_COLS + 1); // the number of columns is the stats that we gather //set Header outputTextTable.setHeader(0, "Server"); outputTextTable.setHeader(CURTIME, "Time"); outputTextTable.setHeader(CUR_ITMS, "items"); outputTextTable.setHeader(BYTES, "cache_MB"); outputTextTable.setHeader(CUR_CONNS, "conns"); outputTextTable.setHeader(SETS, "sets/s"); outputTextTable.setHeader(GETS, "gets/s"); outputTextTable.setHeader(GET_HITS, "get_hits/s"); outputTextTable.setHeader(GET_MISSES, "get_misses/s"); outputTextTable.setHeader(EVICTS, "evicts/s"); outputTextTable.setHeader(BYTES_R, "rB/s"); outputTextTable.setHeader(BYTES_W, "wB/s"); previousStats = new long[serverEntries.size()][NUM_COLS+1]; } outputTextTable.setField(row, 0, key); //get this value's iterator Set<Map.Entry> statsMapEntries = statsMap.entrySet(); // Populate the rest of the table. for (Map.Entry statsMapEntry : statsMapEntries) { StringBuffer str = new StringBuffer(); String fldKey = statsMapEntry.getKey().toString(); String fldValue = ((CharSequence)(statsMapEntry.getValue())).toString(); logger.fine("key = " + fldKey + ", value = " + fldValue); /* * NOTE: Although it seems that we're doing a portion of the same * code (setting val, curVal) in all of the if statements, we can't * take it out in a common way. This is because other stats that * we're not looking at are not integers. */ /* We do absolute stats for CUR_ITMS,BYTES and CUR_CONNS */ if (fldKey.equals("time")) { longval = Long.parseLong(fldValue) * 1000; // sec to ms outputTextTable.setField(row, CURTIME, df.format(new Date(longval))); } else if (fldKey.equals("curr_items")) { intval = Integer.parseInt(fldValue); outputTextTable.setField(row, CUR_ITMS, intval.toString()); } else if (fldKey.equals("bytes")) { // bytes can be large, so store in long - convert to MBs longval = Long.parseLong(fldValue); dblval = (double)longval / 1000000; decval.format(dblval, str, fld); outputTextTable.setField(row, BYTES, str); } else if (fldKey.equals("curr_connections")) { intval = Integer.parseInt(fldValue); outputTextTable.setField(row, CUR_CONNS, intval.toString()); } else if (fldKey.equals("cmd_set")) { longval = Long.parseLong(fldValue); dblval = (double)(longval - previousStats[row][SETS])/interval; decval.format(dblval, str, fld); outputTextTable.setField(row, SETS, str); previousStats[row][SETS] = longval; } else if (fldKey.equals("cmd_get")) { longval = Long.parseLong(fldValue); dblval = (double)(longval - previousStats[row][GETS])/interval; decval.format(dblval, str, fld); outputTextTable.setField(row, GETS, str); previousStats[row][GETS] = longval; } else if (fldKey.equals("get_hits")) { longval = Long.parseLong(fldValue); dblval = (double)(longval - previousStats[row][GET_HITS])/interval; decval.format(dblval, str, fld); outputTextTable.setField(row, GET_HITS, str); previousStats[row][GET_HITS] = longval; } else if (fldKey.equals("get_misses")) { longval = Long.parseLong(fldValue); dblval = (double)(longval - previousStats[row][GET_MISSES])/interval; decval.format(dblval, str, fld); outputTextTable.setField(row, GET_MISSES, str); previousStats[row][GET_MISSES] = longval; } else if (fldKey.equals("evictions")) { longval = Long.parseLong(fldValue); intval = (int)((longval - previousStats[row][EVICTS])/interval); outputTextTable.setField(row, EVICTS, intval.toString()); previousStats[row][EVICTS] = longval; } else if (fldKey.equals("bytes_read")) { longval = Long.parseLong(fldValue); intval = (int)((longval - previousStats[row][BYTES_R])/interval); outputTextTable.setField(row, BYTES_R, intval.toString()); previousStats[row][BYTES_R] = longval; } else if (fldKey.equals("bytes_written")) { longval = Long.parseLong(fldValue); intval = (int)((longval - previousStats[row][BYTES_W])/interval); outputTextTable.setField(row, BYTES_W, intval.toString()); previousStats[row][BYTES_W] = longval; } // Some version of memcached do not have evicts. if (outputTextTable.getField(row, EVICTS) == null) outputTextTable.setField(row, EVICTS, "-"); } row++; } return outputTextTable; } /** * This main method is used to gather dynamic statistics on memcache server instances. * The primary arguments are the names of the memcached servers * It accepts the following optional argument: * -i interval the snapshot period to collect the stats, in seconds (default 10) * Usage: java com.sun.faban.harnes.tools.MemcacheStats host[:port]... [-i interval] * It creates an instance of MemcacheStats and sets up a timer task * at the specified interval to gather the stats. * @param args String [] * */ public static void main(String[] args) { int intervalTime = 10000; // in msecs LinkedHashSet<String> serverSet = new LinkedHashSet<String>(); if (args==null || args.length < 1) { System.err.println("Usage: java com.sun.faban.harness.tools.MemcacheStats host[:port]... [-i interval]"); System.exit(1); } for (int i = 0; i < args.length; i++) { if (args[i].startsWith("-i")) { if (args[i].length() > 2) // -iarg { intervalTime = Integer.parseInt(args[i].substring(2)) * 1000; } else // -i arg { intervalTime = Integer.parseInt(args[++i]) * 1000; } } else if (args[i].contains(":")) {// host:port pair serverSet.add(args[i]); } else { // host only. Append default port 11211. serverSet.add(args[i] + ":11211"); } } //finished processing all of the args. populate server list String memCacheServers[] = new String[serverSet.size()]; memCacheServers = serverSet.toArray(memCacheServers); /*logger.fine("Starting memcache stats"); MemcacheStats memcacheStats = new MemcacheStats(memCacheServers, intervalTime/1000); try { Timer timer = new Timer(); MemCacheTask task = new MemCacheTask(memcacheStats); timer.scheduleAtFixedRate(task, 0, intervalTime); } catch (Exception ex) { logger.severe("Exception in setting up timer " + ex); logger.log(Level.FINE, "Exception", ex); return; }*/ } /** * Configures this MemcacheStats tool. */ @Configure public void configure() throws ConfigurationException { LinkedHashSet<String> serverSet = new LinkedHashSet<String>(); List<String> toolArgs = ctx.getToolArgs(); if(toolArgs == null){ throw new ConfigurationException("MemcacheStats toolArgs is not provided"); } List<NameValuePair<Integer>> myHostPorts = ctx.getServiceContext().getUniqueHostPorts(); if(myHostPorts == null){ throw new ConfigurationException("Memcached host:port is not provided"); } for(NameValuePair<Integer> myHostPort : myHostPorts){ if(myHostPort.value == null) myHostPort.value = 11211; serverSet.add(myHostPort.name +":"+myHostPort.value); } String[] args = new String[toolArgs.size()]; for(int i=0; i < toolArgs.size(); i++){ args[i] = toolArgs.get(i); } int intervalTime = 10000; // in msecs if (args==null || args.length < 1) { logger.log(Level.INFO, "MemcacheStats [-i interval]"); } for (int i = 0; i < args.length; i++) { if (args[i].startsWith("-i")) { if (args[i].length() > 2) // -iarg { intervalTime = Integer.parseInt(args[i].substring(2)) * 1000; } else // -i arg { intervalTime = Integer.parseInt(args[++i]) * 1000; } } } //finished processing all of the args. populate server list memCacheServers = new String[serverSet.size()]; memCacheServers = serverSet.toArray(memCacheServers); interval = intervalTime/1000; // Trim host:ports down to the ones pertaining to this host. // Parse interval from the mcstat command from the tool context. // store interval/1000 and host ports in field interval and ports. } /** * Starts monitoring the memcached instances. * @throws IOException Error communicating with memcached */ @Start public void start() throws IOException { logger.fine("Starting memcache stats"); // MemcacheStats memcacheStats = new MemcacheStats(memCacheServers, intervalTime/1000); cache = new StatsClient(memCacheServers); try { timer = new Timer(); out = new PrintWriter(ctx.getOutputFile()); MemCacheTask task = new MemCacheTask(this, out); timer.scheduleAtFixedRate(task, 0, interval); } catch (Exception ex) { logger.severe("Exception in setting up timer " + ex); logger.log(Level.FINE, "Exception", ex); return; } } /** * Stops monitoring the memcached instances. */ @Stop public void stop() { timer.cancel(); out.flush(); out.close(); } /* class for TimerTask */ private static class MemCacheTask extends TimerTask { private MemcacheStats memcacheStats; private PrintWriter out; public MemCacheTask(MemcacheStats memcacheStats, PrintWriter out) { this.memcacheStats = memcacheStats; this.out = out; } public void run() { out.println(memcacheStats.getStats()); } } /** * The client code to interface with all memcached servers. */ private static class StatsClient { ArrayList<StatsConnection> connections; Map<String, Map<String, String>> stats; /** * Constructs the client for all given servers. * @param servers host:port pairs for the server * @throws IOException Error communicating with memcached */ public StatsClient(String[] servers) throws IOException { connections = new ArrayList<StatsConnection>(servers.length); stats = new LinkedHashMap<String, Map<String, String>>(servers.length); for (String server : servers) try { StatsConnection conn = new StatsConnection(server); connections.add(conn); stats.put(server, conn.result); } catch (IOException e) { logger.log(Level.SEVERE, "Cannot connect to " + server + ".", e); } if (connections.size() == 0) throw new IOException("No host available."); } /** * Constructs the client for the given servers on the local system. * @param ports The list of ports * @throws IOException Error communicating to memcached */ public StatsClient(ArrayList<Integer> ports) throws IOException { connections = new ArrayList<StatsConnection>(ports.size()); stats = new LinkedHashMap<String, Map<String, String>>(ports.size()); for (int port : ports) try { StatsConnection conn = new StatsConnection("localhost", port); connections.add(conn); stats.put("localhost:" + port, conn.result); } catch (IOException e) { logger.log(Level.SEVERE, "Cannot connect to " + port + ".", e); } if (connections.size() == 0) throw new IOException("No host available."); } /** * Obtains the stats from all servers. * @return the stats from all servers. */ public Map<String, Map<String, String>> stats() { for (StatsConnection connection : connections) { try { connection.stats(); } catch (IOException e) { logger.log(Level.SEVERE, "Error obtaining stats from " + connection.server + ".", e); } } return stats; } } private static class StatsConnection { String server; Socket socket; BufferedReader reader; OutputStream out; HashMap<String, String> result = new HashMap<String, String>(); static final byte[] cmd = "stats\r\n".getBytes(); StatsConnection(String server) throws IOException { this.server = server; int colIdx = server.indexOf(':'); String host = server.substring(0, colIdx); int port = Integer.parseInt(server.substring(colIdx + 1)); init(host, port); } StatsConnection(String host, int port) throws IOException { server = host + ':' + port; init(host, port); } private void init(String host, int port) throws IOException { socket = new Socket(host, port); out = socket.getOutputStream(); reader = new BufferedReader(new InputStreamReader( socket.getInputStream())); } Map<String, String> stats() throws IOException { out.write(cmd); for (;;) { String line = reader.readLine(); if ("END".equals(line)) break; StringTokenizer t = new StringTokenizer(line); String statStr = t.nextToken(); if (!"STAT".equals(statStr)) throw new IOException("Expecting STAT, got " + statStr); String key = t.nextToken(); String value = t.nextToken(); result.put(key, value); } return result; } } }