/*
Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved.
Contact:
SYSTAP, LLC DBA Blazegraph
2501 Calvert ST NW #106
Washington, DC 20008
licenses@blazegraph.com
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
/*
* Created on Mar 13, 2008
*/
package com.bigdata.counters;
import java.io.IOException;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryUsage;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
import org.apache.log4j.Logger;
import org.apache.system.SystemUtil;
import com.bigdata.Banner;
import com.bigdata.BigdataStatics;
import com.bigdata.counters.httpd.CounterSetHTTPD;
import com.bigdata.counters.linux.StatisticsCollectorForLinux;
import com.bigdata.counters.osx.StatisticsCollectorForOSX;
import com.bigdata.counters.win.StatisticsCollectorForWindows;
import com.bigdata.io.DirectBufferPool;
import com.bigdata.util.Bytes;
import com.bigdata.util.config.NicUtil;
import com.bigdata.util.httpd.AbstractHTTPD;
import com.bigdata.util.httpd.Config;
/**
* Base class for collecting data on a host. The data are described by a
* hierarchical collection of {@link ICounterSet}s and {@link ICounter}s. A
* {@link IRequiredHostCounters minimum set of counters} is defined which SHOULD
* be available for decision-making. Implementations are free to report any
* additional data which they can make available. Reporting is assumed to be
* periodic, e.g., every 60 seconds or so. The purpose of these data is to
* support decision-making concerning the over- and under-utilization of hosts
* in support of load balancing of services deployed over those hosts.
* <p>
* An effort has been made to align the core set of counters for both Windows
* and Un*x platforms so as to support the declared counters on all platforms.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
*/
abstract public class AbstractStatisticsCollector implements IStatisticsCollector {
protected static final String ps = ICounterSet.pathSeparator;
final protected static Logger log = Logger
.getLogger(AbstractStatisticsCollector.class);
/** {@link InetAddress#getCanonicalHostName()} for this host. */
static final public String fullyQualifiedHostName;
/** The path prefix under which all counters for this host are found. */
static final public String hostPathPrefix;
static {
fullyQualifiedHostName = Banner.getFullyqualifiedhostname();
hostPathPrefix = ICounterSet.pathSeparator + fullyQualifiedHostName
+ ICounterSet.pathSeparator;
if (log.isInfoEnabled()) {
// log.info("hostname : " + hostname);
log.info("FQDN : " + fullyQualifiedHostName);
log.info("hostPrefix: " + hostPathPrefix);
}
}
/** Reporting interval in seconds. */
final protected int interval;
/** The name of this process. */
private final String processName;
/**
* The interval in seconds at which the counter values are read from the
* host platform.
*/
@Override
public int getInterval() {
return interval;
}
/**
* The name of the process (or more typically its service {@link UUID})
* whose per-process performance counters are to be collected.
*/
public String getProcessName() {
return processName;
}
protected AbstractStatisticsCollector(final int interval,
final String processName) {
if (interval <= 0)
throw new IllegalArgumentException();
if(processName == null)
throw new IllegalArgumentException();
if(log.isInfoEnabled()) log.info("interval=" + interval);
this.interval = interval;
this.processName = processName;
}
// /**
// * Return the load average for the last minute if available and -1
// * otherwise.
// * <p>
// * Note: The load average is available on 1.6+ JVMs.
// *
// * @see OperatingSystemMXBean
// */
// public double getSystemLoadAverage()
// {
//
//// double version = Double.parseDouble(System.getProperty("java.vm.version"));
//// if(version>=1.6) {
//
// double loadAverage = -1;
//
// final OperatingSystemMXBean mbean = ManagementFactory
// .getOperatingSystemMXBean();
//
// /*
// * Use reflection since method is only available as of 1.6
// */
// Method method;
// try {
// method = mbean.getClass().getMethod("getSystemLoadAverage",
// new Class[] {});
// loadAverage = (Double) method.invoke(mbean, new Object[] {});
// } catch (SecurityException e) {
// log.warn(e.getMessage(), e);
// } catch (NoSuchMethodException e) {
// // Note: method is only defined since 1.6
// log.warn(e.getMessage(), e);
// } catch (IllegalAccessException e) {
// log.warn(e.getMessage(), e);
// } catch (InvocationTargetException e) {
// log.warn(e.getMessage(), e);
// }
//
// return loadAverage;
//
// }
/**
* {@link CounterSet} hierarchy.
*/
private CounterSet countersRoot;
/**
* Return the counter hierarchy. The returned hierarchy only includes those
* counters whose values are available from the JVM. This collection is
* normally augmented with platform specific performance counters collected
* using an {@link AbstractProcessCollector}.
* <p>
* Note: Subclasses MUST extend this method to initialize their own
* counters.
*
* TODO Why does this use the older <code>synchronized</code> pattern with a
* shared {@link #countersRoot} object rather than returning a new object
* per request? Check assumptions in the scale-out and local journal code
* bases for this.
*/
@Override
synchronized
public CounterSet getCounters() {
if (countersRoot == null) {
// final CounterSet
countersRoot = new CounterSet();
// os.arch
countersRoot.addCounter(hostPathPrefix
+ IHostCounters.Info_Architecture,
new OneShotInstrument<String>(System.getProperty("os.arch")));
// os.name
countersRoot.addCounter(hostPathPrefix
+ IHostCounters.Info_OperatingSystemName,
new OneShotInstrument<String>(System.getProperty("os.name")));
// os.version
countersRoot.addCounter(hostPathPrefix
+ IHostCounters.Info_OperatingSystemVersion,
new OneShotInstrument<String>(System.getProperty("os.version")));
// #of processors.
countersRoot.addCounter(hostPathPrefix
+ IHostCounters.Info_NumProcessors,
new OneShotInstrument<Integer>(SystemUtil.numProcessors()));
// processor info
countersRoot.addCounter(hostPathPrefix
+ IHostCounters.Info_ProcessorInfo,
new OneShotInstrument<String>(SystemUtil.cpuInfo()));
}
return countersRoot;
}
/**
* Adds the Info and Memory counter sets under the <i>serviceRoot</i>.
*
* @param serviceRoot
* The {@link CounterSet} corresponding to the service (or
* client).
* @param serviceName
* The name of the service.
* @param serviceIface
* The class or interface that best represents the service or
* client.
* @param properties
* The properties used to configure that service or client.
*/
static public void addBasicServiceOrClientCounters(
final CounterSet serviceRoot, final String serviceName,
final Class serviceIface, final Properties properties) {
// Service info.
{
final CounterSet serviceInfoSet = serviceRoot.makePath("Info");
serviceInfoSet.addCounter("Service Type",
new OneShotInstrument<String>(serviceIface.getName()));
serviceInfoSet.addCounter("Service Name",
new OneShotInstrument<String>(serviceName));
AbstractStatisticsCollector.addServiceProperties(serviceInfoSet,
properties);
}
serviceRoot.attach(getMemoryCounterSet());
}
/**
* Return the {@link IProcessCounters#Memory memory counter set}. This
* should be attached to the service root.
*/
static public CounterSet getMemoryCounterSet() {
final CounterSet serviceRoot = new CounterSet();
// Service per-process memory data
{
serviceRoot.addCounter(
IProcessCounters.Memory_runtimeMaxMemory,
new OneShotInstrument<Long>(Runtime.getRuntime().maxMemory()));
serviceRoot.addCounter(IProcessCounters.Memory_runtimeFreeMemory,
new Instrument<Long>() {
@Override
public void sample() {
setValue(Runtime.getRuntime().freeMemory());
}
});
serviceRoot.addCounter(IProcessCounters.Memory_runtimeTotalMemory,
new Instrument<Long>() {
@Override
public void sample() {
setValue(Runtime.getRuntime().totalMemory());
}
});
// add counters for garbage collection.
AbstractStatisticsCollector
.addGarbageCollectorMXBeanCounters(serviceRoot
.makePath(ICounterHierarchy.Memory_GarbageCollectors));
// add counters for memory pools.
AbstractStatisticsCollector
.addMemoryPoolMXBeanCounters(serviceRoot
.makePath(ICounterHierarchy.Memory_Memory_Pools));
/*
* Add counters reporting on the various DirectBufferPools.
*/
serviceRoot.makePath(
IProcessCounters.Memory + ICounterSet.pathSeparator
+ "DirectBufferPool").attach(
DirectBufferPool.getCounters());
// @see BLZG-1501 (remove LRUNexus)
// if (LRUNexus.INSTANCE != null) {
//
// /*
// * Add counters reporting on the global LRU and the per-store
// * caches.
// */
//
// serviceRoot.makePath(
// IProcessCounters.Memory + ICounterSet.pathSeparator
// + "LRUNexus").attach(
// LRUNexus.INSTANCE.getCounterSet());
//
// }
}
return serviceRoot;
}
/**
* Lists out all of the properties and then report each property using a
* {@link OneShotInstrument}.
*
* @param serviceInfoSet
* The {@link ICounterHierarchy#Info} {@link CounterSet} for the
* service.
* @param properties
* The properties to be reported out.
*/
static public void addServiceProperties(final CounterSet serviceInfoSet,
final Properties properties) {
final CounterSet ptmp = serviceInfoSet.makePath("Properties");
final Enumeration<?> e = properties.propertyNames();
while (e.hasMoreElements()) {
final String name;
final String value;
try {
name = (String) e.nextElement();
value = (String) properties.getProperty(name);
} catch (ClassCastException ex) {
log.warn(ex.getMessage());
continue;
}
if (value == null)
continue;
ptmp.addCounter(name, new OneShotInstrument<String>(value));
}
}
/**
* Adds/updates counters relating to JVM Garbage Collection. These counters
* should be located within a per-service path.
*
* @param counterSet
* The counters set that is the direct parent.
*/
static public void addGarbageCollectorMXBeanCounters(
final CounterSet counterSet) {
final String name_pools = "Memory Pool Names";
final String name_count = "Collection Count";
final String name_time = "Cumulative Collection Time";
synchronized (counterSet) {
final List<GarbageCollectorMXBean> list = ManagementFactory
.getGarbageCollectorMXBeans();
for (final GarbageCollectorMXBean bean : list) {
final String name = bean.getName();
// counter set for this GC bean (may be pre-existing).
final CounterSet tmp = counterSet.makePath(name);
synchronized (tmp) {
// memory pool names.
{
if (tmp.getChild(name_pools) == null) {
tmp.addCounter(name_pools,
new Instrument<String>() {
@Override
protected void sample() {
setValue(Arrays.toString(bean
.getMemoryPoolNames()));
}
});
}
}
// collection count.
{
if (tmp.getChild(name_count) == null) {
tmp.addCounter(name_count, new Instrument<Long>() {
@Override
protected void sample() {
setValue(bean.getCollectionCount());
}
});
}
}
// collection time.
{
if (tmp.getChild(name_time) == null) {
tmp.addCounter(name_time, new Instrument<Long>() {
@Override
protected void sample() {
setValue(bean.getCollectionTime());
}
});
}
}
}
}
}
}
/**
* Adds/updates counters relating to JVM Memory Pools. These counters
* should be located within a per-service path.
*
* @param counterSet
* The counters set that is the direct parent.
*/
static public void addMemoryPoolMXBeanCounters(
final CounterSet counterSet) {
final String name_pool = "Memory Pool";
final String name_max = "Maximum Usage";
final String name_used = "Current Usage";
synchronized (counterSet) {
final List<MemoryPoolMXBean> list = ManagementFactory
.getMemoryPoolMXBeans();
for (final MemoryPoolMXBean bean : list) {
final String name = bean.getName();
// counter set for this GC bean (may be pre-existing).
final CounterSet tmp = counterSet.makePath(name);
synchronized (tmp) {
// memory pool names.
{
if (tmp.getChild(name_pool) == null) {
tmp.addCounter(name_pool,
new Instrument<String>() {
@Override
protected void sample() {
setValue(bean.getName());
}
});
}
}
// usage (max).
{
if (tmp.getChild(name_max) == null) {
tmp.addCounter(name_max, new Instrument<Long>() {
@Override
protected void sample() {
final MemoryUsage u = bean.getUsage();
setValue(u.getMax());
}
});
}
}
// usage (current)
{
if (tmp.getChild(name_used) == null) {
tmp.addCounter(name_used, new Instrument<Long>() {
@Override
protected void sample() {
final MemoryUsage u = bean.getUsage();
setValue(u.getUsed());
}
});
}
}
}
}
}
}
/**
* Start collecting host performance data -- must be extended by the
* concrete subclass.
*/
@Override
public void start() {
if (log.isInfoEnabled())
log.info("Starting collection.");
installShutdownHook();
}
/**
* Stop collecting host performance data -- must be extended by the concrete
* subclass.
*/
@Override
public void stop() {
if (log.isInfoEnabled())
log.info("Stopping collection.");
}
/**
* Installs a {@link Runtime#addShutdownHook(Thread)} that executes
* {@link #stop()}.
* <p>
* Note: The runtime shutdown hook appears to be a robust way to handle ^C
* by providing a clean service termination. However, under eclipse (at
* least when running under Windows) you may find that the shutdown hook
* does not run when you terminate a Java application and that typedef
* process build up in the Task Manager as a result. This should not be the
* case during normal deployment.
*/
protected void installShutdownHook() {
final Thread t = new Thread() {
@Override
public void run() {
AbstractStatisticsCollector.this.stop();
}
};
t.setDaemon(true);
Runtime.getRuntime().addShutdownHook(t);
}
/**
* Options for {@link AbstractStatisticsCollector}
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
*/
public interface Options {
/**
* The interval in seconds at which the performance counters of the host
* platform will be sampled (default 60).
*/
public String PERFORMANCE_COUNTERS_SAMPLE_INTERVAL = AbstractStatisticsCollector.class
.getPackage().getName()
+ ".interval";
public String DEFAULT_PERFORMANCE_COUNTERS_SAMPLE_INTERVAL = "60";
/**
* The name of the process whose per-process performance counters are to
* be collected (required, no default). This causes the per-process
* counters to be reported using the path:
*
* <strong>/<i>fullyQualifiedHostname</i>/<i>processName</i>/...</strong>
* <p>
* Note: Services are generally associated with a {@link UUID} and that
* {@link UUID} is generally used as the service name. A single host may
* run many different services and will report the counters for each
* service using the path formed as described above.
*/
public String PROCESS_NAME = AbstractStatisticsCollector.class
.getPackage().getName()
+ ".processName";
}
/**
* Create an instance appropriate for the operating system on which the JVM
* is running.
*
* @param properties
* See {@link Options}
*
* @throws UnsupportedOperationException
* If there is no implementation available on the operating
* system on which you are running.
*
* @see Options
*/
public static AbstractStatisticsCollector newInstance(
final Properties properties) {
final int interval = Integer.parseInt(properties.getProperty(
Options.PERFORMANCE_COUNTERS_SAMPLE_INTERVAL,
Options.DEFAULT_PERFORMANCE_COUNTERS_SAMPLE_INTERVAL));
if (interval <= 0)
throw new IllegalArgumentException();
final String processName = properties.getProperty(Options.PROCESS_NAME);
if (processName == null)
throw new IllegalArgumentException(
"Required option not specified: " + Options.PROCESS_NAME);
// final String osname = System.getProperty("os.name").toLowerCase();
//
// if(osname.equalsIgnoreCase("linux")) {
if(SystemUtil.isLinux()) {
return new StatisticsCollectorForLinux(interval, processName);
// } else if(osname.contains("windows")) {
} else if(SystemUtil.isWindows()) {
return new StatisticsCollectorForWindows(interval, processName);
// } else if(osname.contains("os x")) {
} else if(SystemUtil.isOSX()) {
return new StatisticsCollectorForOSX(interval, processName);
} else {
throw new UnsupportedOperationException(
"No implementation available on "
+ System.getProperty("os.getname"));
}
}
/**
* Utility runs the {@link AbstractStatisticsCollector} appropriate for your
* operating system. Before performance counter collection starts the static
* counters will be written on stdout. The appropriate process(es) are then
* started to collect the dynamic performance counters. Collection will
* occur every {@link Options#PERFORMANCE_COUNTERS_SAMPLE_INTERVAL} seconds.
* The program will make 10 collections by default and will write the
* updated counters on stdout every
* {@link Options#PERFORMANCE_COUNTERS_SAMPLE_INTERVAL} seconds.
* <p>
* Parameters also may be specified using <code>-D</code>. See
* {@link Options}.
*
* @param args <code>[<i>interval</i> [<i>count</i>]]</code>
* <p>
* <i>interval</i> is the collection interval in seconds and
* defaults to
* {@link Options#DEFAULT_PERFORMANCE_COUNTERS_SAMPLE_INTERVAL}.
* <p>
* <i>count</i> is the #of collections to be made and defaults
* to <code>10</code>. Specify zero (0) to run until halted.
*
* @throws InterruptedException
* @throws RuntimeException
* if the arguments are not valid.
* @throws UnsupportedOperationException
* if no implementation is available for your operating system.
*/
public static void main(final String[] args) throws InterruptedException {
Banner.banner();
final int DEFAULT_COUNT = 10;
final int nargs = args.length;
final int interval;
final int count;
if (nargs == 0) {
interval = Integer.parseInt(Options.DEFAULT_PERFORMANCE_COUNTERS_SAMPLE_INTERVAL);
count = DEFAULT_COUNT;
} else if (nargs == 1) {
interval = Integer.parseInt(args[0]);
count = DEFAULT_COUNT;
} else if (nargs == 2) {
interval = Integer.parseInt(args[0]);
count = Integer.parseInt(args[1]);
} else {
throw new RuntimeException("usage: [interval [count]]");
}
if (interval <= 0)
throw new RuntimeException("interval must be positive");
if (count < 0)
throw new RuntimeException("count must be non-negative");
final Properties properties = new Properties(System.getProperties());
if (nargs != 0) {
// Override the interval property from the command line.
properties.setProperty(Options.PERFORMANCE_COUNTERS_SAMPLE_INTERVAL,""+interval);
}
if(properties.getProperty(Options.PROCESS_NAME)==null) {
/*
* Set a default process name if none was specified in the
* environment.
*
* Note: Normally the process name is specified explicitly by the
* service which instantiates the performance counter collection for
* that process. We specify a default here since main() is used for
* testing purposes only.
*/
properties.setProperty(Options.PROCESS_NAME,"testService");
}
final AbstractStatisticsCollector client = AbstractStatisticsCollector
.newInstance( properties );
final CounterSet counterSet = client.getCounters();
counterSet.attach(getMemoryCounterSet());
// write counters before we start the client
System.out.println(counterSet.toString());
System.err.println("Starting performance counter collection: interval="
+ client.interval + ", count=" + count);
client.start();
/*
* HTTPD service reporting out statistics.
*/
AbstractHTTPD httpd = null;
{
final int port = Config.BLAZEGRAPH_HTTP_PORT;
if (port != 0) {
try {
httpd = new CounterSetHTTPD(port, client);
} catch (IOException e) {
log.warn("Could not start httpd: port=" + port+" : "+e);
}
}
}
int n = 0;
final long begin = System.currentTimeMillis();
// Note: runs until killed when count==0.
while (count == 0 || n < count) {
Thread.sleep(client.interval * 1000/*ms*/);
final long elapsed = System.currentTimeMillis() - begin;
System.err.println("Report #"+n+" after "+(elapsed/1000)+" seconds ");
System.out.println(client.getCounters().toString());
n++;
}
System.err.println("Stopping performance counter collection");
client.stop();
if (httpd != null)
httpd.shutdown();
System.err.println("Done");
}
/**
* Converts KB to bytes.
*
* @param kb
* The #of kilobytes.
*
* @return The #of bytes.
*/
static public Double kb2b(final String kb) {
final double d = Double.parseDouble(kb);
final double x = d * Bytes.kilobyte32;
return x;
}
}