0 ? true : false); // open a pipe to rrdtool for writing and reading if ($use_proxy) { $rrd_process = rrd_init(false); } else { $rrd_process = dsstats_rrdtool_init(); // dsstats_rrdtool_init() returns [false, false] when RRDtool could not be // spawned; without live pipes the execute path would fwrite() to null and // crash the poller, so bail out of this run gracefully if (!is_resource($rrd_process[0])) { cacti_log('ERROR: DSStats unable to obtain RRDtool process, skipping run', false, 'DSSTATS'); return; } } if (cacti_sizeof($rrdfiles)) { foreach ($rrdfiles as $file) { $dsses += $file['dsses']; if ($file['data_source_path'] != '') { // resolve through get_data_source_path() so the RRA containment check applies here too $rrdfile = get_data_source_path($file['local_data_id'], true); $local_data_id = $file['local_data_id']; $dsnames = explode(',', $file['dsnames']); $stats[$local_data_id] = dsstats_obtain_data_source_avgpeak_values($local_data_id, $rrdfile, $interval, $mode, $peak, $rrd_process); } else { $data_source_name = db_fetch_cell_prepared('SELECT name_cache FROM data_template_data WHERE local_data_id = ?', [$file['local_data_id']]); cacti_log("WARNING: Data Source '$data_source_name' is damaged and contains no path. Please delete and re-create both the Graph and Data Source.", false, 'DSSTATS'); } } } if ($use_proxy) { rrd_close($rrd_process); } else { dsstats_rrdtool_close($rrd_process); } dsstats_write_buffer($stats, $interval, $mode); if (!empty($type)) { $total_user += $user_time; $total_system += $system_time; $total_real += $real_time; $total_dsses += $dsses; set_config_option('dsstats_rrd_system_' . $type . '_' . $thread_id, $total_system); set_config_option('dsstats_rrd_user_' . $type . '_' . $thread_id, $total_user); set_config_option('dsstats_rrd_real_' . $type . '_' . $thread_id, $total_real); set_config_option('dsstats_total_rrds_' . $type . '_' . $thread_id, $rrd_files); set_config_option('dsstats_total_dsses_' . $type . '_' . $thread_id, $total_dsses); } } /** * dsstats_write_buffer - this routine provide bulk database insert services to the various tables that store * the average and peak information for Data Sources. * * @param array $stats_array A multi dimensional array keyed by the local_data_id that contains both * the average and max values for each internal RRDfile Data Source. * @param string $interval 'daily', 'weekly', 'monthly', and 'yearly'. Used for determining the table to * update during the dumping of the buffer. * @param int $mode The mode of collection legacy '0' or advanced '1' * * @return void */ function dsstats_write_buffer(array &$stats_array, string $interval, int $mode) : void { $outbuf = ''; $out_length = 0; $i = 1; $max_packet = 256000; // Format $stats[ldi][avg|max][rrd_name][metric] = $value if ($mode == 1) { $sql_prefix = "INSERT INTO data_source_stats_$interval (local_data_id, rrd_name, cf, average, peak, p95n, p90n, p75n, p50n, p25n, sum, stddev, lslslope, lslint, lslcorrel) VALUES"; $sql_suffix = ' ON DUPLICATE KEY UPDATE average=VALUES(average), peak=VALUES(peak), p95n=VALUES(p95n), p90n=VALUES(p90n), p75n=VALUES(p75n), p50n=VALUES(p50n), p25n=VALUES(p25n), sum=VALUES(sum), stddev=VALUES(stddev), lslslope=VALUES(lslslope), lslint=VALUES(lslint), lslcorrel=VALUES(lslcorrel)'; } else { $sql_prefix = "INSERT INTO data_source_stats_$interval (local_data_id, rrd_name, cf, average, peak) VALUES"; $sql_suffix = ' ON DUPLICATE KEY UPDATE average=VALUES(average), peak=VALUES(peak)'; } $overhead = strlen($sql_prefix) + strlen($sql_suffix); // don't attempt to process an empty array // Format $stats[ldi][avg|max][rrd_name][metric] = $value if (cacti_sizeof($stats_array)) { foreach ($stats_array as $local_data_id => $ldi_stats) { if (cacti_sizeof($ldi_stats)) { foreach ($ldi_stats as $cf => $cf_stats) { if ($cf == 'avg') { $mycf = '0'; } else { $mycf = '1'; } foreach ($cf_stats as $rrd_name => $stats) { if ($mode == 0) { $outbuf .= ($i == 1 ? ' ' : ', ') . "('" . $local_data_id . "','" . $rrd_name . "','" . $mycf . "','" . $stats['avg'] . "','" . $stats['peak'] . "')"; } else { $outbuf .= ($i == 1 ? ' ' : ', ') . "('" . $local_data_id . "','" . $rrd_name . "','" . $mycf . "','" . $stats['avg'] . "','" . $stats['peak'] . "','" . $stats['p95n'] . "','" . $stats['p90n'] . "','" . $stats['p75n'] . "','" . $stats['p50n'] . "','" . $stats['p25n'] . "','" . $stats['sum'] . "','" . $stats['stddev'] . "','" . $stats['lslslope'] . "','" . $stats['lslint'] . "','" . $stats['lslcorrel'] . "')"; } $out_length += strlen($outbuf); if (($out_length + $overhead) > $max_packet) { db_execute($sql_prefix . $outbuf . $sql_suffix); $outbuf = ''; $out_length = 0; $i = 1; } else { $i++; } } } } else { $host_id = db_fetch_cell_prepared('SELECT host_id FROM data_local WHERE id = ?', [$local_data_id]); cacti_log(sprintf('WARNING: Problem with Data Source for Device[%s], DS[%s], Interval[%s]', $host_id, $local_data_id, ucfirst($interval)), false, 'DSSTATS'); } } // flush the buffer if it still has elements in it if ($out_length > 0) { db_execute($sql_prefix . $outbuf . $sql_suffix); } } } /** * dsstats_obtain_data_source_avgpeak_values - this routine, given the rrdfile name, interval and RRDtool process * pipes, will obtain the average a peak values from the RRDfile. It does this in two steps: * * 1) It first reads the RRDfile's information header to obtain all of the internal data source names, * poller interval and consolidation functions. * 2) Based upon the available consolidation functions, it then grabs either AVERAGE, and MAX, or just AVERAGE * in the case where the MAX consolidation function is not included in the RRDfile, and then proceeds to * gather data from the RRDfile for the time period in question. It allows RRDtool to select the RRA to * use by simply limiting the number of rows to be returned to the default. * * Once it has all of the information from the RRDfile. It then decomposes the resulting XML file to its * components and then calculates the AVERAGE and MAX values from that data and returns an array to the calling * function for storage into the respective database table. * * @param int $local_data_id The Cacti Local Data Id * @param string $rrdfile The rrdfile to process * @param string $interval The interval type to process * @param int $mode If we should be collecting legacy stats or not * @param bool $peak If the peak should be take from the max cf * @param array $rrd_process Pipes to the background RRDtool process * * @return array An array of AVERAGE, and MAX values in an RRDfile by Data Source name */ function dsstats_obtain_data_source_avgpeak_values(int $local_data_id, string $rrdfile, string $interval, int $mode, bool $peak, array $rrd_process) : array { global $user_time, $system_time, $real_time; $use_proxy = (read_config_option('storage_location') ? true : false); if ($use_proxy) { $file_exists = rrdtool_execute('file_exists ' . cacti_escapeshellarg($rrdfile), true, RRDTOOL_OUTPUT_BOOLEAN, $rrd_process, 'DSSTATS'); } else { clearstatcache(); $file_exists = file_exists($rrdfile); } $dsvalues = []; // don't attempt to get information if the file does not exist if ($file_exists) { $stats_command = db_fetch_cell_prepared('SELECT stats_command FROM data_source_stats_command_cache WHERE local_data_id = ?', [$local_data_id]); if ($stats_command == '') { $stats_command = dsstats_get_stats_command($local_data_id, $rrdfile, $use_proxy, $mode, $peak, $rrd_process); } if ($stats_command !== false) { // change the interval to something RRDtool understands switch($interval) { case 'daily': $interval = 'day'; break; case 'weekly': $interval = 'week'; break; case 'monthly': $interval = 'month'; break; case 'yearly': $interval = 'year'; break; } // now execute the graph command $stats_cmd = 'graph x --start now-1' . $interval . ' --end now ' . trim($stats_command); // print $stats_cmd . PHP_EOL . PHP_EOL; if ($use_proxy) { $xport_data = rrdtool_execute($stats_cmd, false, RRDTOOL_OUTPUT_STDOUT, $rrd_process, 'DSSTATS'); } else { $xport_data = dsstats_rrdtool_execute($stats_cmd, $rrd_process); } // process the xport array and return average and peak values if ($xport_data != '') { $xport_array = explode("\n", $xport_data); if (cacti_sizeof($xport_array)) { foreach ($xport_array as $index => $line) { $line = trim($line); if ($line == '' || $line == '0x0') { continue; } if ($index > 0) { // Catch the last line if (str_starts_with($line, 'OK')) { $line = trim($line, ' OK'); $parts = explode(' ', $line); // print $line . PHP_EOL; foreach ($parts as $line) { $sparts = explode(':', $line); switch($sparts[0]) { case 'u': $user_time = $sparts[1]; break; case 's': $system_time = $sparts[1]; break; case 'r': $real_time = $sparts[1]; break; } } break; } else { // Get the rrd_name and the remainder $parts = explode('-', $line); $rrd_name = $parts[0]; $namvalpt = $parts[1]; // Get the combined variable and value $parts = explode('=', $namvalpt); $variable = $parts[0]; $value = $parts[1]; // Get the metric and the mode $parts = explode('_', $variable); $metric = $parts[0]; $amode = $parts[1]; if (stripos($value, 'nan') === false) { $dsvalues[$amode][$rrd_name][$metric] = $value; } else { $dsvalues[$amode][$rrd_name][$metric] = 'NULL'; } } } } return $dsvalues; } } } } elseif (($interval == 'daily') || ($interval == 'day')) { // only alarm if performing the 'daily' averages cacti_log("WARNING: File does not exist! DS[$local_data_id], FILE[" . $rrdfile . ']', false, 'DSSTATS'); } return $dsvalues; } function dsstats_get_stats_command(int $local_data_id, string $rrdfile, bool $use_proxy, int $mode, bool $peak, array $rrd_process) : mixed { global $user_time, $system_time, $real_time; // high speed or snail speed if ($use_proxy) { $info = rrdtool_execute('info ' . cacti_escapeshellarg($rrdfile), false, RRDTOOL_OUTPUT_STDOUT, $rrd_process, 'DSSTATS'); } else { $info = dsstats_rrdtool_execute('info ' . cacti_escapeshellarg($rrdfile), $rrd_process); } /** * Get the current values of stats and peak settings to see if there is any change to purge the cache */ $temp_mode = read_config_option('dsstats_mode'); $temp_peak = read_config_option('dsstats_peak'); set_config_option('dsstats_temp_mode', $temp_mode); set_config_option('dsstats_temp_peak', $temp_peak); $length = strlen($info); $command = ''; // don't do anything if RRDfile did not return data if ($info != '') { $info_array = explode("\n", $info); $average = false; $max = false; $dsnames = []; /* figure out what is in this RRDfile. Assume CF Uniformity as Cacti does not allow async rrdfiles. * also verify the consolidation functions in the RRDfile for average and max calculations. */ if (cacti_sizeof($info_array)) { foreach ($info_array as $line) { if (str_contains($line, 'ds[')) { $parts = explode(']', $line); $parts2 = explode('[', $parts[0]); $dsnames[trim($parts2[1])] = 1; } elseif (str_contains($line, '.cf')) { $parts = explode('=', $line); if (str_contains($parts[1], 'AVERAGE')) { $average = true; } if (str_contains($parts[1], 'MAX')) { $max = true; } } elseif (str_contains($line, 'step')) { $parts = explode('=', $line); $poller_interval = trim($parts[1]); } } } // create the command syntax to get data // assume that an RRDfile has not more than 62 data sources $defs = 'abcdefghijklmnopqrstuvwxyz012345789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; $i = 0; $j = 0; $def = ''; $command = ''; $dsvalues = []; // escape the file name if on Windows if (CACTI_SERVER_OS != 'unix') { $rrdfile = str_replace(':', '\\:', $rrdfile); } // setup the graph command by parsing through the internal data source names if (cacti_sizeof($dsnames)) { foreach ($dsnames as $dsname => $present) { $mydata_avg = $defs[$j] . $defs[$i] . '_a'; $mydata_max = $defs[$j] . $defs[$i] . '_m'; if ($average) { $def .= 'DEF:' . $mydata_avg . '="' . $rrdfile . '":' . $dsname . ':AVERAGE '; $command .= " VDEF:{$mydata_avg}_aa=$mydata_avg,AVERAGE PRINT:{$mydata_avg}_aa:{$dsname}-avg_avg=%lf"; $command .= " VDEF:{$mydata_avg}_am=$mydata_avg,MAXIMUM PRINT:{$mydata_avg}_am:{$dsname}-peak_avg=%lf"; $i++; } if ($max && $peak) { $def .= 'DEF:' . $mydata_max . '="' . $rrdfile . '":' . $dsname . ':MAX '; $command .= " VDEF:{$mydata_max}_ma=$mydata_max,AVERAGE PRINT:{$mydata_max}_ma:{$dsname}-avg_max=%lf"; $command .= " VDEF:{$mydata_max}_mm=$mydata_max,MAXIMUM PRINT:{$mydata_max}_mm:{$dsname}-peak_max=%lf"; $i++; } if ($mode == 1) { $pt = ['95', '90', '75', '50', '25']; if ($average) { foreach ($pt as $s) { $command .= " VDEF:{$mydata_avg}_p{$s}n={$mydata_avg},$s,PERCENTNAN PRINT:{$mydata_avg}_p{$s}n:{$dsname}-p{$s}n_avg=%lf"; } // TOTAL $command .= " VDEF:{$mydata_avg}_sum=$mydata_avg,TOTAL PRINT:{$mydata_avg}_sum:{$dsname}-sum_avg=%lf"; // STDDEV $command .= " VDEF:{$mydata_avg}_stddev=$mydata_avg,STDEV PRINT:{$mydata_avg}_stddev:{$dsname}-stddev_avg=%lf"; // LSLSLOPE $command .= " VDEF:{$mydata_avg}_lslslope=$mydata_avg,LSLSLOPE PRINT:{$mydata_avg}_lslslope:{$dsname}-lslslope_avg=%lf"; // LSLINT $command .= " VDEF:{$mydata_avg}_lslint=$mydata_avg,LSLINT PRINT:{$mydata_avg}_lslint:{$dsname}-lslint_avg=%lf"; // LSLCORREL $command .= " VDEF:{$mydata_avg}_lslcorrel=$mydata_avg,LSLCORREL PRINT:{$mydata_avg}_lslcorrel:{$dsname}-lslcorrel_avg=%lf"; } if ($max && $peak) { foreach ($pt as $s) { $command .= " VDEF:{$mydata_max}_p{$s}n={$mydata_max},$s,PERCENTNAN PRINT:{$mydata_max}_p{$s}n:{$dsname}-p{$s}n_max=%lf"; } // TOTAL $command .= " VDEF:{$mydata_max}_sum=$mydata_max,TOTAL PRINT:{$mydata_max}_sum:{$dsname}-sum_max=%lf"; // STDDEV $command .= " VDEF:{$mydata_max}_stddev=$mydata_max,STDEV PRINT:{$mydata_max}_stddev:{$dsname}-stddev_max=%lf"; // LSLSLOPE $command .= " VDEF:{$mydata_max}_lslslope=$mydata_max,LSLSLOPE PRINT:{$mydata_max}_lslslope:{$dsname}-lslslope_max=%lf"; // LSLINT $command .= " VDEF:{$mydata_max}_lslint=$mydata_max,LSLINT PRINT:{$mydata_max}_lslint:{$dsname}-lslint_max=%lf"; // LSLCORREL $command .= " VDEF:{$mydata_max}_lslcorrel=$mydata_max,LSLCORREL PRINT:{$mydata_max}_lslcorrel:{$dsname}-lslcorrel_max=%lf"; } } if ($i > 50) { $j++; $i = 0; } } } $command = trim($def) . ' ' . $command; db_execute_prepared('REPLACE INTO data_source_stats_command_cache (local_data_id, stats_command) VALUES(?, ?)', [$local_data_id, $command]); return $command; } return false; } /** * dsstats_log_statistics - provides generic timing message to both the Cacti log and the settings * table so that the statistics can be graphed as well. * * @param string $type The type of statistics to log, either 'HOURLY', 'DAILY', 'BOOST' or 'MAJOR'. * * @return void */ function dsstats_log_statistics(string $type) : void { global $start; dsstats_debug($type); $sub_type = ''; if ($type == 'HOURLY') { $sub_type = ''; } elseif ($type == 'MAJOR') { $sub_type = 'dchild'; } elseif ($type == 'DAILY') { $sub_type = 'child'; } elseif ($type == 'BOOST') { $sub_type = 'bchild'; } // take time and log performance data $end = microtime(true); if ($sub_type != '') { $totals = dsstats_fetch_statistics_totals('%' . $sub_type . '%'); $rrd_user = $totals['rrd_user']; $rrd_system = $totals['rrd_system']; $rrd_real = $totals['rrd_real']; $rrd_files = $totals['rrd_files']; $dsses = $totals['dsses']; $processes = read_config_option('dsstats_parallel'); $cacti_stats = sprintf('Time:%01.2f Type:%s Threads:%s RRDfiles:%s DSSes:%s RRDUser:%01.2f RRDSystem:%01.2f RRDReal:%01.2f', $end - $start, $type, $processes, $rrd_files, $dsses, $rrd_user, $rrd_system, $rrd_real); db_execute("DELETE FROM settings WHERE name LIKE 'dsstats_rrd_%$sub_type%' OR name LIKE 'dsstats_total_rrds%_$sub_type%' OR name LIKE 'dsstats_total_dsses%_$sub_type%'"); } else { $cacti_stats = sprintf('Time:%01.2f Type:%s', $end - $start, $type); } // take time and log performance data $start = microtime(true); // log to the database set_config_option('stats_dsstats_' . $type, $cacti_stats); // log to the logfile cacti_log('DSSTATS STATS: ' . $cacti_stats , true, 'SYSTEM'); } /** * dsstats_log_child_stats - logs dsstats child process information * * @param string $type The type of child, MAJOR, DAILY, BOOST * @param int $thread_id The parallel thread id * @param float $total_time The total time to collect date * * @return void */ function dsstats_log_child_stats(string $type, int $thread_id, float $total_time) : void { $totals = dsstats_fetch_statistics_totals('%' . $type . '_' . $thread_id . '%'); $rrd_user = $totals['rrd_user']; $rrd_system = $totals['rrd_system']; $rrd_real = $totals['rrd_real']; $rrd_files = $totals['rrd_files']; $dsses = $totals['dsses']; $cacti_stats = sprintf('Time:%01.2f Type:%s ProcessNumber:%s RRDfiles:%s DSSes:%s RRDUser:%01.2f RRDSystem:%01.2f RRDReal:%01.2f', $total_time, cacti_strtoupper($type), $thread_id, $rrd_files, $dsses, $rrd_user, $rrd_system, $rrd_real); cacti_log('DSSTATS CHILD STATS: ' . $cacti_stats, true, 'SYSTEM'); } /** * Fetch all DSStats timing and count totals in one database round trip. * * @param string $suffix_pattern SQL LIKE pattern appended to each metric prefix. * * @return array{rrd_user: float, rrd_system: float, rrd_real: float, rrd_files: float, dsses: float} */ function dsstats_fetch_statistics_totals(string $suffix_pattern) : array { $patterns = [ 'dsstats_rrd_user_' . $suffix_pattern, 'dsstats_rrd_system_' . $suffix_pattern, 'dsstats_rrd_real_' . $suffix_pattern, 'dsstats_total_rrds_' . $suffix_pattern, 'dsstats_total_dsses_' . $suffix_pattern, ]; $totals = db_fetch_row_prepared('SELECT COALESCE(SUM(CASE WHEN name LIKE ? THEN value END), 0) AS rrd_user, COALESCE(SUM(CASE WHEN name LIKE ? THEN value END), 0) AS rrd_system, COALESCE(SUM(CASE WHEN name LIKE ? THEN value END), 0) AS rrd_real, COALESCE(SUM(CASE WHEN name LIKE ? THEN value END), 0) AS rrd_files, COALESCE(SUM(CASE WHEN name LIKE ? THEN value END), 0) AS dsses FROM settings WHERE name LIKE \'dsstats_%\'', $patterns); return [ 'rrd_user' => (float) ($totals['rrd_user'] ?? 0), 'rrd_system' => (float) ($totals['rrd_system'] ?? 0), 'rrd_real' => (float) ($totals['rrd_real'] ?? 0), 'rrd_files' => (float) ($totals['rrd_files'] ?? 0), 'dsses' => (float) ($totals['dsses'] ?? 0), ]; } /** * dsstats_error_handler - this routine logs all PHP error transactions * to make sure they are properly logged. * * @param int $errno The errornum reported by the system * @param string $errmsg The error message provides by the error * @param string $filename The filename that encountered the error * @param int $linenum The line number where the error occurred * @param array $vars The current state of PHP variables. * * @return bool always returns true for some reason */ function dsstats_error_handler(int $errno, string $errmsg, string $filename, int $linenum, array $vars = []) : bool { if (read_config_option('log_verbosity') >= POLLER_VERBOSITY_DEBUG) { // define all error types $errortype = [ E_ERROR => 'Error', E_WARNING => 'Warning', E_PARSE => 'Parsing Error', E_NOTICE => 'Notice', E_CORE_ERROR => 'Core Error', E_CORE_WARNING => 'Core Warning', E_COMPILE_ERROR => 'Compile Error', E_COMPILE_WARNING => 'Compile Warning', E_USER_ERROR => 'User Error', E_USER_WARNING => 'User Warning', E_USER_NOTICE => 'User Notice', E_RECOVERABLE_ERROR => 'Catchable Fatal Error' ]; // create an error string for the log $err = "ERRNO:'" . $errno . "' TYPE:'" . $errortype[$errno] . "' MESSAGE:'" . $errmsg . "' IN FILE:'" . $filename . "' LINE NO:'" . $linenum . "'"; // let's ignore some lesser issues if (str_contains($errmsg, 'date_default_timezone')) { return true; } if (str_contains($errmsg, 'Only variables')) { return true; } // log the error to the Cacti log cacti_log('PROGERR: ' . $err, false, 'DSSTATS'); return false; } return true; } /** * dsstats_poller_output - this routine runs in parallel with the cacti poller and * populates the last and cache tables. On larger systems, it should be noted that * the memory overhead for the global arrays, $ds_types, $ds_last, $ds_multi * could be serval hundred megabytes. So, this should be kept in mind when running the * sizing your system. * * The routine basically loads those 4 structures into memory, and then uses them to * determine what should be stored in both the Cache and the Last tables. The 4 structures * contain the following information: * * $ds_types - The type of data source, keyed by the local_data_id and the rrd_name stored inside * of the RRDfile. * $ds_last - For the COUNTER, and DERIVE DS types, the last measured and stored value. * $ds_multi - For Multi Part responses, stores the mapping of the Data Input Fields to the * Internal RRDfile DS names. * * The routine loops through all poller output items and makes decisions relative to the output * that should be stored into the two tables, and then bulk inserts that information once * all poller items have been processed. * * The purpose for loading then entire structures into memory at one time is to reduce the latency * related to multiple database calls. The author believed that PHP's array hashing algorithms * would be as fast, if not faster, than MySQL, when considering the transaction overhead and therefore * chose this method. * * @param mixed $rrd_update_array The output from the poller output table to be processed by dsstats * * @return void */ function dsstats_poller_output(mixed &$rrd_update_array) : void { static $ds_types = []; static $ds_multi = []; // suppress warnings if (defined('E_DEPRECATED')) { error_reporting(E_ALL ^ E_DEPRECATED); } else { error_reporting(E_ALL); } // install the dsstats error handler set_error_handler('dsstats_error_handler'); // do not make any calculations unless enabled if (read_config_option('dsstats_enable') == 'on') { if (cacti_sizeof($rrd_update_array) > 0) { $local_data_ids = []; foreach ($rrd_update_array as $data_source) { $local_data_id = (int) ($data_source['local_data_id'] ?? 0); if ($local_data_id > 0) { $local_data_ids[$local_data_id] = $local_data_id; } } $missing_data_ids = array_values(array_diff($local_data_ids, array_keys($ds_types))); if ($missing_data_ids !== []) { $placeholders = implode(',', array_fill(0, count($missing_data_ids), '?')); $metadata = db_fetch_assoc_prepared("SELECT dtr.local_data_id, dtr.data_source_name, dtr.data_source_type_id, dtd.rrd_step, dtr.rrd_maximum FROM data_template_rrd AS dtr INNER JOIN data_template_data AS dtd ON dtd.local_data_id = dtr.local_data_id WHERE dtr.local_data_id IN ($placeholders)", $missing_data_ids); foreach ($missing_data_ids as $local_data_id) { $ds_types[$local_data_id] = []; } foreach ($metadata as $row) { $ds_types[(int) $row['local_data_id']][$row['data_source_name']] = [ 'data_source_type_id' => $row['data_source_type_id'], 'rrd_step' => $row['rrd_step'], 'rrd_maximum' => $row['rrd_maximum'], ]; } } $last_values = []; if ($local_data_ids !== []) { $placeholders = implode(',', array_fill(0, count($local_data_ids), '?')); $last_rows = db_fetch_assoc_prepared("SELECT local_data_id, rrd_name, `value` FROM data_source_stats_hourly_last WHERE local_data_id IN ($placeholders)", array_values($local_data_ids)); foreach ($last_rows as $row) { $last_values[(int) $row['local_data_id']][$row['rrd_name']] = $row['value']; } } // we will assume a smaller than the max packet size. This would appear to be around the sweat spot. $max_packet = '264000'; // initialize some variables related to the DB inserts $outbuf = ''; $sql_cache_prefix = 'INSERT INTO data_source_stats_hourly_cache (local_data_id, rrd_name, time, `value`) VALUES'; $sql_last_prefix = 'INSERT INTO data_source_stats_hourly_last (local_data_id, rrd_name, `value`, calculated) VALUES'; $sql_suffix = ' ON DUPLICATE KEY UPDATE `value`=VALUES(`value`)'; $sql_last_suffix = ' ON DUPLICATE KEY UPDATE `value`=VALUES(`value`), `calculated`=VALUES(`calculated`)'; $overhead = strlen($sql_cache_prefix) + strlen($sql_suffix); $overhead_last = strlen($sql_last_prefix) + strlen($sql_last_suffix); /* make the association between the multi-part name value pairs and the RRDfile internal * data source names. */ if (!cacti_sizeof($ds_multi)) { $ds_multi = array_rekey( db_fetch_assoc('SELECT DISTINCT data_name, data_source_name FROM graph_templates_item AS gti INNER JOIN data_template_rrd AS dtr ON gti.task_item_id = dtr.id INNER JOIN data_input_fields AS dif ON dif.id = dtr.data_input_field_id WHERE dtr.data_input_field_id != 0'), 'data_name', 'data_source_name' ); } // required for updating tables $cache_i = 1; $last_i = 1; $out_length = 0; $last_length = 0; $lastbuf = ''; $cachebuf = ''; // process each array $n = 1; foreach ($rrd_update_array as $data_source) { if (isset($data_source['times'])) { foreach ($data_source['times'] as $time => $sample) { foreach ($sample as $ds => $value) { $result['local_data_id'] = $data_source['local_data_id']; $result['rrd_name'] = $ds; $result['time'] = date('Y-m-d H:i:s', $time); $currentval = ''; if (is_numeric($value)) { $result['output'] = $value; } elseif ($value == 'U' || cacti_strtolower($value) == 'nan') { $result['output'] = 'NULL'; } else { $result['output'] = 'NULL'; cacti_log('ERROR: Output from local_data_id ' . $data_source['local_data_id'] . ", for RRDfile DS Name '$ds', is invalid. " . "It outputs was : '" . $value . "'. " . 'Please check your script or data input method for errors.'); } $lastval = ''; $ds_metadata = $ds_types[(int) $result['local_data_id']][$result['rrd_name']] ?? []; $polling_interval = $ds_metadata['rrd_step'] ?? read_config_option('poller_interval'); $ds_type = $ds_metadata['data_source_type_id'] ?? 0; switch ($ds_type) { case 2: // COUNTER case 6: // DCOUNTER // get the last values from the database for COUNTER and DERIVE data sources $ds_last = $last_values[(int) $result['local_data_id']][$result['rrd_name']] ?? false; if ($ds_last == '' || $ds_last == 'NULL') { $currentval = 'NULL'; } elseif ($result['output'] == 'NULL') { $currentval = 'NULL'; } elseif ($result['output'] >= $ds_last) { // everything is normal $currentval = $result['output'] - $ds_last; } else { $max_value = $ds_metadata['rrd_maximum'] ?? 'U'; // possible overflow, see if its 32bit or 64bit if ($ds_last > 4294967295) { $currentval = (18446744073709551615 - $ds_last) + $result['output']; } else { $currentval = (4294967295 - $ds_last) + $result['output']; } if ($max_value != 'U' && $currentval > $max_value) { $currentval = 'NULL'; } } if ($currentval != 'NULL') { $currentval /= $polling_interval; if ($ds_type == 6) { $currentval = round($currentval, 0); } } $lastval = $result['output']; if ($ds_type == 6 && $lastval != 'NULL') { $lastval = round($lastval, 0); } break; case 3: // DERIVE case 7: // DDERIVE // get the last values from the database for COUNTER and DERIVE data sources $ds_last = $last_values[(int) $result['local_data_id']][$result['rrd_name']] ?? false; if ($ds_last == '') { $currentval = 'NULL'; } elseif ($result['output'] != 'NULL') { $currentval = ($result['output'] - $ds_last) / $polling_interval; if ($ds_type == 7) { $currentval = round($currentval, 0); } } else { $currentval = 'NULL'; } $lastval = $result['output']; if ($ds_type == 7 && $lastval != 'NULL') { $lastval = round($lastval, 0); } break; case 4: // ABSOLUTE if (!dsstats_is_unknown_data($result['output'])) { $currentval = abs($result['output']); $lastval = $currentval; } else { $currentval = 'NULL'; $lastval = $currentval; } break; case 1: // GAUGE if (!dsstats_is_unknown_data($result['output'])) { $currentval = $result['output']; $lastval = $result['output']; } else { $currentval = 'NULL'; $lastval = $currentval; } break; default: cacti_log("WARNING: Unknown RRDtool Data Type '$ds_type', For '" . $result['rrd_name'] . "'", false, 'DSSTATS'); break; } // when doing bulk inserts, the second record is different if ($cache_i == 1) { $cache_delim = ' '; } else { $cache_delim = ', '; } if ($last_i == 1) { $last_delim = ' '; } else { $last_delim = ', '; } if ($currentval == '' || $currentval == '-') { $currentval = 'NULL'; } // setup the output buffer for the cache first $cachebuf .= $cache_delim . '(' . (int) $result['local_data_id'] . ', ' . db_qstr($result['rrd_name']) . ', ' . db_qstr($result['time']) . ', ' . $currentval . ')'; $out_length = strlen($cachebuf); // now do the last value, if applicable if ($lastval != '') { $last_values[(int) $result['local_data_id']][$result['rrd_name']] = $lastval; $lastbuf .= $last_delim . '(' . (int) $result['local_data_id'] . ', ' . db_qstr($result['rrd_name']) . ', ' . $lastval . ', ' . $currentval . ')'; $last_i++; $last_length = strlen($lastbuf); } // if we exceed our output buffer, it's time to write if ((($out_length + $overhead) > $max_packet) || (($last_length + $overhead_last) > $max_packet)) { db_execute($sql_cache_prefix . $cachebuf . $sql_suffix); if ($last_i > 1) { db_execute($sql_last_prefix . $lastbuf . $sql_last_suffix); } $cachebuf = ''; $lastbuf = ''; $out_length = 0; $last_length = 0; $cache_i = 1; $last_i = 1; } else { $cache_i++; } $n++; if (($n % 1000) == 0) { print '.'; } } } } } if ($cache_i > 1) { db_execute($sql_cache_prefix . $cachebuf . $sql_suffix); } if ($last_i > 1) { db_execute($sql_last_prefix . $lastbuf . $sql_last_suffix); } } } // restore original error handler restore_error_handler(); } /** * checks the output from the rrdfile column and returns false if it's unknown data * * @param string $value - The rrdtool value data * * @return bool */ function dsstats_is_unknown_data(string $value) : bool { if ($value != 'NULL' && $value != 'U' && cacti_strtolower($value) != 'nan') { return false; } else { return true; } } /** * dsstats_boost_bottom - this routine accommodates mass updates after the boost process * has completed. The use of boost will require boost version 2.5 or above. The idea * if that daily averages will be updated on the boost cycle. * * @return void */ function dsstats_boost_bottom() : void { global $total_user, $total_system, $total_real, $total_dsses; $total_user = 0; $total_system = 0; $total_real = 0; $total_dsses = 0; if (read_config_option('dsstats_enable') == 'on') { include_once(CACTI_PATH_LIBRARY . '/rrd.php'); // run the daily stats. log to database to prevent secondary runs set_config_option('dsstats_last_daily_run_time', date('Y-m-d G:i:s', time())); // run the daily stats $expected_children = dsstats_launch_children('bmaster'); dsstats_wait_for_children('bmaster', $expected_children); dsstats_get_and_store_ds_avgpeak_values('daily', 'child', 1); dsstats_log_statistics('DAILY'); } } /** * dsstats_wait_for_children - waits for launched dsstats children of the given * type to finish. exec_background() is non-blocking, so a bare * `while (dsstats_processes_running($type))` can observe zero registered * children before any of them have started, mistaking "not started yet" for * "already done" and returning immediately -- the same startup race Boost's * boost_all_children_registered() barrier was added to close (see * poller_boost.php). This applies the same shape: wait for the running count * to reach the launched count before trusting it can fall back to zero. * * @param string $type The process type (e.g. 'bmaster') * @param int $expected_children Number of children dsstats_launch_children() launched * * @return void */ function dsstats_wait_for_children(string $type, int $expected_children) : void { if ($expected_children <= 0) { return; } $startup_deadline = time() + 30; while (dsstats_processes_running($type) < $expected_children && time() < $startup_deadline) { sleep(1); } if (dsstats_processes_running($type) < $expected_children) { cacti_log(sprintf('WARNING: DSStats startup barrier timed out; %d of %d %s children registered before draining.', dsstats_processes_running($type), $expected_children, $type), false, 'DSSTATS'); } // Wait for all processes to continue while ($running = dsstats_processes_running($type)) { dsstats_debug(sprintf('%s Processes Running, Sleeping for 2 seconds.', $running)); sleep(2); } } /** * dsstats_memory_limit - this routine increases/decreases the memory available for the script * It is divided into two functions as the main dsstats poller calls this function directly * as opposed to the call during the processing of poller output in the main cacti poller. * * @return void */ function dsstats_memory_limit() : void { ini_set('memory_limit', read_config_option('dsstats_poller_mem_limit') . 'M'); } /** * dsstats_poller_bottom - This routine launches the main dsstats poller so that it might * calculate the Hourly, Daily, Weekly, Monthly, and Yearly averages. It is forked independently * to the Cacti poller after all polling has finished. * * @return void */ function dsstats_poller_bottom() : void { if (read_config_option('dsstats_enable') == 'on') { include_once(CACTI_PATH_LIBRARY . '/poller.php'); chdir(CACTI_PATH_BASE); $command_string = read_config_option('path_php_binary'); if (read_config_option('path_dsstats_log') != '') { if (CACTI_SERVER_OS == 'unix') { $extra_args = '-q ' . CACTI_PATH_BASE . '/poller_dsstats.php >> ' . cacti_escapeshellarg(read_config_option('path_dsstats_log')) . ' 2>&1'; } else { $extra_args = '-q ' . CACTI_PATH_BASE . '/poller_dsstats.php >> ' . cacti_escapeshellarg(read_config_option('path_dsstats_log')); } } else { $extra_args = '-q ' . CACTI_PATH_BASE . '/poller_dsstats.php'; } exec_background($command_string, $extra_args); } } /** * dsstats_rrdtool_init - This routine provides a bi-directional socket based connection to RRDtool. * it provides a high speed connection to rrdfile in the case where the traditional Cacti call does * not when performing fetch type calls. * * @return array An array that includes both the process resource and the pipes to communicate * with RRDtool. */ function dsstats_rrdtool_init() : array { if (CACTI_SERVER_OS == 'unix') { $fds = [ 0 => ['pipe', 'r'], // stdin 1 => ['pipe', 'w'], // stdout 2 => ['file', '/dev/null', 'a'] // stderr ]; } else { $fds = [ 0 => ['pipe', 'r'], // stdin 1 => ['pipe', 'w'], // stdout 2 => ['file', 'nul', 'a'] // stderr ]; } // set the rrdtool default font if (read_config_option('path_rrdtool_default_font')) { putenv('RRD_DEFAULT_FONT=' . read_config_option('path_rrdtool_default_font')); } $command = read_config_option('path_rrdtool') . ' - '; $process = proc_open($command, $fds, $pipes); // proc_open returns false if RRDtool could not be spawned; without a process // the pipes are not populated, so bail out before touching them if (!is_resource($process)) { cacti_log('ERROR: Unable to start RRDtool process for DSStats', false, 'DSSTATS'); return [false, false]; } // make stdin/stdout/stderr non-blocking stream_set_blocking($pipes[0], false); stream_set_blocking($pipes[1], false); return [$process, $pipes]; } /** * dsstats_rrdtool_execute - this routine passes commands to RRDtool and returns the information * back to DSStats. It is important to note here that RRDtool needs to provide an either 'OK' * or 'ERROR' response across the pipe as it does not provide EOF characters to key upon. * * This may not be the best method and may be changed after I have a conversation with a few * developers. * * @param string $command The rrdtool command to execute * @param array $rrd_process An array of stdin and stdout pipes to read and write data from * * @return string The output from RRDtool */ function dsstats_rrdtool_execute(string $command, array $rrd_process) : string { static $broken = false; $use_proxy = (intval(read_config_option('storage_location')) > 0 ? true : false); if ($use_proxy) { $stdout = rrdtool_execute($command, false, RRDTOOL_OUTPUT_STDOUT, $rrd_process, 'DSSTATS'); } else { $stdout = ''; if ($command == '') { return ''; } $pipes = $rrd_process[1]; $command .= "\r\n"; $return_code = fwrite($pipes[0], $command); if (is_resource($pipes[1])) { while (!feof($pipes[1])) { $stdout .= fgets($pipes[1], 4096); if (str_contains($stdout, 'OK')) { break; } if (str_contains($stdout, 'ERROR')) { break; } } } elseif (!$broken) { cacti_log('ERROR: RRDtool was unable to fork. Likely RRDtool can not be found or system out of resources. Blocking subsequent messages.', false, 'POLLER'); $broken = true; } } return $stdout; } /** * dsstats_rrdtool_close - this routine closes the RRDtool process thus also * closing the pipes. * * @param array $rrd_process * * @return void */ function dsstats_rrdtool_close(array $rrd_process) : void { proc_terminate($rrd_process[0]); proc_close($rrd_process[0]); } /** * dsstats_launch_children - this function will launch collector children based upon * the maximum number of threads and the process type * * @param string $type The process type * * @return int The number of children launched */ function dsstats_launch_children(string $type) : int { global $debug; $processes = read_config_option('dsstats_parallel'); if (empty($processes)) { $processes = 1; } $php_binary = read_config_option('path_php_binary'); dsstats_debug("About to launch $processes processes."); $sub_type = dsstats_get_subtype($type); for ($i = 1; $i <= $processes; $i++) { dsstats_debug(sprintf('Launching DSStats Process Number %s for Type %s', $i, $type)); cacti_log(sprintf('NOTE: Launching DSStats Process Number %s for Type %s', $i, $type), false, 'BOOST', POLLER_VERBOSITY_MEDIUM); exec_background($php_binary, CACTI_PATH_BASE . "/poller_dsstats.php --type=$sub_type --child=$i" . ($debug ? ' --debug' : '')); } sleep(2); return (int) $processes; } /** * dsstats_get_subtype - this function determine the applicable * sub-type (child name) and return if based upon a type * * @param string $type The process type * * @return string - The sub type */ function dsstats_get_subtype(string $type) : string { switch($type) { case 'master': case 'pmaster': return 'child'; case 'bmaster': return 'bchild'; case 'dmaster': return 'dchild'; } return 'false'; } /** * dsstats_kill_running_processes - this function is part of an interrupt * handler to kill children processes when the parent is killed * * @return void */ function dsstats_kill_running_processes() : void { global $type; // This function has always used unregister_process() from poller.php. Load // that dependency explicitly so the newer identity check is available too, // regardless of which entry point loaded dsstats.php. require_once CACTI_PATH_LIBRARY . '/poller.php'; if ($type == 'bmaster') { $processes = db_fetch_assoc_prepared('SELECT * FROM processes WHERE tasktype = "dsstats" AND taskname = "bchild" AND pid != ?', [getmypid()]); } else { $processes = db_fetch_assoc_prepared('SELECT * FROM processes WHERE tasktype = "dsstats" AND taskname IN ("child", "dchild") AND pid != ?', [getmypid()]); } if (cacti_sizeof($processes)) { foreach ($processes as $p) { if (cacti_process_still_running((int) $p['pid'])) { cacti_log(sprintf('WARNING: Killing DSStats %s PID %d due to signal or overrun.', ucfirst($p['taskname']), $p['pid']), false, 'BOOST'); cacti_process_kill((int) $p['pid'], SIGTERM, 'BOOST'); } unregister_process($p['tasktype'], $p['taskname'], $p['taskid'], $p['pid']); } } } /** * dsstats_processes_running - given a type, determine the number * of sub-type or children that are currently running * * @param string $type The process type * * @return int The number of running processes */ function dsstats_processes_running(string $type) : int { $sub_type = dsstats_get_subtype($type); $running = db_fetch_cell_prepared('SELECT COUNT(*) FROM processes WHERE tasktype = "dsstats" AND taskname = ?', [$sub_type]); if ($running == 0) { return 0; } return $running; } /** * dsstats_get_best_partition - given a time range, determine the * best partition to use to gather graph statistics * * @param int $start_time As a unix timestamp * @param int $end_time As a unix timestamp * * @return string The table name of the best partition */ function dsstats_get_best_partition(int $start_time, int $end_time = 0) : string { if ($end_time == 0) { $end_time = time(); } $cur_time = time(); $days = ceil(($end_time - $start_time) / 86400); if ($days <= 1) { // Daily partition $desired_suffix = '_v' . date('Y', $start_time) . substr('000' . date('z'), -3); $current_suffix = '_v' . date('Y', $cur_time) . substr('000' . date('z', $cur_time), -3); if ($desired_suffix == $current_suffix) { return 'data_source_stats_daily'; } if (db_table_exists('data_source_stats_daily' . $desired_suffix)) { return 'data_source_stats_daily' . $desired_suffix; } else { return 'data_source_stats_daily'; } } elseif ($days > 1 && $days <= 7) { // Weekly partition $desired_suffix = '_v' . date('Y', $start_time) . substr('000' . date('W'), -3); $current_suffix = '_v' . date('Y', $cur_time) . substr('000' . date('W', $cur_time), -3); if ($desired_suffix == $current_suffix) { return 'data_source_stats_weekly'; } if (db_table_exists('data_source_stats_weekly' . $desired_suffix)) { return 'data_source_stats_weekly' . $desired_suffix; } else { return 'data_source_stats_weekly'; } } elseif ($days > 7 && $days <= 30) { // Monthly partition $desired_suffix = '_v' . date('Y', $start_time) . substr('000' . date('m'), -3); $current_suffix = '_v' . date('Y', $cur_time) . substr('000' . date('m', $cur_time), -3); if ($desired_suffix == $current_suffix) { return 'data_source_stats_monthly'; } if (db_table_exists('data_source_stats_monthly' . $desired_suffix)) { return 'data_source_stats_monthly' . $desired_suffix; } else { return 'data_source_stats_monthly'; } } else { // Yearly partition $desired_suffix = '_v' . date('Y', $start_time); $current_suffix = '_v' . date('Y', $cur_time); if ($desired_suffix == $current_suffix) { return 'data_source_stats_yearly'; } if (db_table_exists('data_source_stats_yearly' . $desired_suffix)) { return 'data_source_stats_yearly' . $desired_suffix; } else { return 'data_source_stats_yearly'; } } }