/*******************************************************************************
* This file is part of OpenNMS(R).
*
* Copyright (C) 2006-2011 The OpenNMS Group, Inc.
* OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
* OpenNMS(R) 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, either version 3 of the License,
* or (at your option) any later version.
*
* OpenNMS(R) 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 OpenNMS(R). If not, see:
* http://www.gnu.org/licenses/
*
* For more information contact:
* OpenNMS(R) Licensing <license@opennms.org>
* http://www.opennms.org/
* http://www.opennms.com/
*******************************************************************************/
package org.opennms.netmgt.rrd.rrdtool;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import org.opennms.core.utils.StringUtils;
import org.opennms.core.utils.ThreadCategory;
import org.opennms.netmgt.rrd.RrdDataSource;
import org.opennms.netmgt.rrd.RrdException;
import org.opennms.netmgt.rrd.RrdGraphDetails;
import org.opennms.netmgt.rrd.RrdStrategy;
import org.opennms.netmgt.rrd.RrdUtils;
import org.springframework.util.FileCopyUtils;
/**
* Provides an rrdtool based implementation of RrdStrategy. It uses the existing
* JNI based single-threaded interface to write the rrdtool compatibile RRD
* files.
*
* The JNI interface takes command-like arguments and doesn't provide open files
* so the the Objects used to represent open files are really partial command
* strings
*
* See the individual methods for more details
*
* @author ranger
* @version $Id: $
*/
public class JniRrdStrategy implements RrdStrategy<String,StringBuffer> {
private final static String IGNORABLE_LIBART_WARNING_STRING = "*** attempt to put segment in horiz list twice";
private final static String IGNORABLE_LIBART_WARNING_REGEX = "\\*\\*\\* attempt to put segment in horiz list twice\r?\n?";
private Properties m_configurationProperties;
/**
* <p>getConfigurationProperties</p>
*
* @return a {@link java.util.Properties} object.
*/
public Properties getConfigurationProperties() {
return m_configurationProperties;
}
/** {@inheritDoc} */
public void setConfigurationProperties(Properties configurationParameters) {
this.m_configurationProperties = configurationParameters;
}
/**
* The 'closes' the rrd file. This is where the actual work of writing the
* RRD files takes place. The passed in rrd is actually an rrd command
* string containing updates. This method executes this command.
*
* @param rrd a {@link java.lang.StringBuffer} object.
* @throws java.lang.Exception if any.
*/
public void closeFile(StringBuffer rrd) throws Exception {
String command = rrd.toString();
String[] results = Interface.launch(command);
if (results[0] != null) {
throw new Exception(results[0]);
}
}
/** {@inheritDoc} */
public String createDefinition(String creator, String directory, String rrdName, int step, List<RrdDataSource> dataSources, List<String> rraList) throws Exception {
File f = new File(directory);
f.mkdirs();
String fileName = directory + File.separator + rrdName + RrdUtils.getExtension();
if (new File(fileName).exists()) {
return null;
}
StringBuffer createCmd = new StringBuffer("create");
createCmd.append(' ' + fileName);
createCmd.append(" --start=" + (System.currentTimeMillis() / 1000L - 10L));
createCmd.append(" --step=" + step);
for (RrdDataSource dataSource : dataSources) {
createCmd.append(" DS:");
createCmd.append(dataSource.getName()).append(':');
createCmd.append(dataSource.getType()).append(":");
createCmd.append(dataSource.getHeartBeat()).append(':');
createCmd.append(dataSource.getMin()).append(':');
createCmd.append(dataSource.getMax());
}
for (String rra : rraList) {
createCmd.append(' ');
createCmd.append(rra);
}
return createCmd.toString();
}
/**
* Creates a the rrd file from the rrdDefinition. Since this definition is
* really just the create command string it just executes it.
*
* @param rrdDef a {@link java.lang.String} object.
* @throws java.lang.Exception if any.
*/
public void createFile(String rrdDef) throws Exception {
if (rrdDef == null) return;
log().debug("Executing: rrdtool "+rrdDef.toString());
Interface.launch(rrdDef);
}
/**
* {@inheritDoc}
*
* The 'opens' the given rrd file. In actuality since the JNI interface does
* not provide files that may be open, this constructs the beginning portion
* of the rrd command to update the file.
*/
public StringBuffer openFile(String fileName) throws Exception {
return new StringBuffer("update " + fileName);
}
/**
* {@inheritDoc}
*
* This 'updates' the given rrd file by providing data. Since the JNI
* interface does not provide files that can be open, this just appends the
* data to the command string constructed so far. The data is not
* immediately written to the file since this would eliminate the
* possibility of getting performance benefit by doing more than one write
* per open. The updates are all performed at once in the closeFile method.
*/
public void updateFile(StringBuffer rrd, String owner, String data) throws Exception {
rrd.append(' ');
rrd.append(data);
}
/**
* Initialized the JNI Interface
*
* @throws java.lang.Exception if any.
*/
public JniRrdStrategy() throws Exception {
Interface.init();
}
/**
* {@inheritDoc}
*
* Fetches the last value directly from the rrd file using the JNI
* Interface.
*/
public Double fetchLastValue(String rrdFile, String ds, int interval) throws NumberFormatException, RrdException {
return fetchLastValue(rrdFile, ds, "AVERAGE", interval);
}
/** {@inheritDoc} */
public Double fetchLastValue(String rrdFile, String ds, String consolidationFunction, int interval) {
/*
* Generate rrd_fetch() command through jrrd JNI interface in order to
* retrieve LAST pdp for the datasource stored in the specified RRD
* file.
*
* String array returned from launch() native method format:
* String[0] - If success is null, otherwise contains reason
* for failure
* String[1] - All data source names contained in the RRD (space
* delimited)
* String[2 ... n] - RRD fetch data in the following format:
* <timestamp> <value1> <value2> ... <valueX>
* X is the total number of data sources.
*
* NOTE: Specifying start time of 'now-<interval>' and end time of
* 'now-<interval>' where <interval> is the configured thresholding
* interval (and should be the same as the RRD step size) in order to
* guarantee that we don't get a 'NaN' value from the fetch command.
* This is necessary because the collection is being done by collectd at
* effectively random times and there is nothing keeping us in sync.
*
* interval argument is in milliseconds so must convert to seconds
*/
// TODO: Combine fetchLastValueInRange and fetchLastValue
String fetchCmd = "fetch " + rrdFile + " "+consolidationFunction+" -s now-" + interval / 1000 + " -e now-" + interval / 1000;
if (log().isDebugEnabled()) {
log().debug("fetch: Issuing RRD command: " + fetchCmd);
}
String[] fetchStrings = Interface.launch(fetchCmd);
// Sanity check the returned string array
if (fetchStrings == null) {
log().error("fetch: Unexpected error issuing RRD 'fetch' command, no error text available.");
return null;
}
// Check error string at index 0, will be null if 'fetch' was successful
if (fetchStrings[0] != null) {
log().error("fetch: RRD database 'fetch' failed, reason: " + fetchStrings[0]);
return null;
}
// Sanity check
if (fetchStrings[1] == null || fetchStrings[2] == null) {
log().error("fetch: RRD database 'fetch' failed, no data retrieved.");
return null;
}
// String at index 1 contains the RRDs datasource names
//
String[] dsNames = fetchStrings[1].split("\\s");
int dsIndex = 0;
for (int i = 0; i < dsNames.length; i++) {
if (dsNames[i].equals(ds)) dsIndex = i;
}
String dsName = dsNames[dsIndex].trim();
// String at index 2 contains fetched values for the current time
// Convert value string into a Double
//
String[] dsValues = fetchStrings[2].split("\\s");
Double dsValue = null;
if (dsValues[dsIndex].trim().equalsIgnoreCase("nan")) {
dsValue = new Double(Double.NaN);
} else {
try {
dsValue = new Double(dsValues[dsIndex].trim());
} catch (NumberFormatException nfe) {
log().warn("fetch: Unable to convert fetched value (" + dsValues[dsIndex].trim() + ") to Double for data source " + dsName);
throw nfe;
}
}
if (log().isDebugEnabled()) {
log().debug("fetch: fetch successful: " + dsName + "= " + dsValue);
}
return dsValue;
}
/** {@inheritDoc} */
public Double fetchLastValueInRange(String rrdFile, String ds, int interval, int range) throws NumberFormatException, RrdException {
// Generate rrd_fetch() command through jrrd JNI interface in order to
// retrieve
// LAST pdp for the datasource stored in the specified RRD file
//
// String array returned from launch() native method format:
// String[0] - If success is null, otherwise contains reason for failure
// String[1] - All data source names contained in the RRD (space
// delimited)
// String[2]...String[n] - RRD fetch data in the following format:
// <timestamp> <value1> <value2> ... <valueX> where X is
// the total number of data sources
//
// NOTE: Specifying start time of 'now-<interval>' and
// end time of 'now-<interval>' where <interval> is the
// configured thresholding interval (and should be the
// same as the RRD step size) in order to guarantee that
// we don't get a 'NaN' value from the fetch command. This
// is necessary because the collection is being done by collectd
// and there is nothing keeping us in sync.
//
// interval argument is in milliseconds so must convert to seconds
//
// TODO: Combine fetchLastValueInRange and fetchLastValue
long now = System.currentTimeMillis();
long latestUpdateTime = (now - (now % interval)) / 1000L;
long earliestUpdateTime = ((now - (now % interval)) - range) / 1000L;
if (log().isDebugEnabled()) {
log().debug("fetchInRange: fetching data from " + earliestUpdateTime + " to " + latestUpdateTime);
}
String fetchCmd = "fetch " + rrdFile + " AVERAGE -s " + earliestUpdateTime + " -e " + latestUpdateTime;
String[] fetchStrings = Interface.launch(fetchCmd);
// Sanity check the returned string array
if (fetchStrings == null) {
log().error("fetchInRange: Unexpected error issuing RRD 'fetch' command, no error text available.");
return null;
}
// Check error string at index 0, will be null if 'fetch' was successful
if (fetchStrings[0] != null) {
log().error("fetchInRange: RRD database 'fetch' failed, reason: " + fetchStrings[0]);
return null;
}
// Sanity check
if (fetchStrings[1] == null || fetchStrings[2] == null) {
log().error("fetchInRange: RRD database 'fetch' failed, no data retrieved.");
return null;
}
int numFetched = fetchStrings.length;
if (log().isDebugEnabled()) {
log().debug("fetchInRange: got " + numFetched + " strings from RRD");
}
// String at index 1 contains the RRDs datasource names
//
String[] dsNames = fetchStrings[1].split("\\s");
int dsIndex = 0;
for (int i = 0; i < dsNames.length; i++) {
if (dsNames[i].equals(ds)) dsIndex = i;
}
String dsName = dsNames[dsIndex].trim();
Double dsValue;
// Back through the RRD output until I get something interesting
for(int i = fetchStrings.length - 2; i > 1; i--) {
String[] dsValues = fetchStrings[i].split("\\s");
if ( dsValues[dsIndex].trim().equalsIgnoreCase("nan") ) {
log().debug("fetchInRange: Got a NaN value - continuing back in time");
} else {
try {
dsValue = new Double(dsValues[dsIndex].trim());
if (log().isDebugEnabled()) {
log().debug("fetchInRange: fetch successful: " + dsName + "= " + dsValue);
}
return dsValue;
} catch (NumberFormatException nfe) {
log().warn("fetchInRange: Unable to convert fetched value (" + dsValues[dsIndex].trim() + ") to Double for data source " + dsName);
throw nfe;
}
}
}
return null;
}
/**
* {@inheritDoc}
*
* Executes the given graph command as process with workDir as the current
* directory. The output stream of the command (a PNG image) is copied to a
* the InputStream returned from the method.
*/
public InputStream createGraph(String command, File workDir) throws IOException, RrdException {
byte[] byteArray = createGraphAsByteArray(command, workDir);
return new ByteArrayInputStream(byteArray);
}
private byte[] createGraphAsByteArray(String command, File workDir) throws IOException, RrdException {
String[] commandArray = StringUtils.createCommandArray(command, '@');
Process process;
try {
process = Runtime.getRuntime().exec(commandArray, null, workDir);
} catch (IOException e) {
IOException newE = new IOException("IOException thrown while executing command '" + command + "' in " + workDir.getAbsolutePath() + ": " + e);
newE.initCause(e);
throw newE;
}
// this closes the stream when its finished
byte[] byteArray = FileCopyUtils.copyToByteArray(process.getInputStream());
// this close the stream when its finished
String errors = FileCopyUtils.copyToString(new InputStreamReader(process.getErrorStream()));
// one particular warning message that originates in libart should be ignored
if (errors.length() > 0 && errors.contains(IGNORABLE_LIBART_WARNING_STRING)) {
log().debug("Ignoring libart warning message in rrdtool stderr stream: " + IGNORABLE_LIBART_WARNING_STRING);
errors = errors.replaceAll(IGNORABLE_LIBART_WARNING_REGEX, "");
}
if (errors.length() > 0) {
throw new RrdException(errors);
}
return byteArray;
}
/**
* No stats are kept for this implementation.
*
* @return a {@link java.lang.String} object.
*/
public String getStats() {
return "";
}
/**
* <p>log</p>
*
* @return a {@link org.opennms.core.utils.ThreadCategory} object.
*/
private final ThreadCategory log() {
return ThreadCategory.getInstance(getClass());
}
// These offsets work perfectly for ranger@ with rrdtool 1.2.23 and Firefox
/**
* <p>getGraphLeftOffset</p>
*
* @return a int.
*/
public int getGraphLeftOffset() {
return 65;
}
/**
* <p>getGraphRightOffset</p>
*
* @return a int.
*/
public int getGraphRightOffset() {
return -30;
}
/**
* <p>getGraphTopOffsetWithText</p>
*
* @return a int.
*/
public int getGraphTopOffsetWithText() {
return -75;
}
/**
* <p>getDefaultFileExtension</p>
*
* @return a {@link java.lang.String} object.
*/
public String getDefaultFileExtension() {
return ".rrd";
}
/** {@inheritDoc} */
public RrdGraphDetails createGraphReturnDetails(String command, File workDir) throws IOException, org.opennms.netmgt.rrd.RrdException {
// Creating Temp PNG File
File pngFile = File.createTempFile("opennms.rrdtool.", ".png");
command = command.replaceFirst("graph - ", "graph " + pngFile.getAbsolutePath() + " ");
int width;
int height;
String[] printLines;
InputStream pngStream;
try {
// Executing RRD Command
InputStream is = createGraph(command, workDir);
// Processing Command Output
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
try {
String s[] = reader.readLine().split("x");
width = Integer.parseInt(s[0]);
height = Integer.parseInt(s[1]);
List<String> printLinesList = new ArrayList<String>();
String line = null;
while ((line = reader.readLine()) != null) {
printLinesList.add(line);
}
printLines = printLinesList.toArray(new String[printLinesList.size()]);
} finally {
reader.close();
}
// Creating PNG InputStream
byte[] byteArray = FileCopyUtils.copyToByteArray(pngFile);
pngStream = new ByteArrayInputStream(byteArray);
} catch (Throwable e) {
throw new RrdException("Can't execute command " + command, e);
} finally {
pngFile.delete();
}
// Creating Graph Details
RrdGraphDetails details = new JniGraphDetails(width, height, printLines, pngStream);
return details;
}
/** {@inheritDoc} */
public void promoteEnqueuedFiles(Collection<String> rrdFiles) {
// no need to do anything since this strategy doesn't queue
}
}