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