/**
* Helios, OpenSource Monitoring
* Brought to you by the Helios Development Group
*
* Copyright 2007, Helios Development Group and individual contributors
* as indicated by the @author tags. See the copyright.txt file in the
* distribution for a full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*
*/
package org.helios.apmrouter.monitor.script;
import org.helios.apmrouter.jmx.ConfigurationHelper;
import org.helios.apmrouter.monitor.AbstractMonitor;
import org.helios.apmrouter.nativex.APMSigar;
import org.helios.apmrouter.trace.ITracer;
import org.helios.apmrouter.trace.TracerFactory;
import org.helios.apmrouter.util.URLHelper;
import javax.script.*;
import java.io.File;
import java.io.FilenameFilter;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* <p>Title: ScriptMonitor</p>
* <p>Description: A monitor implementation that loads scripts from a sysprop specified URL, loads scripts from that URL
* and executes those scripts on an interval. There ae currently only 3 supported URL patterns:<ol>
* <li>HTTP URLs are supported provided they point to one and only one JS resource</li>
* <li>File based URLs that point to one JS file</li>
* <li>File based URLs that point to a directory which will be scanned for JS files.</li>
* </ol></p>
* <p>Company: Helios Development Group LLC</p>
* @author Whitehead (nwhitehead AT heliosdev DOT org)
* <p><code>org.helios.apmrouter.monitor.script.ScriptMonitor</code></p>
* FIXME: Only supports JS for now.
* FIXME: Need to periodically scan for updates
*/
public class ScriptMonitor extends AbstractMonitor {
/** The script engine manager that creates the monitoring script engines */
protected final ScriptEngineManager scriptEngineManager;
/** A map of compiled scripts keyed by the script URL resource */
protected static final Map<String, ScriptContainer> compiledScripts = new ConcurrentHashMap<String, ScriptContainer>();
/** The bindings that are passed to each script */
protected final Bindings scriptBindings = new SimpleBindings();
/** The configured maximum number of times a script can fail before it is ignored */
protected int maxErrors = DEFAULT_SCRIPT_ERRCNT;
/** The tracer to bind */
protected final ITracer tracer;
/** The JMXHelper to bind. JMXHelper is all static but JS will not accept straight classes */
protected final JMXScriptHelper jmxHelper = new JMXScriptHelper();;
/** The system propety that specifies the maximum number of times a script can fail before it is ignored */
public static final String SCRIPT_ERRCNT_PROP = "org.helios.agent.monitor.script.maxerr";
/** The default maximum number of times a script can fail before it is ignored */
public static final int DEFAULT_SCRIPT_ERRCNT = 3;
/** The system propety that specifies the URL of the script location */
public static final String SCRIPT_URL_PROP = "org.helios.agent.monitor.script.url";
/** The default URL of the script location */
public static final URL DEFAULT_SCRIPT_URL = URLHelper.toURL(new File(System.getProperty("user.home") + File.separator + ".apmrouter" + File.separator + "mscripts"));
/** The binding name of the tracer */
public static final String TRACER_BINDING_KEY = "tracer";
/** The binding name of the bindings for transfering state*/
public static final String BINDINGS_BINDING_KEY = "state";
/** The binding name of the JMXHelper */
public static final String JMXHELPER_BINDING_KEY = "jmx";
/** The binding name for the stdout stream */
public static final String STD_OUT = "pout";
/** The binding name for the stderr stream */
public static final String STD_ERR = "perr";
/** The binding name for the {@link APMSigar} instance */
public static final String APM_SIGAR = "sigar";
/** Supporting scripts */
public static final String[] supportScripts = {
"function isNumber(v) {try { var i = parseInt(v); return !isNaN(i); } catch (e) { return false; }}"
};
/*
* set inited
* state-control (state.get().get('inited') + ":" + state.get().get('lastelapsed'))
*/
/**
* Creates a new ScriptMonitor
*/
public ScriptMonitor() {
this(null);
}
/**
* Creates a new ScriptMonitor
* @param scriptLibs An optional array of URLs for additional scripting libraries
*/
public ScriptMonitor(URL...scriptLibs) {
if(scriptLibs!=null && scriptLibs.length>0) {
URLClassLoader ucl = new URLClassLoader(scriptLibs, getClass().getClassLoader());
scriptEngineManager = new ScriptEngineManager(ucl);
} else {
scriptEngineManager = new ScriptEngineManager();
}
maxErrors = ConfigurationHelper.getIntSystemThenEnvProperty(SCRIPT_ERRCNT_PROP, DEFAULT_SCRIPT_ERRCNT);
tracer = TracerFactory.getTracer();
scriptBindings.put(TRACER_BINDING_KEY, tracer);
//scriptBindings.put(BINDINGS_BINDING_KEY, scriptBindings);
scriptBindings.put(JMXHELPER_BINDING_KEY, jmxHelper);
scriptBindings.put(STD_OUT, System.out);
scriptBindings.put(STD_ERR, System.err);
scriptBindings.put(APM_SIGAR, APMSigar.getInstance());
//scriptLoad();
scheduleNewScriptCheck();
}
/**
* Looks up and returns the named compiled script
* @param name The name of the script
* @return the named script or null if one was not found
*/
public static ScriptContainer getScript(String name) {
if(name==null || name.trim().isEmpty()) return null;
return compiledScripts.get(name.trim().toLowerCase());
}
/**
* {@inheritDoc}
* @see org.helios.apmrouter.monitor.AbstractMonitor#doCollect(long)
*/
@Override
protected void doCollect(long collectionSweep) {
for(Iterator<ScriptContainer> iter = compiledScripts.values().iterator(); iter.hasNext();) {
ScriptContainer sc = iter.next();
try {
if(sc.getCustomFrequency()==-1L) {
sc.invoke();
}
} catch (UnavailableMBeanServerException uex) {
/* No Op */
} catch (Throwable e) {
long err = sc.getConsecutiveErrors();
if(err>=maxErrors) {
sc.setDisabled(true);
System.err.println("Monitor Script [" + sc.scriptUrl + "] has failed [" + err + "] consecutive times. It is being ignored until modified. Last error follows:");
e.printStackTrace(System.err);
}
}
}
}
/**
* Searches for loadable scripts
*/
protected void scriptLoad() {
URL scriptURL = null;
String url = ConfigurationHelper.getSystemThenEnvProperty(SCRIPT_URL_PROP, null);
Set<URL> deployed = new HashSet<URL>();
if(url==null) {
File scriptDir = new File(DEFAULT_SCRIPT_URL.getFile());
if(scriptDir.exists() && scriptDir.isDirectory()) {
scriptURL = DEFAULT_SCRIPT_URL;
}
}
try {
if(scriptURL==null) {
if(url==null) return;
scriptURL = URLHelper.toURL(url);
}
if(scriptURL.getProtocol().equals("file")) {
Collections.addAll(deployed, processFileScripts(scriptURL));
} else if(scriptURL.getProtocol().equals("http")) {
Collections.addAll(deployed, processHttpScript(scriptURL));
}
} catch (Exception e) {
System.err.println("Failed to resolve monitor scripts from [" + url + "]");
}
if(!deployed.isEmpty()) {
StringBuilder b = new StringBuilder("\n\tDeployed Monitoring Scripts:\n\t============================");
for(URL u: deployed) {
b.append("\n\t\t").append(u);
}
b.append("\n");
System.out.println(b);
}
}
/**
* Schedules a task to check for new script files
*/
protected void scheduleNewScriptCheck() {
scheduler.scheduleWithFixedDelay(new Runnable(){
public void run() {
scriptLoad();
}
}, 15000, 5000, TimeUnit.MILLISECONDS);
}
/**
* Attempts to load a monitor script from the passed URL
* @param scriptURL the monitor script URL
* @throws ScriptException rethrows any exception thrown from the container
* @return The URL of the deployed script or null if one was not deployed
*/
private URL processHttpScript(URL scriptURL) throws ScriptException {
if(compiledScripts.containsKey(ScriptContainer.urlToName(scriptURL))) return null;
ScriptEngine se = scriptEngineManager.getEngineByExtension(URLHelper.getExtension(scriptURL, "").toLowerCase());
if(se==null) throw new RuntimeException("No script engine found for [" + scriptURL + "]", new Throwable());
ScriptContainer sc = new ScriptContainer(se, scriptBindings, scriptURL);
if(sc.getCustomFrequency()>0) scheduleCustomFrequencyScript(sc, -1L);
compiledScripts.put(sc.getName(), sc);
return scriptURL;
}
/** An empty URL array */
protected static final URL[] EMPTY_URL_ARR = new URL[0];
/**
* Attempts to load file based monitor scripts from the passed URL.
* If the URL resolves to a directory, that directory will be scanned for JS monitor scripts.
* Otherwise, if the extension of the file is [case insensitive] JS, that single file will be loaded.
* @param scriptURL The file based script URL
* @throws ScriptException rethrows any exception thrown from the container
* @return The URLs of the deployed scripts
*/
private URL[] processFileScripts(URL scriptURL) throws ScriptException {
if(compiledScripts.containsKey(ScriptContainer.urlToName(scriptURL))) return EMPTY_URL_ARR;
String fileName = scriptURL.getFile();
File file = new File(fileName);
if(!file.exists()) return EMPTY_URL_ARR;
if(file.isFile()) {
ScriptEngine se = scriptEngineManager.getEngineByExtension(URLHelper.getFileExtension(file, "").toLowerCase());
if(se!=null) {
ScriptContainer sc = new ScriptContainer(se, scriptBindings, scriptURL);
if(sc.getCustomFrequency()>0) scheduleCustomFrequencyScript(sc, -1L);
compiledScripts.put(sc.getName(), sc);
return new URL[]{scriptURL};
}
return EMPTY_URL_ARR;
} else if(file.isDirectory()) {
Set<URL> deployed = new HashSet<URL>();
for(File scriptFile: file.listFiles(new FilenameFilter(){
@Override
public boolean accept(File dir, String name) {
return scriptEngineManager.getEngineByExtension(URLHelper.getFileExtension(name, "").toLowerCase())!=null;
}})) {
try {
URL fileURL = scriptFile.toURI().toURL();
if(compiledScripts.containsKey(ScriptContainer.urlToName(fileURL))) continue;
ScriptEngine se = scriptEngineManager.getEngineByExtension(URLHelper.getFileExtension(scriptFile, "").toLowerCase());
ScriptContainer sc = new ScriptContainer(se, scriptBindings, fileURL);
if(sc.getCustomFrequency()>0) scheduleCustomFrequencyScript(sc, -1L);
compiledScripts.put(sc.getName(), sc);
deployed.add(fileURL);
} catch (Exception e) {
crap(e);
}
}
return deployed.toArray(new URL[deployed.size()]);
}
return EMPTY_URL_ARR;
}
/**
* Schedules the passed ScriptContainer for recurring invocations at custom intervals
* @param sc The script container to schedule invocations for
* @param frequency The delay until the next execution. If zero or less, ignored.
*/
protected void scheduleCustomFrequencyScript(final ScriptContainer sc, long frequency) {
scheduler.schedule(new Runnable(){
public void run() {
long nextFrequency = sc.getCustomFrequency();
try {
sc.invoke();
} catch (UnavailableMBeanServerException umx) {
/* No Op */
} catch (Exception ex) {
nextFrequency = nextFrequency * 5;
long err = sc.getConsecutiveErrors();
if(err>=maxErrors) {
sc.setDisabled(true);
System.err.println("Monitor Script [" + sc.scriptUrl + "] has failed [" + err + "] consecutive times. It is being ignored until modified. Last error follows:");
ex.printStackTrace(System.err);
}
}
scheduleCustomFrequencyScript(sc, nextFrequency);
}
}, frequency<0 ? sc.getCustomFrequency() : frequency, TimeUnit.MILLISECONDS);
}
/**
* Take a crap with this exception
* @param e The exception to take a crap with
*/
protected void crap(Exception e) {
System.err.println("Script load took a crap. CrapTrace follows:");
e.printStackTrace(System.err);
}
}