#!/usr/bin/php -q
This script is only meant to run at the command line.');
}
// We are not talking to the browser
$no_http_headers = true;
$dir = __DIR__;
chdir($dir);
if (strpos($dir, 'spikekill') !== false) {
chdir('../../');
}
// Start Initialization Section
if (file_exists('./include/global.php') & !file_exists('../rrdtool-proxy.php')) {
include_once('./include/global.php');
$using_cacti = true;
} else {
$using_cacti = false;
}
// setup defaults
$debug = false;
$dryrun = false;
$avgnan = 'avg';
$rrdfile = '';
$std_kills = false;
$var_kills = false;
$html = false;
$backup = false;
$config = [];
$new_output = [];
if ($using_cacti) {
$method = read_config_option('spikekill_method');
$numspike = read_config_option('spikekill_number');
$stddev = read_config_option('spikekill_deviations');
$percent = read_config_option('spikekill_percent');
$outliers = read_config_option('spikekill_outliers');
} else {
$method = 1; // Standard Deviation
$numspike = 100;
$stddev = 10;
$percent = 500;
$outliers = 5;
}
// process calling arguments
$parms = $_SERVER['argv'];
array_shift($parms);
foreach ($parms as $parameter) {
@[$arg, $value] = @explode('=', $parameter);
switch ($arg) {
case '--method':
case '-M':
if ($value == 'variance') {
$method = 2;
} elseif ($value == 'stddev') {
$method = 1;
} else {
print "FATAL: You must specify either 'stddev' or 'variance' as methods.\n\n";
display_help();
exit(-11);
}
break;
case '--avgnan':
case '-A':
if ($value == 'avg') {
$avgnan = 'avg';
} elseif ($value == 'nan') {
$avgnan = 'nan';
} else {
print "FATAL: You must specify either 'avg' or 'nan' as replacement methods.\n\n";
display_help();
exit(-10);
}
break;
case '--rrdfile':
case '-R':
$rrdfile = $value;
if (!file_exists($rrdfile)) {
print "FATAL: File '$rrdfile' does not exist.\n";
exit(-9);
}
if (!is_writable($rrdfile)) {
print "FATAL: File '$rrdfile' is not writable by this account.\n";
exit(-8);
}
break;
case '--stddev':
case '-S':
$stddev = $value;
if (!is_numeric($stddev) || ($stddev < 1)) {
print "FATAL: Standard Deviation must be a positive integer.\n\n";
display_help();
exit(-7);
}
break;
case '--outliers':
case '-O':
$outliers = $value;
if (!is_numeric($outliers) || ($outliers < 1)) {
print "FATAL: The number of outliers to exclude must be a positive integer.\n\n";
display_help();
exit(-6);
}
break;
case '--percent':
case '-P':
$percent = $value;
if (!is_numeric($percent) || ($percent <= 0)) {
print "FATAL: Percent deviation must be a positive floating point number.\n\n";
display_help();
exit(-5);
}
break;
case '--html':
$html = true;
break;
case '--backup':
$backup = true;
break;
case '-d':
case '--debug':
$debug = true;
break;
case '-D':
case '--dryrun':
$dryrun = true;
break;
case '--number':
case '-N':
case '-n':
$numspike = $value;
if (!is_numeric($numspike) || ($numspike < 1)) {
print "FATAL: Number of spikes to remove must be a positive integer\n\n";
display_help();
exit(-4);
}
break;
case '-h':
case '-v':
case '-V':
case '--version':
case '--help':
display_help();
exit(0);
default:
print 'ERROR: Invalid Parameter ' . $parameter . "\n\n";
display_help();
exit(-3);
}
}
// additional error check
if ($rrdfile == '') {
print "FATAL: You must specify an RRDfile!\n\n";
display_help();
exit(-2);
}
// let's see if we can find rrdtool
$rrdtool_path = getenv('RRDP_RRDTOOL_PATH');
if ($rrdtool_path === false || $rrdtool_path === '') {
$rrdtool_path = $using_cacti ? read_config_option('path_rrdtool') : (substr_count(PHP_OS, 'WIN') ? 'rrdtool.exe' : 'rrdtool');
}
if (!$using_cacti) {
$response = removespikes_run_rrdtool($rrdtool_path, []);
if ($response !== false && strlen($response['stdout'] . $response['stderr'])) {
$response = $response['stdout'] . $response['stderr'];
$response_array = explode(' ', $response);
print 'NOTE: Using ' . $response_array[0] . ' Version ' . $response_array[1] . "\n";
} else {
print "FATAL: RRDTool not found in path. Please insure RRDTool can be found in your path!\n";
exit(-1);
}
}
// determine the temporary file name
$seed = mt_rand();
if ($using_cacti) {
if ($config['cacti_server_os'] == 'win32') {
$tempdir = getenv('TEMP');
$xmlfile = tempnam($tempdir, 'rrdproxy-dump-');
$bakfile = $tempdir . '/' . str_replace('.rrd', '', basename($rrdfile)) . '.backup.' . $seed . '.rrd';
} else {
$tempdir = '/tmp';
$xmlfile = tempnam($tempdir, 'rrdproxy-dump-');
$bakfile = '/tmp/' . str_replace('.rrd', '', basename($rrdfile)) . '.backup.' . $seed . '.rrd';
}
} elseif (substr_count(PHP_OS, 'WIN')) {
$tempdir = getenv('TEMP');
$xmlfile = tempnam($tempdir, 'rrdproxy-dump-');
$bakfile = $tempdir . '/' . str_replace('.rrd', '', basename($rrdfile)) . '.backup.' . $seed . '.rrd';
} else {
$tempdir = '/tmp';
$xmlfile = tempnam($tempdir, 'rrdproxy-dump-');
$bakfile = '/tmp/' . str_replace('.rrd', '', basename($rrdfile)) . '.backup.' . $seed . '.rrd';
}
if ($xmlfile === false) {
print "FATAL: Unable to create a secure temporary file.\n";
exit(-12);
}
if ($html) {
print "
";
}
if ($using_cacti) {
cacti_log("NOTE: Removing Spikes for '$rrdfile', Method:'$method'", false, 'WEBUI');
}
// execute the dump command
print ($html ? "| " : '') . "NOTE: Creating XML file '$xmlfile' from '$rrdfile'" . ($html ? " |
\n" : "\n");
$dump_result = removespikes_run_rrdtool($rrdtool_path, ['dump', $rrdfile]);
if ($dump_result !== false && $dump_result['status'] === 0 && $dump_result['stdout'] !== '') {
file_put_contents($xmlfile, $dump_result['stdout'], LOCK_EX);
} else {
unlink($xmlfile);
}
// read the xml file into an array
if (file_exists($xmlfile)) {
$output = file($xmlfile);
// remove the temp file
unlink($xmlfile);
} else {
if ($using_cacti) {
print ($html ? "| " : '') . 'FATAL: RRDtool Command Failed. Please verify that the RRDtool path is valid in Settings->Paths!' . ($html ? " |
\n" : "\n");
} else {
print ($html ? "| " : '') . 'FATAL: RRDtool Command Failed. Please insure your RRDtool install is valid!' . ($html ? " |
\n" : "\n");
}
exit(-12);
}
// backup the rrdfile if requested
if ($backup && !$dryrun) {
if (copy($rrdfile, $bakfile)) {
print ($html ? "| " : '') . "NOTE: RRDfile '$rrdfile' backed up to '$bakfile'" . ($html ? " |
\n" : "\n");
} else {
print ($html ? "| " : '') . "FATAL: RRDfile Backup of '$rrdfile' to '$bakfile' FAILED!" . ($html ? " |
\n" : "\n");
exit(-13);
}
}
// process the xml file and remove all comments
$output = removeComments($output);
/* Read all the rra's ds values and obtain the following pieces of information from each
rra archive.
* numsamples - The number of 'valid' non-nan samples
* sumofsamples - The sum of all 'valid' samples.
* average - The average of all samples
* standard_deviation - The standard deviation of all samples
* max_value - The maximum value of all samples
* min_value - The minimum value of all samples
* max_cutoff - Any value above this value will be set to the average.
* min_cutoff - Any value lower than this value will be set to the average.
This will end up being a n-dimensional array as follows:
rra[x][ds#]['totalsamples'];
rra[x][ds#]['numsamples'];
rra[x][ds#]['sumofsamples'];
rra[x][ds#]['average'];
rra[x][ds#]['stddev'];
rra[x][ds#]['max_value'];
rra[x][ds#]['min_value'];
rra[x][ds#]['max_cutoff'];
rra[x][ds#]['min_cutoff'];
There will also be a secondary array created with the actual samples. This
array will be used to calculate the standard deviation of the sample set.
samples[rra_num][ds_num][];
Also track the min and max value for each ds and store it into the two
arrays: ds_min[ds#], ds_max[ds#].
The we don't need to know the type of rra, only it's number for this analysis
the same applies for the ds' as well.
*/
$rra = [];
$rra_cf = [];
$rra_pdp = [];
$rra_num = 0;
$ds_num = 0;
$total_kills = 0;
$in_rra = false;
$in_db = false;
$ds_min = [];
$ds_max = [];
$ds_name = [];
/* perform a first pass on the array and do the following:
1) Get the number of good samples per ds
2) Get the sum of the samples per ds
3) Get the max and min values for all samples
4) Build both the rra and sample arrays
5) Get each ds' min and max values
*/
if (sizeof($output)) {
foreach ($output as $line) {
if (substr_count($line, '')) {
$linearray = explode('', $line);
// discard the row
array_shift($linearray);
$ds_num = 0;
foreach ($linearray as $dsvalue) {
// peel off garbage
$dsvalue = trim(str_replace('', '', str_replace('', '', $dsvalue)));
if (strtolower($dsvalue) != 'nan') {
if (!isset($rra[$rra_num][$ds_num]['numsamples'])) {
$rra[$rra_num][$ds_num]['numsamples'] = 1;
} else {
$rra[$rra_num][$ds_num]['numsamples']++;
}
if (!isset($rra[$rra_num][$ds_num]['sumofsamples'])) {
$rra[$rra_num][$ds_num]['sumofsamples'] = $dsvalue;
} else {
$rra[$rra_num][$ds_num]['sumofsamples'] += $dsvalue;
}
if (!isset($rra[$rra_num][$ds_num]['max_value'])) {
$rra[$rra_num][$ds_num]['max_value'] = $dsvalue;
} elseif ($dsvalue > $rra[$rra_num][$ds_num]['max_value']) {
$rra[$rra_num][$ds_num]['max_value'] = $dsvalue;
}
if (!isset($rra[$rra_num][$ds_num]['min_value'])) {
$rra[$rra_num][$ds_num]['min_value'] = $dsvalue;
} elseif ($dsvalue < $rra[$rra_num][$ds_num]['min_value']) {
$rra[$rra_num][$ds_num]['min_value'] = $dsvalue;
}
// store the sample for standard deviation calculation
$samples[$rra_num][$ds_num][] = $dsvalue;
}
if (!isset($rra[$rra_num][$ds_num]['totalsamples'])) {
$rra[$rra_num][$ds_num]['totalsamples'] = 1;
} else {
$rra[$rra_num][$ds_num]['totalsamples']++;
}
$ds_num++;
}
} elseif (substr_count($line, '')) {
$in_rra = true;
} elseif (substr_count($line, '')) {
$ds_min[] = trim(str_replace('', '', str_replace('', '', trim($line))));
} elseif (substr_count($line, '')) {
$ds_max[] = trim(str_replace('', '', str_replace('', '', trim($line))));
} elseif (substr_count($line, '')) {
$ds_name[] = trim(str_replace('', '', str_replace('', '', trim($line))));
} elseif (substr_count($line, '')) {
$rra_cf[] = trim(str_replace('', '', str_replace('', '', trim($line))));
} elseif (substr_count($line, '')) {
$rra_pdp[] = trim(str_replace('', '', str_replace('', '', trim($line))));
} elseif (substr_count($line, '')) {
$in_rra = false;
$rra_num++;
} elseif (substr_count($line, '')) {
$step = trim(str_replace('', '', str_replace('', '', trim($line))));
}
}
}
// For all the samples determine the average with the outliers removed
calculateVarianceAverages($rra, $samples);
/* Now scan the rra array and the samples array and calculate the following
1) The standard deviation of all samples
2) The average of all samples per ds
3) The max and min cutoffs of all samples
4) The number of kills in each ds based upon the thresholds
*/
print ($html ? "| " : '') . "NOTE: Searching for Spikes in XML file '$xmlfile'" . ($html ? " |
\n" : "\n");
calculateOverallStatistics($rra, $samples);
// debugging and/or status report
if ($debug || $dryrun) {
outputStatistics($rra);
}
// create an output array
if ($method == 1) {
// standard deviation subroutine
if ($std_kills) {
if (!$dryrun) {
$new_output = updateXML($output, $rra);
}
} else {
print ($html ? "| " : '') . "NOTE: NO Standard Deviation Spikes found in '$rrdfile'" . ($html ? " |
\n" : "\n");
}
} else {
// variance subroutine
if ($var_kills) {
if (!$dryrun) {
$new_output = updateXML($output, $rra);
}
} else {
print ($html ? "| " : '') . "NOTE: NO Variance Spikes found in '$rrdfile'" . ($html ? " |
\n" : "\n");
}
}
// finally update the file XML file and Reprocess the RRDfile
if (!$dryrun) {
if ($total_kills) {
if (writeXMLFile($new_output, $xmlfile)) {
if (backupRRDFile($rrdfile)) {
if (!createRRDFileFromXML($xmlfile, $rrdfile)) {
print ($html ? "| " : '') . "FATAL: Unable to restore '$rrdfile' from '$xmlfile'" . ($html ? " |
\n" : "\n");
exit(-14);
}
} else {
print ($html ? "| " : '') . "FATAL: Unable to backup '$rrdfile'" . ($html ? " |
\n" : "\n");
}
} else {
print ($html ? "| " : '') . "FATAL: Unable to write XML file '$xmlfile'" . ($html ? " |
\n" : "\n");
}
}
} else {
print ($html ? "| " : '') . 'NOTE: Dryrun requested. No updates performed' . ($html ? " |
\n" : "\n");
}
if ($html) {
print '
';
}
// All Functions
/**
* Restores an RRD file from its (spike-cleaned) XML dump via `rrdtool restore`.
*
* @param string $xmlfile
* @param string $rrdfile
*
* @return bool
*/
function createRRDFileFromXML($xmlfile, $rrdfile) {
global $html, $rrdtool_path;
// execute the dump command
print ($html ? "| " : '') . "NOTE: Re-Importing '$xmlfile' to '$rrdfile'" . ($html ? " |
\n" : "\n");
$result = removespikes_run_rrdtool($rrdtool_path, ['restore', '-f', '-r', $xmlfile, $rrdfile]);
$response = $result === false ? 'Unable to start RRDtool' : $result['stdout'] . $result['stderr'];
if (strlen($response)) {
print ($html ? "| " : '') . $response . ($html ? " |
\n" : "\n");
}
return $result !== false && $result['status'] === 0;
}
/**
* Runs the rrdtool binary with the given argv-style arguments (no shell
* interpolation) and captures its combined stdout/stderr and exit status.
*
* @param string $binary
* @param array $arguments
*
* @return array{status: int, stdout: string, stderr: string}|false
*/
function removespikes_run_rrdtool($binary, $arguments) {
$descriptors = [
1 => ['pipe', 'w'],
2 => ['redirect', 1],
];
$process = proc_open(array_merge([$binary], $arguments), $descriptors, $pipes);
if (!is_resource($process)) {
return false;
}
$stdout = stream_get_contents($pipes[1]);
fclose($pipes[1]);
$status = proc_close($process);
return ['status' => $status, 'stdout' => $stdout, 'stderr' => ''];
}
/**
* Writes the (spike-cleaned) XML dump lines out to $xmlfile.
*
* @param array $output
* @param string $xmlfile
*
* @return int|false
*/
function writeXMLFile($output, $xmlfile) {
return file_put_contents($xmlfile, $output);
}
/**
* Copies $rrdfile into the configured (or temp) backup directory before it is
* modified, appending a random seed to the filename if a file of that name
* already exists there.
*
* @param string $rrdfile
*
* @return bool
*/
function backupRRDFile($rrdfile) {
global $using_cacti, $tempdir, $seed, $html;
if ($using_cacti) {
$backupdir = read_config_option('spikekill_backupdir');
if ($backupdir == '') {
$backupdir = $tempdir;
}
} else {
$backupdir = $tempdir;
}
if (file_exists($backupdir . '/' . basename($rrdfile))) {
$newfile = basename($rrdfile) . '.' . $seed;
} else {
$newfile = basename($rrdfile);
}
print ($html ? "| " : '') . "NOTE: Backing Up '$rrdfile' to '" . $backupdir . '/' . $newfile . "'" . ($html ? " |
\n" : "\n");
return copy($rrdfile, $backupdir . '/' . $newfile);
}
/**
* Computes each RRA/data-source's outlier-trimmed "variance" average (the mean
* of the samples after removing the top/bottom $outliers extremes), storing it
* into $rra[$rra_num][$ds_num]['variance_avg'].
*
* @param array>> $rra
* @param array>> $samples
*
* @return void
*/
function calculateVarianceAverages(&$rra, &$samples) {
global $outliers;
if (sizeof($samples)) {
foreach ($samples as $rra_num => $dses) {
if (sizeof($dses)) {
foreach ($dses as $ds_num => $ds) {
if (sizeof($ds) < $outliers * 3) {
$rra[$rra_num][$ds_num]['variance_avg'] = 'NAN';
} else {
rsort($ds, SORT_NUMERIC);
$ds = array_slice($ds, $outliers);
sort($ds, SORT_NUMERIC);
$ds = array_slice($ds, $outliers);
$rra[$rra_num][$ds_num]['variance_avg'] = array_sum($ds) / sizeof($ds);
}
}
}
}
}
}
/**
* For every RRA/data-source, computes the standard deviation, average, and
* min/max cutoffs from the collected samples, then counts (and flags via
* $std_kills/$var_kills) how many samples fall outside the standard-deviation
* and variance thresholds.
*
* @param array>> $rra
* @param array>> $samples
*
* @return void
*/
function calculateOverallStatistics(&$rra, &$samples) {
global $percent, $stddev, $ds_min, $ds_max, $var_kills, $std_kills;
$rra_num = 0;
if (sizeof($rra)) {
foreach ($rra as $dses) {
$ds_num = 0;
if (sizeof($dses)) {
foreach ($dses as $ds) {
if (isset($samples[$rra_num][$ds_num])) {
$rra[$rra_num][$ds_num]['standard_deviation'] = standard_deviation($samples[$rra_num][$ds_num]);
if ($rra[$rra_num][$ds_num]['standard_deviation'] == 'NAN') {
$rra[$rra_num][$ds_num]['standard_deviation'] = 0;
}
$rra[$rra_num][$ds_num]['average'] = $rra[$rra_num][$ds_num]['sumofsamples'] / $rra[$rra_num][$ds_num]['numsamples'];
$rra[$rra_num][$ds_num]['min_cutoff'] = $rra[$rra_num][$ds_num]['average'] - ($stddev * $rra[$rra_num][$ds_num]['standard_deviation']);
if ($rra[$rra_num][$ds_num]['min_cutoff'] < $ds_min[$ds_num]) {
$rra[$rra_num][$ds_num]['min_cutoff'] = $ds_min[$ds_num];
}
$rra[$rra_num][$ds_num]['max_cutoff'] = $rra[$rra_num][$ds_num]['average'] + ($stddev * $rra[$rra_num][$ds_num]['standard_deviation']);
if ($rra[$rra_num][$ds_num]['max_cutoff'] > $ds_max[$ds_num]) {
$rra[$rra_num][$ds_num]['max_cutoff'] = $ds_max[$ds_num];
}
$rra[$rra_num][$ds_num]['numnksamples'] = 0;
$rra[$rra_num][$ds_num]['sumnksamples'] = 0;
$rra[$rra_num][$ds_num]['avgnksamples'] = 0;
// go through values and find cutoffs
$rra[$rra_num][$ds_num]['stddev_killed'] = 0;
$rra[$rra_num][$ds_num]['variance_killed'] = 0;
if (sizeof($samples[$rra_num][$ds_num])) {
foreach ($samples[$rra_num][$ds_num] as $sample) {
if (($sample > $rra[$rra_num][$ds_num]['max_cutoff']) ||
($sample < $rra[$rra_num][$ds_num]['min_cutoff'])) {
debug(sprintf("Std Kill: Value '%.4e', StandardDev '%.4e', StdDevLimit '%.4e'", $sample, $rra[$rra_num][$ds_num]['standard_deviation'], ($rra[$rra_num][$ds_num]['max_cutoff'] * (1 + $percent))));
$rra[$rra_num][$ds_num]['stddev_killed']++;
$std_kills = true;
} else {
$rra[$rra_num][$ds_num]['numnksamples']++;
$rra[$rra_num][$ds_num]['sumnksamples'] += $sample;
}
if ($rra[$rra_num][$ds_num]['variance_avg'] == 'NAN') {
// not enough samples to calculate
} elseif ($sample > ($rra[$rra_num][$ds_num]['variance_avg'] * (1 + $percent))) {
// kill based upon variance
debug(sprintf("Var Kill: Value '%.4e', VarianceDev '%.4e', VarianceLimit '%.4e'", $sample, $rra[$rra_num][$ds_num]['variance_avg'], ($rra[$rra_num][$ds_num]['variance_avg'] * (1 + $percent))));
$rra[$rra_num][$ds_num]['variance_killed']++;
$var_kills = true;
}
}
}
if ($rra[$rra_num][$ds_num]['numnksamples'] > 0) {
$rra[$rra_num][$ds_num]['avgnksamples'] = $rra[$rra_num][$ds_num]['sumnksamples'] / $rra[$rra_num][$ds_num]['numnksamples'];
}
} else {
$rra[$rra_num][$ds_num]['standard_deviation'] = 'N/A';
$rra[$rra_num][$ds_num]['average'] = 'N/A';
$rra[$rra_num][$ds_num]['min_cutoff'] = 'N/A';
$rra[$rra_num][$ds_num]['max_cutoff'] = 'N/A';
$rra[$rra_num][$ds_num]['numnksamples'] = 'N/A';
$rra[$rra_num][$ds_num]['sumnksamples'] = 'N/A';
$rra[$rra_num][$ds_num]['avgnksamples'] = 'N/A';
$rra[$rra_num][$ds_num]['stddev_killed'] = 'N/A';
$rra[$rra_num][$ds_num]['variance_killed'] = 'N/A';
$rra[$rra_num][$ds_num]['stddev_killed'] = 'N/A';
$rra[$rra_num][$ds_num]['numnksamples'] = 'N/A';
$rra[$rra_num][$ds_num]['sumnksamples'] = 'N/A';
$rra[$rra_num][$ds_num]['variance_killed'] = 'N/A';
$rra[$rra_num][$ds_num]['avgnksamples'] = 'N/A';
}
$ds_num++;
}
}
$rra_num++;
}
}
}
/**
* Prints a per-data-source summary table (plain text or HTML) of the computed
* statistics and kill counts for each RRA.
*
* @param array>> $rra
*
* @return void
*/
function outputStatistics($rra) {
global $rra_cf, $rra_name, $ds_name, $rra_pdp, $html;
if (sizeof($rra)) {
if (!$html) {
print "\n";
printf("%10s %16s %10s %7s %7s %10s %10s %10s %10s %10s %10s %10s %10s %10s %10s\n",
'Size', 'DataSource', 'CF', 'Samples', 'NonNan', 'Avg', 'StdDev',
'MaxValue', 'MinValue', 'MaxStdDev', 'MinStdDev', 'StdKilled', 'VarKilled', 'StdDevAvg', 'VarAvg');
printf("%10s %16s %10s %7s %7s %10s %10s %10s %10s %10s %10s %10s %10s %10s %10s\n",
'----------', '---------------', '----------', '-------', '-------', '----------', '----------', '----------',
'----------', '----------', '----------', '----------', '----------', '----------',
'----------');
foreach ($rra as $rra_key => $dses) {
if (sizeof($dses)) {
foreach ($dses as $dskey => $ds) {
printf('%10s %16s %10s %7s %7s ' .
($ds['average'] < 1E6 ? '%10s ' : '%10.4e ') .
($ds['standard_deviation'] < 1E6 ? '%10s ' : '%10.4e ') .
(isset($ds['max_value']) ? ($ds['max_value'] < 1E6 ? '%10s ' : '%10.4e ') : '%10s ') .
(isset($ds['min_value']) ? ($ds['min_value'] < 1E6 ? '%10s ' : '%10.4e ') : '%10s ') .
(isset($ds['max_cutoff']) ? ($ds['max_cutoff'] < 1E6 ? '%10s ' : '%10.4e ') : '%10s ') .
(isset($ds['min_cutoff']) ? ($ds['min_cutoff'] < 1E6 ? '%10s ' : '%10.4e ') : '%10s ') .
'%10s %10s ' .
(isset($ds['avgnksampled']) ? ($ds['avgnksamples'] < 1E6 ? '%10s ' : '%10.4e ') : '%10s ') .
(isset($ds['variance_avg']) ? ($ds['variance_avg'] < 1E6 ? '%10s ' : '%10.4e ') : '%10s ') . "\n",
displayTime($rra_pdp[$rra_key]),
$ds_name[$dskey],
$rra_cf[$rra_key],
$ds['totalsamples'],
(isset($ds['numsamples']) ? $ds['numsamples'] : '0'),
($ds['average'] != 'N/A' ? round($ds['average'],2) : $ds['average']),
($ds['standard_deviation'] != 'N/A' ? round($ds['standard_deviation'],2) : $ds['standard_deviation']),
(isset($ds['max_value']) ? round($ds['max_value'],2) : 'N/A'),
(isset($ds['min_value']) ? round($ds['min_value'],2) : 'N/A'),
($ds['max_cutoff'] != 'N/A' ? round($ds['max_cutoff'],2) : $ds['max_cutoff']),
($ds['min_cutoff'] != 'N/A' ? round($ds['min_cutoff'],2) : $ds['min_cutoff']),
$ds['stddev_killed'],
$ds['variance_killed'],
($ds['avgnksamples'] != 'N/A' ? round($ds['avgnksamples'],2) : $ds['avgnksamples']),
(isset($ds['variance_avg']) ? round($ds['variance_avg'],2) : 'N/A'));
}
}
}
print "\n";
} else {
printf("| %s | %s | %s | %s | %s | %s | %s | %s | %s | %s | %s | %s | %s | %s | %s |
\n",
'Size', 'DataSource', 'CF', 'Samples', 'NonNan', 'Avg', 'StdDev',
'MaxValue', 'MinValue', 'MaxStdDev', 'MinStdDev', 'StdKilled', 'VarKilled', 'StdDevAvg', 'VarAvg');
foreach ($rra as $rra_key => $dses) {
if (sizeof($dses)) {
foreach ($dses as $dskey => $ds) {
printf('| %s | %s | %s | %s | %s | ' .
($ds['average'] < 1E6 ? '%s | ' : '%.4e | ') .
($ds['standard_deviation'] < 1E6 ? '%s | ' : '%.4e | ') .
(isset($ds['max_value']) ? ($ds['max_value'] < 1E6 ? '%s | ' : '%.4e | ') : '%s | ') .
(isset($ds['min_value']) ? ($ds['min_value'] < 1E6 ? '%s | ' : '%.4e | ') : '%s | ') .
(isset($ds['max_cutoff']) ? ($ds['max_cutoff'] < 1E6 ? '%s | ' : '%.4e | ') : '%s | ') .
(isset($ds['min_cutoff']) ? ($ds['min_cutoff'] < 1E6 ? '%s | ' : '%.4e | ') : '%s | ') .
'%s | %s | ' .
(isset($ds['avgnksampled']) ? ($ds['avgnksamples'] < 1E6 ? '%s | ' : '%.4e | ') : '%s | ') .
(isset($ds['variance_avg']) ? ($ds['variance_avg'] < 1E6 ? "%s |
\n" : "%.4e\n") : "%s\n") . "\n",
displayTime($rra_pdp[$rra_key]),
$ds_name[$dskey],
$rra_cf[$rra_key],
$ds['totalsamples'],
(isset($ds['numsamples']) ? $ds['numsamples'] : '0'),
($ds['average'] != 'N/A' ? round($ds['average'],2) : $ds['average']),
($ds['standard_deviation'] != 'N/A' ? round($ds['standard_deviation'],2) : $ds['standard_deviation']),
(isset($ds['max_value']) ? round($ds['max_value'],2) : 'N/A'),
(isset($ds['min_value']) ? round($ds['min_value'],2) : 'N/A'),
($ds['max_cutoff'] != 'N/A' ? round($ds['max_cutoff'],2) : $ds['max_cutoff']),
($ds['min_cutoff'] != 'N/A' ? round($ds['min_cutoff'],2) : $ds['min_cutoff']),
$ds['stddev_killed'],
$ds['variance_killed'],
($ds['avgnksamples'] != 'N/A' ? round($ds['avgnksamples'],2) : $ds['avgnksamples']),
(isset($ds['variance_avg']) ? round($ds['variance_avg'],2) : 'N/A'));
}
}
}
}
}
}
/**
* Rewrites the dumped XML's value lines, replacing any sample outside
* the computed thresholds with the RRA's average (or 'NaN'), up to $numspike
* replacements per RRA.
*
* @param array $output
* @param array>> $rra
*
* @return array
*/
function updateXML(&$output, &$rra) {
global $numspike, $percent, $avgnan, $method, $total_kills;
// variance subroutine
$rra_num = 0;
$ds_num = 0;
$kills = 0;
$new_array = [];
if (sizeof($output)) {
foreach ($output as $line) {
if (substr_count($line, '')) {
$linearray = explode('', $line);
// discard the row
array_shift($linearray);
// initialize variables
$ds_num = 0;
$out_row = '';
foreach ($linearray as $dsvalue) {
// peel off garbage
$dsvalue = trim(str_replace('
', '', str_replace('', '', $dsvalue)));
if (strtolower($dsvalue) == 'nan') {
// do nothing, it's a NaN
} else {
if ($method == 2) {
if ($dsvalue > (1 + $percent) * $rra[$rra_num][$ds_num]['variance_avg']) {
if ($kills < $numspike) {
if ($avgnan == 'avg') {
$dsvalue = $rra[$rra_num][$ds_num]['variance_avg'];
} else {
$dsvalue = 'NaN';
}
$kills++;
$total_kills++;
}
}
} else {
if (($dsvalue > $rra[$rra_num][$ds_num]['max_cutoff']) ||
($dsvalue < $rra[$rra_num][$ds_num]['min_cutoff'])) {
if ($kills < $numspike) {
if ($avgnan == 'avg') {
$dsvalue = $rra[$rra_num][$ds_num]['average'];
} else {
$dsvalue = 'NaN';
}
$kills++;
$total_kills++;
}
}
}
}
$out_row .= ' ' . $dsvalue . '';
$ds_num++;
}
$out_row .= '
';
$new_array[] = $out_row;
} else {
if (substr_count($line, '')) {
$ds_minmax = [];
$rra_num++;
$kills = 0;
} elseif (substr_count($line, '')) {
$ds_num++;
$kills = 0;
}
$new_array[] = $line;
}
}
}
return $new_array;
}
/**
* Strips blank lines and `` XML comments from a dumped RRD file's
* lines.
*
* @param array $output
*
* @return array|null
*/
function removeComments(&$output) {
$new_array = [];
if (sizeof($output)) {
foreach ($output as $line) {
$line = trim($line);
if ($line == '') {
continue;
} else {
// is there a comment, remove it
$comment_start = strpos($line, '');
if ($comment_start == 0) {
$line = trim(substr($line, $comment_end + 3));
} else {
$line = trim(substr($line,0,$comment_start - 1) . substr($line,$comment_end + 3));
}
}
if ($line != '') {
$new_array[] = $line;
}
}
}
// transfer the new array back to the original array
return $new_array;
}
}
/**
* Formats a PDP (primary data point) count as a human-readable duration
* (seconds, minutes, hours, or days) based on the RRD's step size.
*
* @param int $pdp
*
* @return string
*/
function displayTime($pdp) {
global $step;
$total_time = $pdp * $step; // seconds
if ($total_time < 60) {
return $total_time . ' secs';
} else {
$total_time = $total_time / 60;
if ($total_time < 60) {
return $total_time . ' mins';
} else {
$total_time = $total_time / 60;
if ($total_time < 24) {
return $total_time . ' hours';
} else {
$total_time = $total_time / 24;
return $total_time . ' days';
}
}
}
}
/**
* Prints $string prefixed with "DEBUG: " when debug output is enabled.
*
* @param string $string
*
* @return void
*/
function debug($string) {
global $debug;
if ($debug) {
print 'DEBUG: ' . $string . "\n";
}
}
/**
* Computes the (population) standard deviation of a set of numeric samples.
*
* @param array $samples
*
* @return float
*/
function standard_deviation($samples) {
$sample_count = count($samples);
$sample_square = [];
for ($current_sample = 0; $sample_count > $current_sample; ++$current_sample) {
$sample_square[$current_sample] = pow($samples[$current_sample], 2);
}
return sqrt(array_sum($sample_square) / $sample_count - pow((array_sum($samples) / $sample_count), 2));
}
// display_help - displays the usage of the function
/**
* Prints removespikes' version banner and command-line usage/help text.
*
* @return void
*/
function display_help() {
global $using_cacti;
if ($using_cacti) {
$version = spikekill_version();
} else {
$version = 'v1.1';
}
print 'Cacti Spike Remover ' . ($using_cacti ? 'v' . $version['version'] : $version) . ", Copyright 2004-2026, The Cacti Group, Inc.\n\n";
print "Usage:\n";
print "removespikes.php -R|--rrdfile=rrdfile [-M|--method=stddev] [-A|--avgnan] [-S|--stddev=N]\n";
print " [-P|--percent=N] [-N|--number=N] [-D|--dryrun] [-d|--debug]\n";
print " [--html] [-h|--help|-v|-V|--version]\n\n";
print "The RRDfile input parameter is mandatory. If no other input parameters are specified the defaults\n";
print "are taken from the Spikekill Plugin settings.\n\n";
print "-M|--method - The spike removal method to use. Options are 'stddev'|'variance'\n";
print "-A|--avgnan - The spike replacement method to use. Options are 'avg'|'nan'\n";
print "-S|--stddev - The number of standard deviations +/- allowed\n";
print "-P|--percent - The sample to sample percentage variation allowed\n";
print "-N|--number - The maximum number of spikes to remove from the RRDfile\n";
print "-D|--dryrun - If specified, the RRDfile will not be changed. Instead a summary of\n";
print " changes that would have been performed will be issued.\n";
print "--backup - Backup the original RRDfile to preserve prior values.\n\n";
print "The remainder of arguments are informational\n";
print "--html - Format the output for a web browser\n";
print "-d|--debug - Display verbose output during execution\n";
print "-v -V --version - Display this help message\n";
print "-h --help - display this help message\n";
}