$field) { if (is_string($field)) { $tmp = []; foreach ($data as $key => $row) { $tmp[$key] = $row[$field]; } $args[$n] = $tmp; } } $args[] = &$data; call_user_func_array('array_multisort', $args); return array_pop($args); } /** * Converts a file size in bytes to a human-readable format. * * This function takes a file size in bytes and converts it to a more * readable format, such as Bytes, KBytes, MBytes, or GBytes, depending * on the size. The output is localized using the `__` and `number_format_i18n` * functions for internationalization support. * * @param float|int $file_size The file size in bytes. * @param int $digits The number of decimal places to include in the formatted output. * * @return string A human-readable string representing the file size in the appropriate unit. */ function boost_file_size_display(float|int $file_size, int $digits = 2) : string { if ($file_size > 1024) { $file_size /= 1024; if ($file_size > 1024) { $file_size /= 1024; if ($file_size > 1024) { $file_size /= 1024; return __('%s GBytes', number_format_i18n($file_size, $digits)); } else { return __('%s MBytes', number_format_i18n($file_size, $digits)); } } else { return __('%s KBytes', number_format_i18n($file_size, $digits)); } } else { return __('%s Bytes', number_format_i18n($file_size, $digits)); } } /** * Retrieves the total number of rows from the database tables * that match specific naming patterns. * * This function calculates the sum of rows from tables in the * current database schema where the table names match either * 'poller_output_boost_arch_%' or 'poller_output_boost'. * * @return int The total number of rows from the matching tables. */ function boost_get_total_rows() : int { return (int) db_fetch_cell("SELECT SUM(TABLE_ROWS) FROM information_schema.TABLES WHERE TABLE_SCHEMA = SCHEMA() AND (TABLE_NAME LIKE 'poller_output_boost_arch_%' OR TABLE_NAME = 'poller_output_boost')"); } /** * Custom error handler for the application. * * This function handles errors based on the application's logging verbosity * level. It logs detailed error information to the Cacti log if the verbosity * level is set to debug. Certain non-critical errors are ignored to reduce * noise in the logs. * * @param int $errno The level of the error raised. * @param string $errmsg The error message. * @param string $filename The filename where the error was raised. * @param int $linenum The line number where the error was raised. * @param array $vars An array of variables that existed in the * scope the error was triggered in. * * @return bool */ function boost_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', E_DEPRECATED => 'Deprecated Warning' ]; // 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 (substr_count($errmsg, 'date_default_timezone')) { return true; } if (substr_count($errmsg, 'Only variables')) { return true; } // log the error to the Cacti log cacti_log('PROGERR: ' . $err, false, 'BOOST'); } return true; } /** * Checks and ensures that the Boost RRD update system is correctly enabled. * * This function verifies if either the `boost_rrd_update_enable` or * `boost_rrd_update_system_enable` configuration options are set to 'on'. * If `boost_rrd_update_enable` is enabled but `boost_rrd_update_system_enable` * is not, it updates the database to enable the system-level updates. * * If neither option is enabled, the function returns false. Error-handler * ownership remains with the caller that installed it. * * @return bool Returns true if the Boost RRD update system is correctly enabled, * otherwise returns false. */ function boost_check_correct_enabled() : bool { if ((read_config_option('boost_rrd_update_enable') == 'on') || (read_config_option('boost_rrd_update_system_enable') == 'on')) { // turn on the system level updates as that is what dictates "off" if (read_config_option('boost_rrd_update_system_enable') != 'on') { db_execute("REPLACE INTO settings (name,value) VALUES ('boost_rrd_update_system_enable','on')"); } } else { return false; } return true; } /** * Read one SQL VALUES token: a quoted string, a bare number, or FUNC(). * * @param string $sql Tuple text * @param int $offset Byte offset; advanced past the token * * @return string|null The token value, or null when the tuple is malformed */ function boost_parse_sql_value(string $sql, int &$offset) : ?string { $length = strlen($sql); while ($offset < $length && ctype_space($sql[$offset])) { $offset++; } if ($offset >= $length) { return null; } if ($sql[$offset] === "'") { $offset++; $value = ''; while ($offset < $length) { $ch = $sql[$offset]; if ($ch === '\\' && $offset + 1 < $length) { $value .= $sql[$offset + 1]; $offset += 2; continue; } if ($ch === "'") { if ($offset + 1 < $length && $sql[$offset + 1] === "'") { $value .= "'"; $offset += 2; continue; } $offset++; return $value; } $value .= $ch; $offset++; } return null; } if (preg_match('/\G[A-Za-z_][A-Za-z0-9_]*\s*\(\s*\)/', $sql, $matches, 0, $offset) === 1) { $offset += strlen($matches[0]); return preg_replace('/\s+/', '', $matches[0]) ?? $matches[0]; } if (preg_match('/\G-?\d+(?:\.\d+)?/', $sql, $matches, 0, $offset) === 1) { $offset += strlen($matches[0]); return $matches[0]; } return null; } /** * Pull local_data_id, rrd_name, and time from a poller_output_boost VALUES tuple. * * @param string $tuple A "(id, 'name', 'time', 'output')" or CURRENT_TIMESTAMP() tuple * * @return array{local_data_id:int, rrd_name:string, time:string}|null */ function boost_parse_output_tuple(string $tuple) : ?array { $tuple = trim($tuple); $offset = 0; if ($tuple === '' || $tuple[0] !== '(') { return null; } $offset = 1; $id = boost_parse_sql_value($tuple, $offset); if ($id === null || !ctype_digit($id)) { return null; } while ($offset < strlen($tuple) && ctype_space($tuple[$offset])) { $offset++; } if ($offset >= strlen($tuple) || $tuple[$offset] !== ',') { return null; } $offset++; $rrd_name = boost_parse_sql_value($tuple, $offset); if ($rrd_name === null) { return null; } while ($offset < strlen($tuple) && ctype_space($tuple[$offset])) { $offset++; } if ($offset >= strlen($tuple) || $tuple[$offset] !== ',') { return null; } $offset++; $time = boost_parse_sql_value($tuple, $offset); if ($time === null) { return null; } return [ 'local_data_id' => (int) $id, 'rrd_name' => $rrd_name, 'time' => $time ]; } /** * Clip a diagnostic fragment so one huge rrd_name cannot flood the log. */ function boost_clip_log_fragment(string $value, int $max = 64) : string { $value = str_replace(["\r", "\n", "\0"], ' ', $value); if (strlen($value) <= $max) { return $value; } return substr($value, 0, $max - 3) . '...'; } /** * Build the BOOST warning for an INSERT IGNORE that dropped rows. * * @param array $tuples The VALUES tuples from the statement that was ignored in part * @param int $attempted Rows in that statement * @param int $inserted Rows the server reported as written * * @return string Empty when nothing was dropped */ function boost_describe_ignored_sample_keys(array $tuples, int $attempted, int $inserted) : string { $dropped = $attempted - $inserted; if ($dropped < 1) { return ''; } $by_key = []; $ids = []; foreach ($tuples as $tuple) { $parsed = boost_parse_output_tuple((string) $tuple); if ($parsed === null) { continue; } $ids[$parsed['local_data_id']] = true; $key = $parsed['local_data_id'] . "\0" . $parsed['rrd_name'] . "\0" . $parsed['time']; if (!isset($by_key[$key])) { $parsed['count'] = 0; $by_key[$key] = $parsed; } $by_key[$key]['count']++; } $in_batch = 0; $examples = []; foreach ($by_key as $row) { if ($row['count'] < 2) { continue; } $in_batch += $row['count'] - 1; if (cacti_count($examples) < 8) { $examples[] = sprintf( 'DS[%d] %s @ %s (x%d)', $row['local_data_id'], boost_clip_log_fragment($row['rrd_name']), boost_clip_log_fragment($row['time']), $row['count'] ); } } if ($in_batch === 0) { foreach ($by_key as $row) { if (cacti_count($examples) >= 8) { break; } $examples[] = sprintf( 'DS[%d] %s @ %s', $row['local_data_id'], boost_clip_log_fragment($row['rrd_name']), boost_clip_log_fragment($row['time']) ); } } $id_keys = array_keys($ids); $id_list = implode(',', array_slice($id_keys, 0, 20)); if (cacti_count($id_keys) > 20) { $id_list .= ',...'; } $vs_existing = $dropped - $in_batch; if ($vs_existing < 0) { $vs_existing = 0; } if ($in_batch > 0 && $vs_existing > 0) { $reason = 'duplicate keys inside this INSERT and vs existing poller_output_boost rows'; } elseif ($in_batch > 0) { $reason = 'duplicate keys inside this INSERT'; } else { $reason = 'keys already in poller_output_boost'; } $poller = defined('POLLER_ID') ? (int) POLLER_ID : 0; return sprintf( 'WARNING: Boost staging ignored %d of %d duplicate sample keys (INSERT IGNORE, first write wins, poller=%d, %s). local_data_id=%s%s', $dropped, $attempted, $poller, $reason, $id_list !== '' ? $id_list : 'unknown', $examples !== [] ? '; ' . implode('; ', $examples) : '' ); } /** * Log INSERT IGNORE collisions with the keys an operator can grep. */ function boost_log_ignored_sample_keys(array $tuples, int $attempted, int $inserted) : void { $message = boost_describe_ignored_sample_keys($tuples, $attempted, $inserted); if ($message !== '') { cacti_log($message, false, 'BOOST'); } } /** * Log a poller_output_boost batch the database did not acknowledge, so a * failed write (including cmd.php's boost_redirect staging, which has no * other failure path) is never silent. */ function boost_log_failed_staging(int $attempted) : void { cacti_log(sprintf('ERROR: Boost staging failed to write %d sample(s) to poller_output_boost; the database did not acknowledge the batch.', $attempted), false, 'BOOST'); } /** * boost_flush_output_batch - writes a batch of pre-built poller_output_boost * VALUE tuples, chunking on max_allowed_packet so a single INSERT statement * never exceeds it. Shared by cmd.php's boost_redirect writes and * boost_poller_on_demand() so the batched-insert logic and the duplicate-key * handling live in exactly one place. * * Duplicate (local_data_id, rrd_name, time) keys are ignored (first write * wins) rather than updated. In practice a collision here means the same * already-collected sample is being (re)written -- e.g. a batch retried * after a partial failure, or two writers racing on the same rounded * second -- not two independently meaningful readings, since RRD only * keeps one value per timestamp regardless. This also matches the sibling * poller_output insert, which this table shadows and which has always used * INSERT IGNORE. * * @param array $value_tuples Pre-built "(local_data_id,rrd_name,time,output)" * VALUES tuples, e.g. "(1,'ds',NOW(),'1.23')" * @param mixed $conn DB connection to use, or false for the default * * @return bool True only when every chunk was accepted by the database. */ function boost_flush_output_batch(array $value_tuples, mixed $conn = false) : bool { if (!cacti_sizeof($value_tuples)) { return true; } // max_allowed_packet is a per-server setting and this helper is called with // both the local and the remote poller connection, so the lookup is cached // per connection rather than once for the whole process. spl_object_id() is // how lib/database.php already identifies a connection; the default (false) // connection gets key 0, which spl_object_id() never returns. static $packet_limits = []; $conn_key = is_object($conn) ? spl_object_id($conn) : 0; if (!isset($packet_limits[$conn_key])) { $row = db_fetch_row("SHOW VARIABLES LIKE 'max_allowed_packet'", true, $conn); $packet_limits[$conn_key] = !empty($row['Value']) ? (int) $row['Value'] : 1048576; } $max_allowed_packet = $packet_limits[$conn_key]; $sql_prefix = 'INSERT IGNORE INTO poller_output_boost (local_data_id, rrd_name, time, output) VALUES '; // account for the prefix and the ',' delimiter ahead of each tuple // after the first when sizing the buffer against max_allowed_packet $overhead = strlen($sql_prefix) + 1; $out_buffer = ''; $out_length = 0; $chunk = []; $rows = 0; foreach ($value_tuples as $tuple) { $tuple_length = strlen($tuple); if ($out_length > 0 && ($out_length + $overhead + $tuple_length) > $max_allowed_packet) { try { $acknowledged = db_execute($sql_prefix . $out_buffer, true, $conn) !== false; } catch (Throwable) { $acknowledged = false; } if (!$acknowledged) { boost_log_failed_staging($rows); return false; } $inserted = (int) db_affected_rows($conn); if ($inserted < $rows) { boost_log_ignored_sample_keys($chunk, $rows, $inserted); } $out_buffer = $tuple; $out_length = $tuple_length; $chunk = [$tuple]; $rows = 1; } else { $out_buffer .= ($out_buffer != '' ? ',' : '') . $tuple; $out_length += $tuple_length + ($out_length > 0 ? 1 : 0); $chunk[] = $tuple; $rows++; } } if ($out_buffer != '') { try { $acknowledged = db_execute($sql_prefix . $out_buffer, true, $conn) !== false; } catch (Throwable) { $acknowledged = false; } if (!$acknowledged) { boost_log_failed_staging($rows); return false; } $inserted = (int) db_affected_rows($conn); if ($inserted < $rows) { boost_log_ignored_sample_keys($chunk, $rows, $inserted); } } return true; } /** * Handles the on-demand poller boost functionality for Cacti. * * This function processes the results of a poller run and inserts the data * into the `poller_output_boost` table. It ensures that the data is inserted * in a way that avoids exceeding the maximum allowed packet size for SQL queries. * Additionally, it manages error handling and configuration options related to * the boost functionality. * * When boost_redirect is enabled, spine/cmd.php have already written these * samples to `poller_output_boost`, so nothing is inserted here and the rows * are reported as staged. poller_recovery.php writes recovered remote rows * through boost_flush_output_batch() directly and is not affected. * * @param array $results An array of poller results, where each result contains: * - 'local_data_id': The ID of the local data source. * - 'rrd_name': The name of the RRD file. * - 'time': The timestamp of the data. * - 'output': The output value to be stored. * * @param-out bool $staged Whether every requested row was durably staged. * * @return bool Returns `false` if the boost functionality is enabled and processed, * or `true` if the boost functionality is disabled or bypassed. */ function boost_poller_on_demand(array &$results, ?bool &$staged = null) : bool { global $remote_db_cnn_id; $staged = false; if (POLLER_ID > 1 && CACTI_CONNECTION == 'online') { // @phpstan-ignore-line $conn = $remote_db_cnn_id; } else { $conn = false; } if (read_config_option('boost_rrd_update_enable') == 'on' || POLLER_ID > 1) { $previous_error_reporting = error_reporting(); set_config_option('boost_rrd_update_enable', 'on'); // suppress warnings if (defined('E_DEPRECATED')) { error_reporting(E_ALL ^ E_DEPRECATED); } else { error_reporting(E_ALL); } // install the boost error handler set_error_handler('boost_error_handler'); if (boost_check_correct_enabled()) { // spine/cmd.php already staged these rows when direct redirection is active, // so the poller must never insert them into poller_output_boost again. if (read_config_option('boost_redirect') == 'on') { $staged = true; restore_error_handler(); error_reporting($previous_error_reporting); return false; } if (cacti_sizeof($results)) { $value_tuples = []; foreach ($results as $result) { $value_tuples[] = '(' . (int) $result['local_data_id'] . ',' . db_qstr($result['rrd_name'], $conn) . ',' . db_qstr($result['time'], $conn) . ',' . db_qstr($result['output'], $conn) . ')'; } $acknowledged = boost_flush_output_batch($value_tuples, $conn); $staged = $acknowledged; $return_value = !$acknowledged; } else { $return_value = false; } } else { $return_value = true; } // restore original error handler restore_error_handler(); error_reporting($previous_error_reporting); return $return_value; } else { return true; } } /** * Checks the validity of the poller ID based on the storage location * and connection type. * * This function ensures that the poller ID is valid, particularly when * running from a remote poller. If the storage location is not set to * RRDproxy and the connection type is 'online', the function will return * false for remote pollers (poller ID > 1). * * @return bool Returns true if the poller ID is valid, otherwise false. */ function boost_poller_id_check() : bool { $storage_location = read_config_option('storage_location'); /* error out if running from a remote poller and the storage * location is not the RRDproxy */ if (POLLER_ID > 1) { if (CACTI_CONNECTION == 'online') { if ($storage_location == 0) { return false; } else { return true; } } else { return false; } } return true; } /** * Fetches and processes cache data for a given local data ID using the Boost plugin. * * This function checks if the Boost plugin is enabled and properly configured. If so, it * processes the poller output for the specified local data ID and updates the RRD files. * It also handles error reporting and manages the RRDTool pipe resource. * * @param int $local_data_id The ID of the local data to process. * @param mixed $rrdtool_pipe An existing RRDTool pipe resource. If not provided, * a new pipe will be initialized and closed within the function. * * @return bool Returns false if Boost is not enabled or not properly configured. */ function boost_fetch_cache_check(int $local_data_id, mixed $rrdtool_pipe = null) : bool { if (read_config_option('boost_rrd_update_enable') == 'on') { // include poller processing routines include_once(CACTI_PATH_LIBRARY . '/poller.php'); // check to see if boost can do its job if (!boost_poller_id_check()) { return false; } $previous_error_reporting = error_reporting(); // suppress warnings if (defined('E_DEPRECATED')) { error_reporting(E_ALL ^ E_DEPRECATED); } else { error_reporting(E_ALL); } // install the boost error handler set_error_handler('boost_error_handler'); // process input parameters if (!is_resource($rrdtool_pipe)) { $rrdtool_pipe = rrd_init(); $close_pipe = true; } else { $close_pipe = false; } // get the information to populate into the rrd files if (boost_check_correct_enabled()) { boost_process_poller_output($local_data_id, $rrdtool_pipe); } // restore original error handler restore_error_handler(); error_reporting($previous_error_reporting); // close rrdtool if ($close_pipe) { rrd_close($rrdtool_pipe); } return true; } return false; } /** * Determines whether a cached image should be returned based on the provided graph data array * and system configuration options. * * This function evaluates several conditions to decide if caching is enabled and applicable: * - If the graph data array contains 'export_csv' or 'export_realtime', caching is disabled. * - If the graph data array explicitly sets 'disable_cache' to true, caching is disabled. * - If the system configuration option 'boost_png_cache_enable' is set to 'on' and the * caching state is determined to be valid, caching is enabled. * * @param array $graph_data_array Reference to the graph data array containing parameters * that influence caching behavior. * * @return bool Returns true if a cached image should be returned, false otherwise. */ function boost_return_cached_image(&$graph_data_array) : bool { if (isset($graph_data_array['export_csv'])) { return false; } if (isset($graph_data_array['export_realtime'])) { return false; } if (isset($graph_data_array['disable_cache']) && $graph_data_array['disable_cache'] == true) { return false; } if (read_config_option('boost_png_cache_enable') == 'on' && boost_determine_caching_state()) { return true; } else { return false; } } /** * Build an opaque cache name so graph identifiers and dimensions are not * enumerable when an administrator places the cache below a web root. */ function boost_graph_cache_filename(string $cache_directory, int $local_graph_id, mixed $rra_id, int $timespan, string $business_hours_index, array $graph_data_array) : string { static $secret = null; if ($secret === null) { $secret = read_config_option('boost_png_cache_secret'); if (!is_string($secret) || !preg_match('/^[a-f0-9]{64}$/D', $secret)) { $candidate = bin2hex(random_bytes(32)); db_execute_prepared('INSERT IGNORE INTO settings (name, value) VALUES (?, ?)', ['boost_png_cache_secret', $candidate]); $secret = read_config_option('boost_png_cache_secret', true); if (!is_string($secret) || !preg_match('/^[a-f0-9]{64}$/D', $secret)) { $secret = $candidate; set_config_option('boost_png_cache_secret', $secret); } } } $cache_key = serialize([ 'theme' => get_selected_theme(), 'local_graph_id' => $local_graph_id, 'rra_id' => (int) $rra_id, 'timespan' => $timespan, 'business_hours' => $business_hours_index, 'height' => $graph_data_array['graph_height'] ?? null, 'width' => $graph_data_array['graph_width'] ?? null, 'nolegend' => isset($graph_data_array['graph_nolegend']), ]); return rtrim($cache_directory, '/\\') . DIRECTORY_SEPARATOR . hash_hmac('sha256', $cache_key, $secret) . '.png'; } /** Replace an existing cache object after Windows rejects rename-over-existing. */ function boost_replace_cache_file_on_windows(string $temp_file, string $cache_file) : bool { if (!is_file($temp_file) || !is_file($cache_file)) { return false; } $backup_file = tempnam(dirname($cache_file), '.boost-old-'); if ($backup_file === false || !@unlink($backup_file) || !@rename($cache_file, $backup_file)) { return false; } if (@rename($temp_file, $cache_file)) { @unlink($backup_file); return true; } // Keep the previously published object when the replacement cannot be // installed. If the rename remains blocked, retain the backup as the // recoverable copy rather than deleting it. @rename($backup_file, $cache_file); return false; } /** Write a complete cache object and publish it with a same-directory rename. */ function boost_atomic_write_cache(string $cache_file, string $output) : bool { $temp_file = tempnam(dirname($cache_file), '.boost-'); if ($temp_file === false) { return false; } $fileptr = fopen($temp_file, 'wb'); if ($fileptr === false) { @unlink($temp_file); return false; } $length = strlen($output); $written = 0; $chunk_size = 1024 * 1024; while ($written < $length) { $chunk = substr($output, $written, $chunk_size); $chunk_length = strlen($chunk); $chunk_written = 0; while ($chunk_written < $chunk_length) { $result = fwrite($fileptr, substr($chunk, $chunk_written)); if ($result === false || $result === 0) { fclose($fileptr); @unlink($temp_file); return false; } $chunk_written += $result; $written += $result; } } $flushed = fflush($fileptr); fclose($fileptr); if (!$flushed || !chmod($temp_file, 0644)) { @unlink($temp_file); return false; } $published = @rename($temp_file, $cache_file); if (!$published && PHP_OS_FAMILY === 'Windows') { $published = boost_replace_cache_file_on_windows($temp_file, $cache_file); } if (!$published) { @unlink($temp_file); return false; } return true; } /** * Checks the graph cache for a given graph and returns the cached image if valid. * If the cache is invalid or unavailable, it falls back to Cacti's graphing functions. * * @param int $local_graph_id The ID of the local graph to check. * @param mixed $rra_id The RRA ID associated with the graph. * @param mixed $rrdtool_pipe Optional RRDTool pipe for processing (default: null). * @param array $graph_data_array Reference to an array containing graph data (default: empty array). * @param bool $return Whether to return the result (default: true). * * @return string|false Returns the cached image data if available and valid, or false otherwise. * * @throws Exception If there are issues with the cache directory or file operations. * */ function boost_graph_cache_check(int $local_graph_id, mixed $rra_id, mixed $rrdtool_pipe = null, array &$graph_data_array = [], bool $return = true) : string|false { // include poller processing routines include_once(CACTI_PATH_LIBRARY . '/poller.php'); $previous_error_reporting = error_reporting(); // suppress warnings if (defined('E_DEPRECATED')) { error_reporting(E_ALL ^ E_DEPRECATED); } else { error_reporting(E_ALL); } // install the boost error handler set_error_handler('boost_error_handler'); // check to see if boost can do its job if (!boost_poller_id_check()) { restore_error_handler(); error_reporting($previous_error_reporting); return false; } // This is a realtime graph if (isset($graph_data_array['export_realtime'])) { // restore original error handler restore_error_handler(); error_reporting($previous_error_reporting); return false; } // if we are just printing the rrd command return if (isset($graph_data_array['print_source'])) { // restore original error handler restore_error_handler(); error_reporting($previous_error_reporting); return false; } // if we want to view the error message, then don't show the cache if ((isset($graph_data_array['output_flag'])) && ($graph_data_array['output_flag'] == RRDTOOL_OUTPUT_STDERR)) { // restore original error handler restore_error_handler(); error_reporting($previous_error_reporting); return false; } // get the information to populate into the rrd files if (boost_check_correct_enabled()) { // before we make a graph, we need to check for rrd updates and perform them. $local_data_ids = db_fetch_assoc_prepared('SELECT DISTINCT data_template_rrd.local_data_id FROM graph_templates_item INNER JOIN data_template_rrd ON (graph_templates_item.task_item_id = data_template_rrd.id) WHERE graph_templates_item.local_graph_id = ? AND data_template_rrd.local_data_id > 0', [$local_graph_id]); // first update the RRD files if (cacti_sizeof($local_data_ids)) { $updates = 0; foreach ($local_data_ids as $local_data_id) { $updates += boost_process_poller_output($local_data_id['local_data_id'], $rrdtool_pipe); } if ($updates) { // restore original error handler restore_error_handler(); error_reporting($previous_error_reporting); return false; } } } if (isset($_SESSION['sess_current_timespan'])) { $timespan = $_SESSION['sess_current_timespan']; } else { $timespan = 0; } /* check the graph cache and use it if it is valid, otherwise turn over to * cacti's graphing functions. */ if (boost_return_cached_image($graph_data_array)) { /* if timespan is greater than 1, it is a predefined, if it does not * exist, it is the old fashioned MRTG type graph */ $cache_directory = read_config_option('boost_png_cache_directory'); if (read_config_option('business_hours_enable') == 'on') { $bh_index = gnrv('business_hours') == 'true' ? '_bh_' : ''; } else { $bh_index = ''; } if ($cache_directory != '') { if (is_dir($cache_directory)) { if (is_writable($cache_directory)) { $cache_file = boost_graph_cache_filename($cache_directory, $local_graph_id, $rra_id, (int) $timespan, $bh_index, $graph_data_array); if (file_exists($cache_file)) { $mod_time = filemtime($cache_file); $poller_interval = read_config_option('poller_interval'); if (!isset($poller_interval)) { $poller_interval = '300'; } if (($mod_time + $poller_interval) > time()) { if ($fileptr = fopen($cache_file, 'rb')) { $output = fread($fileptr, filesize($cache_file)); fclose($fileptr); // restore original error handler restore_error_handler(); error_reporting($previous_error_reporting); // get access to the SNMP Cache of BOOST $mc = new MibCache('CACTI-BOOST-MIB'); $mc->object('boostStatsTotalsImagesCacheReads')->count(); $mc->object('boostStatsLastUpdate')->set(time()); return $output; } else { cacti_log("Attempting to open cache file '$cache_file' failed", false, 'BOOST', POLLER_VERBOSITY_DEBUG); } } else { cacti_log("Boost Cache PNG Expired. Image '$cache_file' will be recreated", false, 'BOOST', POLLER_VERBOSITY_DEBUG); } } } else { cacti_log('ERROR: Boost Cache Directory is not writable! Can not cache images', false, 'BOOST'); } } else { cacti_log('ERROR: Boost Cache Directory does not exist! Can not cache images', false, 'BOOST'); } } else { cacti_log('ERROR: Boost Cache Directory variable is not set! Can not cache images', false, 'BOOST'); } } // restore original error handler restore_error_handler(); error_reporting($previous_error_reporting); return false; } /** * Prepares the graph data array for processing by configuring error handling * and determining the caching state. * * This function temporarily suppresses warnings and installs a custom error * handler to manage errors during the preparation process. It also determines * the caching state and sets the default output flag for the graph data array * if not already defined. * * @param array $graph_data_array The graph data array to be prepared. * * @return array The prepared graph data array with any necessary modifications. */ function boost_prep_graph_array(array $graph_data_array) : array { $previous_error_reporting = error_reporting(); // suppress warnings if (defined('E_DEPRECATED')) { error_reporting(E_ALL ^ E_DEPRECATED); } else { error_reporting(E_ALL); } // install the boost error handler set_error_handler('boost_error_handler'); if (boost_determine_caching_state()) { if (!isset($graph_data_array['output_flag'])) { if (!isset($graph_data_array['print_source'])) { $graph_data_array['output_flag'] = RRDTOOL_OUTPUT_STDOUT; } } } // restore original error handler restore_error_handler(); error_reporting($previous_error_reporting); return $graph_data_array; } /** * Handles the caching of graph images for the Cacti Boost plugin. * * This function checks if caching is enabled and determines the caching state. * If caching is valid, it generates a cache file path based on various parameters * such as graph ID, RRA ID, theme, timespan, and graph dimensions. It then writes * the graph image data to the cache file if the cache directory is writable. * * @param string|null $output The graph image data to be cached. * @param int $local_graph_id The ID of the local graph. * @param int $rra_id The RRA (Round Robin Archive) ID. * * @throws Exception If the cache directory is not writable, does not exist, or is not set. * * @return void */ function boost_graph_set_file(string|null &$output, int $local_graph_id, int|null $rra_id) : void { global $graph_data_array; // get access to the SNMP Cache of BOOST $mc = new MibCache('CACTI-BOOST-MIB'); $previous_error_reporting = error_reporting(); // suppress warnings if (defined('E_DEPRECATED')) { error_reporting(E_ALL ^ E_DEPRECATED); } else { error_reporting(E_ALL); } // install the boost error handler set_error_handler('boost_error_handler'); if (isset($_SESSION['sess_current_timespan'])) { $timespan = $_SESSION['sess_current_timespan']; } else { $timespan = 0; } /* check the graph cache and use it if it is valid, otherwise turn over to * cacti's graphing functions. */ if ((read_config_option('boost_png_cache_enable')) && (boost_determine_caching_state())) { $cache_directory = read_config_option('boost_png_cache_directory'); if (read_config_option('business_hours_enable') == 'on') { $bh_index = gnrv('business_hours') == 'true' ? '_bh_' : ''; } else { $bh_index = ''; } if ($cache_directory != '') { if (is_dir($cache_directory)) { $cache_file = boost_graph_cache_filename($cache_directory, $local_graph_id, $rra_id, (int) $timespan, $bh_index, $graph_data_array); if (is_writable($cache_directory)) { // if the cache file was created in a prior step, save it if (is_string($output) && strlen($output) > 10) { if (boost_atomic_write_cache($cache_file, $output)) { // count the number of images that had to be cached $mc->object('boostStatsTotalsImagesCacheWrites')->count(); $mc->object('boostStatsLastUpdate')->set(time()); } } } else { cacti_log('ERROR: Boost Cache Directory is not writable! Can not cache images', false, 'BOOST'); } } else { cacti_log('ERROR: Boost Cache Directory does not exist! Can not cache images', false, 'BOOST'); } } else { cacti_log('ERROR: Boost Cache Directory variable is not set! Can not cache images', false, 'BOOST'); } } // restore original error handler restore_error_handler(); error_reporting($previous_error_reporting); } /** * Tracks and logs execution time for specific areas of the application. * * This function is used to measure the time taken for a specific area of code * by marking the start and end times. It calculates the total elapsed time * and the number of cycles for the given area. * * @param string $area The name of the area being timed. * @param int $type The type of timer action, either BOOST_TIMER_START or BOOST_TIMER_END. */ function boost_timer(string $area, int $type) : void { global $boost_stats_log; // get the time $btime = microtime(true); if ($type == BOOST_TIMER_START) { $boost_stats_log[$area][BOOST_TIMER_START] = $btime; } elseif ($type == BOOST_TIMER_END) { if (isset($boost_stats_log[$area][BOOST_TIMER_START])) { if (!isset($boost_stats_log[$area][BOOST_TIMER_TOTAL])) { $boost_stats_log[$area][BOOST_TIMER_TOTAL] = 0; $boost_stats_log[$area][BOOST_TIMER_CYCLES] = 0; } $boost_stats_log[$area][BOOST_TIMER_TOTAL] += $btime - $boost_stats_log[$area][BOOST_TIMER_START]; $boost_stats_log[$area][BOOST_TIMER_CYCLES]++; unset($boost_stats_log[$area][BOOST_TIMER_START]); } } } /** * Measures the overhead introduced by the `boost_timer` function. * * This function calculates the time taken to execute a series of * `boost_timer` start and end calls for a specified number of iterations, * defined by the `BOOST_TIMER_OVERHEAD_MULTIPLIER` constant. The measured * overhead is then returned as a floating-point value representing the * elapsed time in seconds. * * @return float The calculated overhead time in seconds. */ function boost_timer_get_overhead() : float { global $boost_stats_log; $start = microtime(true); $area = 'calibrate'; for ($i = 0; $i < BOOST_TIMER_OVERHEAD_MULTIPLIER; $i++) { boost_timer($area, BOOST_TIMER_START); boost_timer($area, BOOST_TIMER_END); } unset($boost_stats_log[$area]); return (microtime(true) - $start); } /** * Clamps the configured boost_parallel value to a sane process count. * * read_config_option() may return '', null, a negative value, or a non-numeric * string. All of those mean "run a single child"; anything else is the integer * process count. Both call sites in poller_boost.php must agree on this so the * parent spawns exactly as many children as it later waits for. * * @param mixed $value Raw boost_parallel option value. * * @return int Process count >= 1. */ function boost_clamp_parallel(mixed $value) : int { $processes = intval($value); return $processes > 0 ? $processes : 1; } /** * Validates an archive table name against the expected boost pattern. * * The name is interpolated into DDL/DML without parameter binding, so it must * match poller_output_boost_arch_ exactly before any use. * * @param mixed $table Candidate table name. * * @return bool True when the name is a well-formed boost archive table. */ function boost_is_valid_archive_table(mixed $table) : bool { return is_string($table) && preg_match('/^poller_output_boost_arch_\d+$/D', $table) === 1; } /** * Decides whether a boost log path is safe to splice into a shell redirect. * * redirect_args bypasses per-argument escaping, so the path must contain no * shell metacharacters. A plain character allow-list is too strict on Windows, * where legitimate paths carry a drive colon, backslashes, and spaces, so those * are permitted on win32 only. * * @param mixed $path Candidate log path. * * @return bool True when the path is safe to use unescaped in a redirect. */ function boost_log_path_is_safe(mixed $path) : bool { if (!is_string($path) || $path === '') { return false; } if (defined('PHP_OS_FAMILY') && PHP_OS_FAMILY === 'Windows') { // Allow drive colon, backslash, and space; still reject shell metacharacters. return preg_match('/[^A-Za-z0-9_.\/\\\\: -]/', $path) === 0; } return preg_match('/[^A-Za-z0-9_.\/\-]/', $path) === 0; } /** * Reports whether every launched boost child has registered. * * exec_background() is non-blocking, so the parent must not enter its drain loop * (and the unconditional archive-table DROP that follows) until all $expected * children are accounted for. A child counts as accounted for once it is either * still running or has already recorded a completion row, which closes the race * where a fast child registers and exits before its siblings boot. * * @param int $expected Number of children launched. * @param int $running Children currently in the processes table. * @param int $completed Completion rows in poller_output_boost_processes. * * @return bool True once running + completed covers every launched child. */ function boost_all_children_registered(int $expected, int $running, int $completed) : bool { return ($running + $completed) >= $expected; } /** * Retrieves the names of the archive tables related to poller output boost. * * @param mixed $latest_table - Optional. The name of the latest table to check * if no other tables are found. * * @return mixed - Returns an associative array of table names if found, * where the keys and values are the table names. * Returns false if no tables are found and the latest * table is not provided or does not exist. */ function boost_get_arch_table_names(mixed $latest_table = '') : mixed { $tableData = db_fetch_assoc("SHOW tables LIKE 'poller_output_boost_arch%'"); $tableNames = []; if (cacti_sizeof($tableData)) { foreach ($tableData as $table) { $table = array_values($table); $table_name = (string) $table[0]; if (boost_is_valid_archive_table($table_name)) { $tableNames[$table_name] = $table_name; } else { $display_name = preg_replace('/[^\x20-\x7e]/', '?', $table_name) ?? ''; cacti_log('WARNING: Boost ignored an unexpected archive-like table name: ' . $display_name, false, 'BOOST'); } } } if (!cacti_sizeof($tableNames)) { $tableNames = array_rekey( db_fetch_assoc("SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = SCHEMA() AND TABLE_NAME LIKE 'poller_output_boost_arch_%'"), 'name', 'name' ); if (is_array($tableNames)) { $tableNames = array_filter($tableNames, 'boost_is_valid_archive_table'); } else { $tableNames = []; } } if (!cacti_sizeof($tableNames)) { // Both lookups above read metadata (SHOW TABLES / information_schema), // which can lag the data on a replica long enough to miss a table the // parent just created. Fall back to the validated name the parent passed // and confirm it with a data-plane read, which replicates with the rows. if (boost_is_valid_archive_table($latest_table) && boost_archive_table_readable($latest_table)) { $tableNames[$latest_table] = $latest_table; return $tableNames; } else { return false; } } else { return $tableNames; } } /** * Confirms an archive table exists by reading from it, not from metadata. * * SHOW TABLES and information_schema are metadata-plane lookups that can lag on * a replica; a SELECT against the table itself replicates with the row data, so * it reflects the table the parent just created. The name must already be a * validated boost archive table before it reaches the interpolated query. * * @param string $table A name that passed boost_is_valid_archive_table(). * * @return bool True when the table can be read. */ function boost_archive_table_readable(string $table) : bool { if (!boost_is_valid_archive_table($table)) { return false; } // COUNT(*) returns a numeric row even for an empty table, so a non-null, // non-false result means the table exists; a query error (missing table) // yields false. $log = false: a fallback miss is expected, not an error. $result = db_fetch_cell("SELECT COUNT(*) FROM `$table`", '', false); return $result !== false && $result !== null; } /** * Grabs data from the 'poller_output' and 'poller_output_boost*' * table and feeds to RRDtool for processing. This function has been repurposed for a * single local_data_id. In the past, it was designed to handle one to many local_data_ids. * * The process works as follows: * * 1) Gather all the rows for the local_data_id from the archive tables on archive table * at a time. * 2) Gather all the rows from the main boost table * 3) Delete those entries from all the aforementioned tables * 4) Merge the results together * 5) Process the entire result set * * @param int $local_data_id - The local data id to update. * @param mixed $rrdtool_pipe - An array for the RRDtool process object * * @return int */ function boost_process_poller_output(int $local_data_id, mixed $rrdtool_pipe = []) : int { global $database_default, $boost_sock, $boost_timeout, $get_memory, $memory_used, $archive_table; static $warning_issued; static $rrdtool_version = null; cacti_system_zone_set(); include_once(CACTI_PATH_LIBRARY . '/rrd.php'); $previous_error_reporting = error_reporting(); // suppress warnings if (defined('E_DEPRECATED')) { error_reporting(E_ALL ^ E_DEPRECATED); } else { error_reporting(E_ALL); } // install the boost error handler set_error_handler('boost_error_handler'); if (cacti_version_compare(get_rrdtool_version(), '1.5', '<')) { while (!db_fetch_cell("SELECT GET_LOCK('boost.single_ds.$local_data_id', 1)")) { usleep(50000); } } $data_ids_to_get = read_config_option('boost_rrd_update_max_records_per_select'); $archive_tables = boost_get_arch_table_names($archive_table); $results = []; // avoid getting rows in the middle of poller run $timestamp = db_fetch_cell('SELECT MIN(UNIX_TIMESTAMP(start_time)) FROM poller_time WHERE end_time="0000-00-00"'); if (empty($timestamp)) { $timestamp = time() - 10; } $query_string = ''; $sql_params = []; $locks = false; $temp_table = false; if (cacti_count($archive_tables)) { $temp_table = 'poller_output_boost_temp_' . $local_data_id . '_' . random_int(0, PHP_INT_MAX); db_execute("CREATE TEMPORARY TABLE `{$temp_table}` LIKE poller_output_boost"); foreach ($archive_tables as $table) { db_execute_prepared("INSERT IGNORE INTO `{$temp_table}` SELECT * FROM `{$table}` WHERE local_data_id = ?", [$local_data_id], false); } } if ($temp_table !== false) { db_execute_prepared("INSERT IGNORE INTO `{$temp_table}` SELECT * FROM poller_output_boost WHERE local_data_id = ? AND time < FROM_UNIXTIME(?)", [$local_data_id, $timestamp], false); $query_string = "SELECT po.local_data_id, dl.data_template_id, UNIX_TIMESTAMP(po.time) AS timestamp, po.rrd_name, po.output FROM `{$temp_table}` AS po INNER JOIN data_local AS dl ON po.local_data_id = dl.id WHERE po.local_data_id = ? AND po.time < FROM_UNIXTIME(?) ORDER BY time ASC, rrd_name ASC"; } else { $query_string = 'SELECT po.local_data_id, dl.data_template_id, UNIX_TIMESTAMP(po.time) AS timestamp, po.rrd_name, po.output FROM poller_output_boost AS po INNER JOIN data_local AS dl ON po.local_data_id = dl.id WHERE po.local_data_id = ? AND po.time < FROM_UNIXTIME(?) ORDER BY time ASC, rrd_name ASC'; } $sql_params[] = $local_data_id; $sql_params[] = $timestamp; boost_timer('get_records', BOOST_TIMER_START); $results = db_fetch_assoc_prepared($query_string, $sql_params); boost_timer('get_records', BOOST_TIMER_END); $boost_results = cacti_sizeof($results); if ($temp_table !== false) { db_execute("DROP TEMPORARY TABLE $temp_table"); } cacti_log('Local Data ID: ' . $local_data_id . ', Boost Results: ' . $boost_results, false, 'BOOST', POLLER_VERBOSITY_MEDIUM); $updates_ok = $results !== false; // log memory if ($get_memory) { $cur_memory = memory_get_usage(); if ($cur_memory > $memory_used) { $memory_used = $cur_memory; } } if (cacti_sizeof($results)) { $local_init = false; if (!$rrdtool_pipe) { $rrdtool_pipe = rrd_init(); $local_init = true; } // create an array keyed off of each .rrd file $time = -1; $outbuf = ''; $last_update = -1; $multi_vals_set = false; $last_item = [ 'local_data_id' => -1, 'timestamp' => -1, 'rrd_name' => '' ]; // we are going to blow away all record if ok $vals_in_buffer = 0; $reset_template = false; $upd_string_len = read_config_option('boost_rrd_update_string_length'); // initialize some variables $rrd_tmpl = ''; $rrd_path = ''; $outlen = 0; $path_template = boost_get_rrd_filename_and_template($local_data_id); if (cacti_sizeof($path_template)) { $rrd_path = $path_template['rrd_path']; $rrd_tmpl = $path_template['rrd_template']; } else { $rrd_path = db_fetch_cell_prepared('SELECT rrd_path FROM poller_item WHERE local_data_id = ?', [$local_data_id]); } cacti_log('The RRDpath is ' . $rrd_path, false, 'BOOST', POLLER_VERBOSITY_MEDIUM); cacti_log('The RRDpath template is ' . $rrd_tmpl, false, 'BOOST', POLLER_VERBOSITY_MEDIUM); $unused_data_source_names = array_rekey( db_fetch_assoc_prepared('SELECT DISTINCT dtr.data_source_name, dtr.data_source_name FROM data_template_rrd AS dtr LEFT JOIN graph_templates_item AS gti ON dtr.id = gti.task_item_id WHERE dtr.local_data_id = ? AND gti.task_item_id IS NULL', [$local_data_id]), 'data_source_name', 'data_source_name' ); boost_timer('results_cycle', BOOST_TIMER_START); // go through each poller_output_boost entries and process foreach ($results as $item) { if (cacti_sizeof($unused_data_source_names) && isset($unused_data_source_names[$item['rrd_name']])) { continue; } /** * detect duplicate records, this should not happen, * but adding just in case. */ if ($last_item['timestamp'] == $item['timestamp'] && $last_item['rrd_name'] == $item['rrd_name']) { cacti_log(sprintf('WARNING: Skipping %s:%s due to duplicate record...', $item['local_data_id'], $item['rrd_name']), false, 'BOOST'); continue; } // don't generate error messages if the RRD has already been updated if ($time < $last_update && cacti_version_compare(get_rrdtool_version(), '1.5', '<')) { cacti_log("WARNING: Stale Poller Data Found! Item Time:'" . $time . "', RRD Time:'" . $last_update . "' Ignoring Value!", false, 'BOOST', POLLER_VERBOSITY_HIGH); $value = 'DNP'; } else { $value = trim($item['output']); } if ($time != $item['timestamp']) { if ($outlen > $upd_string_len) { boost_timer('rrdupdate', BOOST_TIMER_START); $return_value = boost_rrdtool_function_update($local_data_id, $rrd_path, $rrd_tmpl, $outbuf, $rrdtool_pipe); boost_timer('rrdupdate', BOOST_TIMER_END); $outbuf = ''; $outlen = 0; $vals_in_buffer = 0; // check return status for delete operation if (!str_contains(trim($return_value), 'OK') && $return_value != '') { cacti_log("WARNING: RRD Update Warning '" . $return_value . "' for Local Data ID '$local_data_id'", false, 'BOOST'); $updates_ok = false; } } if (!str_contains($value, 'DNP')) { $output = ' ' . $item['timestamp']; $outbuf .= $output; $outlen += strlen($output); } $time = $item['timestamp']; } // single one value output if (str_contains($value, 'DNP')) { // continue, bad time } elseif ((is_numeric($value)) || ($value == 'U' && $item['rrd_name'] != '')) { $output = ':' . $value; $outbuf .= $output; $outlen += strlen($output); $vals_in_buffer++; } elseif ((function_exists('is_hexadecimal')) && (is_hexadecimal($value))) { $output = ':' . hexdec($value); $outbuf .= $output; $outlen += strlen($output); $vals_in_buffer++; } elseif (str_contains($value, ':')) { $values = preg_split('/\s+/', $value); if (!$multi_vals_set) { if ($item['data_template_id'] > 0) { $rrd_field_names = array_rekey( db_fetch_assoc_prepared('SELECT DISTINCT dtr.data_source_name, dif.data_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 dtr.data_input_field_id = dif.id WHERE dtr.local_data_id = ?', [$item['local_data_id']]), 'data_name', 'data_source_name' ); $unused_data_source_names = array_rekey( db_fetch_assoc_prepared('SELECT DISTINCT dtr.data_source_name, dtr.data_source_name FROM data_template_rrd AS dtr LEFT JOIN graph_templates_item AS gti ON dtr.id = gti.task_item_id WHERE dtr.local_data_id = ? AND gti.task_item_id IS NULL', [$item['local_data_id']]), 'data_source_name', 'data_source_name' ); } else { $rrd_field_names = array_rekey( db_fetch_assoc_prepared('SELECT DISTINCT dtr.data_source_name, dif.data_name FROM data_template_rrd AS dtr INNER JOIN data_input_fields AS dif ON dtr.data_input_field_id = dif.id WHERE dtr.local_data_id = ?', [$item['local_data_id']]), 'data_name', 'data_source_name' ); $unused_data_source_names = array_rekey( db_fetch_assoc_prepared('SELECT DISTINCT dtr.data_source_name, dtr.data_source_name FROM data_template_rrd AS dtr LEFT JOIN graph_templates_item AS gti ON dtr.id = gti.task_item_id WHERE dtr.local_data_id = ? AND gti.task_item_id IS NULL', [$item['local_data_id']]), 'data_source_name', 'data_source_name' ); } $rrd_tmpl = ''; } $first_tmpl = true; $multi_ok = false; if (cacti_sizeof($values)) { foreach (($values ?: []) as $value) { $matches = explode(':', $value); if (isset($rrd_field_names[$matches[0]])) { $field = $rrd_field_names[$matches[0]]; if (cacti_sizeof($unused_data_source_names) && isset($unused_data_source_names[$field])) { continue; } $multi_ok = true; if (!$multi_vals_set) { if (!$first_tmpl) { $rrd_tmpl .= ':'; } $rrd_tmpl .= $rrd_field_names[$matches[0]]; $first_tmpl = false; } if (is_numeric($matches[1]) || ($matches[1] == 'U')) { $output = ':' . $matches[1]; $outbuf .= $output; $outlen += strlen($output); } elseif ((function_exists('is_hexadecimal')) && (is_hexadecimal($matches[1]))) { $output = ':' . hexdec($matches[1]); $outbuf .= $output; $outlen += strlen($output); } else { $output = ':U'; $outbuf .= $output; $outlen += strlen($output); } $vals_in_buffer++; } } } // we only want to process the template and gather the fields once $multi_vals_set = true; if ($multi_ok) { $vals_in_buffer++; } } else { if (!$multi_vals_set) { if ($item['data_template_id'] > 0) { $rrd_field_names = array_rekey( db_fetch_assoc_prepared('SELECT DISTINCT dtr.data_source_name, dif.data_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 dtr.data_input_field_id = dif.id WHERE dtr.local_data_id = ?', [$item['local_data_id']]), 'data_name', 'data_source_name' ); $unused_data_source_names = array_rekey( db_fetch_assoc_prepared('SELECT DISTINCT dtr.data_source_name, dtr.data_source_name FROM data_template_rrd AS dtr LEFT JOIN graph_templates_item AS gti ON dtr.id = gti.task_item_id WHERE dtr.local_data_id = ? AND gti.task_item_id IS NULL', [$item['local_data_id']]), 'data_source_name', 'data_source_name' ); } else { $rrd_field_names = array_rekey( db_fetch_assoc_prepared('SELECT DISTINCT dtr.data_source_name, dif.data_name FROM data_template_rrd AS dtr INNER JOIN data_input_fields AS dif ON dtr.data_input_field_id = dif.id WHERE dtr.local_data_id = ?', [$item['local_data_id']]), 'data_name', 'data_source_name' ); $unused_data_source_names = array_rekey( db_fetch_assoc_prepared('SELECT DISTINCT dtr.data_source_name, dtr.data_source_name FROM data_template_rrd AS dtr LEFT JOIN graph_templates_item AS gti ON dtr.id = gti.task_item_id WHERE dtr.local_data_id = ? AND gti.task_item_id IS NULL', [$item['local_data_id']]), 'data_source_name', 'data_source_name' ); } $rrd_tmpl = ''; } $expected = ''; // TODO: This is legacy code that does not get used. Find out why its still here. $nt_rrd_field_names = []; if (cacti_sizeof($nt_rrd_field_names) > 0) { foreach ($nt_rrd_field_names as $field) { // @phpstan-ignore-line if (cacti_sizeof($unused_data_source_names) && isset($unused_data_source_names[$field])) { continue; } $expected .= ($expected != '' ? ' ' : '') . "$field:value"; if ($reset_template) { $rrd_tmpl .= ($rrd_tmpl != '' ? ':' : '') . $field; } $tv_tmpl[$field] = 'U'; } } cacti_log(sprintf('WARNING: Invalid output! MULTI DS[%d] Encountered [%s] Expected [%s]', $item['local_data_id'], $value, $expected), false, 'POLLER'); $vals_in_buffer++; $multi_vals_set = true; } // remember what we actually processed so the duplicate // check above has something real to compare against $last_item = $item; } // process the last rrdupdate if applicable if ($vals_in_buffer) { boost_timer('rrdupdate', BOOST_TIMER_START); $return_value = boost_rrdtool_function_update($local_data_id, $rrd_path, $rrd_tmpl, $outbuf, $rrdtool_pipe); boost_timer('rrdupdate', BOOST_TIMER_END); // check return status for delete operation if (!str_contains(trim($return_value), 'OK') && $return_value != '') { cacti_log("WARNING: RRD Update Warning '" . $return_value . "' for Local Data ID '$local_data_id'", false, 'BOOST'); $updates_ok = false; } } boost_timer('results_cycle', BOOST_TIMER_END); if ($local_init) { rrd_close($rrdtool_pipe); } } // The database rows are the retry record. Remove them only after every RRD // update and every still-open-row forward has been acknowledged. boost_timer('delete', BOOST_TIMER_START); if ($updates_ok && cacti_count($archive_tables)) { foreach ($archive_tables as $table) { $forward_rows = (int) db_fetch_cell_prepared("SELECT COUNT(*) FROM `$table` WHERE local_data_id = ? AND time >= FROM_UNIXTIME(?)", [$local_data_id, $timestamp]); if (db_execute_prepared("INSERT IGNORE INTO poller_output_boost SELECT * FROM `$table` WHERE local_data_id = ? AND time >= FROM_UNIXTIME(?)", [$local_data_id, $timestamp], false) === false) { $updates_ok = false; break; } $forwarded = (int) db_affected_rows(); if ($forwarded < $forward_rows) { cacti_log(sprintf( 'WARNING: Boost archive forwarding ignored %d of %d duplicate sample keys for Local Data ID %d from %s (already in poller_output_boost, first write wins).', $forward_rows - $forwarded, $forward_rows, $local_data_id, $table ), false, 'BOOST'); } } } if ($updates_ok && cacti_count($archive_tables)) { foreach ($archive_tables as $table) { if (db_execute_prepared("DELETE FROM `$table` WHERE local_data_id = ?", [$local_data_id], false) === false) { $updates_ok = false; break; } } } if ($updates_ok && cacti_sizeof($results)) { $updates_ok = db_execute_prepared('DELETE FROM poller_output_boost WHERE local_data_id = ? AND time < FROM_UNIXTIME(?)', [$local_data_id, $timestamp], false) !== false; } boost_timer('delete', BOOST_TIMER_END); if (!$updates_ok) { cacti_log("WARNING: Boost retained staged rows for Local Data ID '$local_data_id' because the handoff was not fully acknowledged.", false, 'BOOST'); } if (cacti_version_compare(get_rrdtool_version(), '1.5', '<')) { db_execute("SELECT RELEASE_LOCK('boost.single_ds.$local_data_id')"); } // restore original error handler restore_error_handler(); error_reporting($previous_error_reporting); return $updates_ok ? cacti_sizeof($results) : -1; } /** * Retrieves the last update time of an RRD file using rrdtool. * * This function checks if the provided RRD file path is valid and exists. * If the file exists, it uses rrdtool to fetch the last update time. * If the file path is empty, it returns the current system time. * * @param string $rrd_path The path to the RRD file. * @param mixed $rrdtool_pipe The rrdtool pipe resource for executing commands. * * @return int|string The last update time of the RRD file as a timestamp, or the current time if the path is empty. */ function boost_rrdtool_get_last_update_time(string $rrd_path, mixed $rrdtool_pipe) : int|string { $return_value = 0; /* check if the rrd_path is empty * It can become empty if someone has removed * a Data Source while boost is running, or a Re-Index * found the Data Source invalid, so it was removed * from the poller_item table */ if ($rrd_path == '') { return time(); } if (read_config_option('storage_location')) { $file_exists = rrdtool_execute('file_exists ' . cacti_escapeshellarg($rrd_path), true, RRDTOOL_OUTPUT_BOOLEAN, $rrdtool_pipe, 'BOOST'); } else { $file_exists = file_exists($rrd_path); } if ($file_exists == true) { $return_value = rrdtool_execute('last ' . cacti_escapeshellarg($rrd_path), true, RRDTOOL_OUTPUT_STDOUT, false, 'BOOST'); } return trim($return_value); } /** * Determines the caching state for the application based on various conditions. * * This function evaluates several factors to decide whether caching should be enabled * or disabled. It considers session variables, request parameters, and custom settings. * * @return bool Returns `true` if caching is enabled, `false` otherwise. */ function boost_determine_caching_state() : bool { set_default_action(); // turn off image caching if viewing thold vrules if (isset($_SESSION[OPTIONS_WEB]['thold_draw_vrules']) && $_SESSION[OPTIONS_WEB]['thold_draw_vrules'] == 'on') { return false; } $action = grv('action'); // turn off image caching for the following actions if ($action == 'properties' || $action == 'zoom' || $action == 'edit' || $action == 'graph_edit') { $cache = false; } else { $cache = true; } if (!isset($_SESSION['custom'])) { $custom = false; } else { $custom = $_SESSION['custom']; } if ($cache && !$custom) { return true; } else { return false; } } /** * Retrieves the RRD filename and template for a given local data ID. * * This function queries the database to fetch the RRD path and template information * associated with the specified local data ID. It determines whether the data sources * have associated RRD names and constructs the RRD template accordingly. * * @param int $local_data_id The ID of the local data for which the RRD filename and template are to be retrieved. * * @return array An associative array containing: * - 'rrd_path' (string): The path to the RRD file. * - 'rrd_template' (string): The RRD template constructed from the data source names. */ function boost_get_rrd_filename_and_template(int $local_data_id) : array { $rrd_path = ''; $all_nulls = true; $ds_null = []; $ds_nnull = []; $data_template_id = db_fetch_cell_prepared('SELECT data_template_id FROM data_local WHERE id = ?', [$local_data_id]); if ($data_template_id > 0) { $ds_names = db_fetch_assoc_prepared("SELECT DISTINCT data_source_name, rrd_name, rrd_path FROM data_template_rrd AS dtr INNER JOIN graph_templates_item AS gti ON gti.task_item_id = dtr.id INNER JOIN poller_item AS pi ON pi.local_data_id = dtr.local_data_id AND (pi.rrd_name = dtr.data_source_name OR pi.rrd_name = '') WHERE dtr.local_data_id = ? ORDER BY data_source_name ASC", [$local_data_id]); } else { $ds_names = db_fetch_assoc_prepared("SELECT DISTINCT data_source_name, rrd_name, rrd_path FROM data_template_rrd AS dtr INNER JOIN poller_item AS pi ON pi.local_data_id = dtr.local_data_id AND (pi.rrd_name = dtr.data_source_name OR pi.rrd_name = '') WHERE dtr.local_data_id = ? ORDER BY data_source_name ASC", [$local_data_id]); } if (cacti_sizeof($ds_names)) { foreach ($ds_names as $ds_name) { if ($rrd_path == '') { $rrd_path = $ds_name['rrd_path']; } if ($ds_name['rrd_name'] == '') { $ds_null[] = $ds_name['data_source_name']; } elseif ($ds_name['rrd_name'] == $ds_name['data_source_name']) { $ds_nnull[] = $ds_name['data_source_name']; $all_nulls = false; } } } if ($all_nulls) { $rrd_template = implode(':', $ds_null); } else { $rrd_template = implode(':', $ds_nnull); } return ['rrd_path' => $rrd_path, 'rrd_template' => trim($rrd_template)]; } /** * Creates an RRDTool data source file for a given local data ID. * * This function generates the necessary RRDTool commands to create a data source file * based on the provided local data ID. It ensures that the file does not already exist, * validates the associated RRA (Round Robin Archives), and constructs the data source * and RRA definitions. It also handles directory creation and permission settings for * structured paths. * * @param int $local_data_id - The ID of the local data source to create. * @param bool $show_source - If true, returns the RRDTool command instead of executing it. * @param mixed $rrdtool_pipe - The RRDTool pipe resource for executing commands. * * @return mixed - Returns the RRDTool command string if $show_source is true, * -1 if the file already exists, * false if no RRA is associated with the data source, * or the result of the RRDTool execution. */ function boost_rrdtool_function_create(int $local_data_id, bool $show_source, mixed $rrdtool_pipe) : mixed { global $consolidation_functions, $data_source_types; include(CACTI_PATH_INCLUDE . '/global_arrays.php'); $data_source_path = get_data_source_path($local_data_id, true); if (!rrd_check_path($data_source_path)) { cacti_log("ERROR: Refusing unsafe data source path '$data_source_path' for local_data_id $local_data_id", false, 'BOOST'); return false; } /* ok, if that passes lets check to make sure an rra does not already exist, the last thing we want to do is overwrite data! */ if ($show_source != true) { if (read_config_option('storage_location')) { $file_exists = rrdtool_execute('file_exists ' . cacti_escapeshellarg($data_source_path), true, RRDTOOL_OUTPUT_BOOLEAN, $rrdtool_pipe, 'POLLER'); } else { $file_exists = file_exists($data_source_path); } if ($file_exists == true) { return -1; } } /** * the first thing we must do is make sure there is at least one * rra associated with this data source... * * * UPDATE: As of version 0.6.6, we are splitting this up into two * SQL strings because of the multiple DS per RRD support. This is * not a big deal however since this function gets called once per * data source */ $rras = db_fetch_assoc_prepared('SELECT dtd.rrd_step, dsp.x_files_factor, dspr.steps, dspr.rows, dspc.consolidation_function_id, (dspr.rows * dspr.steps) AS rra_order FROM data_template_data AS dtd LEFT JOIN data_source_profiles AS dsp ON dtd.data_source_profile_id=dsp.id LEFT JOIN data_source_profiles_rra AS dspr ON dtd.data_source_profile_id=dspr.data_source_profile_id LEFT JOIN data_source_profiles_cf AS dspc ON dtd.data_source_profile_id=dspc.data_source_profile_id WHERE dtd.local_data_id = ? AND (dspr.steps IS NOT NULL OR dspr.rows IS NOT NULL) ORDER BY dspc.consolidation_function_id, rra_order', [$local_data_id]); /* if we find that this DS has no RRA associated; get out. This would * indicate that a data sources has been deleted */ if (cacti_sizeof($rras) <= 0) { return false; } // create the "--step" line $create_ds = RRD_NL . '--start 0 --step ' . $rras[0]['rrd_step'] . ' ' . RRD_NL; /** * We have to check for Non-Templated Data Source first as they may not include * a graph. So, for that case, we need the RRDfile to include all data sources */ $data_template_id = db_fetch_cell_prepared('SELECT data_template_id FROM data_local WHERE id = ?', [$local_data_id]); if ($data_template_id > 0) { $data_sources = db_fetch_assoc_prepared('SELECT DISTINCT dtr.id, dtr.data_source_name, dtr.rrd_heartbeat, dtr.rrd_minimum, dtr.rrd_maximum, dtr.data_source_type_id FROM data_template_rrd AS dtr INNER JOIN graph_templates_item AS gti ON dtr.id = gti.task_item_id WHERE dtr.local_data_id = ? ORDER BY local_data_template_rrd_id', [$local_data_id]); } else { $data_sources = db_fetch_assoc_prepared('SELECT DISTINCT dtr.id, dtr.data_source_name, dtr.rrd_heartbeat, dtr.rrd_minimum, dtr.rrd_maximum, dtr.data_source_type_id FROM data_template_rrd AS dtr WHERE dtr.local_data_id = ? ORDER BY local_data_template_rrd_id', [$local_data_id]); } /** * ONLY make a new DS entry if: * * - There are multiple data sources and this item is not the main one. * - There are only one data source (then use it) */ if (cacti_sizeof($data_sources)) { foreach ($data_sources as $data_source) { // use the cacti ds name by default or the user defined one, if entered $data_source_name = get_data_source_item_name($data_source['id']); if (empty($data_source['rrd_maximum'])) { // in case no maximum is given, use "Undef" value $data_source['rrd_maximum'] = 'U'; } elseif (str_contains($data_source['rrd_maximum'], '|query_')) { $data_local = db_fetch_row_prepared('SELECT * FROM data_local WHERE id = ?', [$local_data_id]); $speed = rrdtool_function_interface_speed($data_local); if ($data_source['rrd_maximum'] == '|query_ifSpeed|' || $data_source['rrd_maximum'] == '|query_ifHighSpeed|') { $data_source['rrd_maximum'] = $speed; } else { $data_source['rrd_maximum'] = substitute_snmp_query_data($data_source['rrd_maximum'],$data_local['host_id'], $data_local['snmp_query_id'], $data_local['snmp_index']); } } elseif (($data_source['rrd_maximum'] != 'U') && (int)$data_source['rrd_maximum'] <= (int)$data_source['rrd_minimum']) { // max > min required, but take care of an "Undef" value $data_source['rrd_maximum'] = (int)$data_source['rrd_minimum'] + 1; } // min==max==0 won't work with rrdtool if ($data_source['rrd_minimum'] == 0 && $data_source['rrd_maximum'] == 0) { $data_source['rrd_maximum'] = 'U'; } $create_ds .= "DS:$data_source_name:" . $data_source_types[$data_source['data_source_type_id']] . ':' . $data_source['rrd_heartbeat'] . ':' . $data_source['rrd_minimum'] . ':' . $data_source['rrd_maximum'] . RRD_NL; } } $create_rra = ''; // loop through each available RRA for this DS foreach ($rras as $rra) { $create_rra .= 'RRA:' . $consolidation_functions[$rra['consolidation_function_id']] . ':' . $rra['x_files_factor'] . ':' . $rra['steps'] . ':' . $rra['rows'] . RRD_NL; } if (CACTI_SERVER_OS != 'win32') { $owner_id = fileowner(CACTI_PATH_RRA); $group_id = filegroup(CACTI_PATH_RRA); } /** * check for structured path configuration, if in place verify directory * exists and if not create it. */ if (read_config_option('extended_paths') == 'on') { if (read_config_option('storage_location') > 0) { if (rrdtool_execute('is_dir ' . cacti_escapeshellarg(dirname($data_source_path)), true, RRDTOOL_OUTPUT_BOOLEAN, $rrdtool_pipe, 'BOOST') === false) { if (rrdtool_execute('mkdir ' . cacti_escapeshellarg(dirname($data_source_path)), true, RRDTOOL_OUTPUT_BOOLEAN, $rrdtool_pipe, 'BOOST') === false) { cacti_log("ERROR: Unable to create directory '" . dirname($data_source_path) . "'", false); } } } elseif (!is_dir(dirname($data_source_path))) { if (CACTI_WEB == false || is_writable(CACTI_PATH_RRA)) { if (mkdir(dirname($data_source_path), 0775, true)) { if (CACTI_SERVER_OS != 'win32' && posix_getuid() == 0) { $success = true; $paths = explode('/', str_replace(CACTI_PATH_RRA, '/', dirname($data_source_path))); $spath = ''; foreach ($paths as $path) { if ($path == '') { continue; } $spath .= '/' . $path; $powner_id = fileowner(CACTI_PATH_RRA . $spath); $pgroup_id = fileowner(CACTI_PATH_RRA . $spath); if ($powner_id != $owner_id) { $success = $owner_id !== false && chown(CACTI_PATH_RRA . $spath, $owner_id); } if ($pgroup_id != $group_id && $success) { $success = $group_id !== false && chgrp(CACTI_PATH_RRA . $spath, $group_id); } if (!$success) { cacti_log("ERROR: Unable to set directory permissions for '" . CACTI_PATH_RRA . $spath . "'", false); break; } } } } else { cacti_log("ERROR: Unable to create directory '" . dirname($data_source_path) . "'", false); } } else { cacti_log("WARNING: Poller has not created structured path '" . dirname($data_source_path) . "' yet.", false); } } } if ($show_source == true) { return read_config_option('path_rrdtool') . ' create' . RRD_NL . "$data_source_path$create_ds$create_rra"; } else { $success = rrdtool_execute('create ' . cacti_escapeshellarg($data_source_path) . " $create_ds$create_rra", false, RRDTOOL_OUTPUT_STDOUT, $rrdtool_pipe, 'BOOST'); if (CACTI_SERVER_OS != 'win32' && posix_getuid() == 0) { chown($data_source_path, (int) $owner_id); chgrp($data_source_path, (int) $group_id); } return $success; } } /** * A re-write of the Cacti rrdtool update command specifically designed for bulk updates * * @param int $local_data_id The ID of the local data source. * @param string $rrd_path The file path to the RRD file. * @param string $rrd_update_template The template string for the RRD update. * @param string &$rrd_update_values The values to update the RRD file with (passed by reference). * @param mixed $rrdtool_pipe Optional. The RRDTool pipe resource for communication. * * @return string Returns 'OK' on successful update or if the RRD file is invalid or missing. */ function boost_rrdtool_function_update(int $local_data_id, string $rrd_path, string $rrd_update_template, string &$rrd_update_values, mixed $rrdtool_pipe = null) : string { // lets count the number of rrd files processed $rrds_processed = 0; // let's check for deleted Data Sources $valid_entry = true; /* check for an empty rrd_path * this can happen when you've removed a data source * while boost is running */ if ($rrd_path == '') { return 'OK'; } // create the rrd if one does not already exist if (read_config_option('storage_location')) { $file_exists = rrdtool_execute('file_exists ' . cacti_escapeshellarg($rrd_path), true, RRDTOOL_OUTPUT_BOOLEAN, $rrdtool_pipe, 'BOOST'); } else { $file_exists = file_exists($rrd_path); } if ($file_exists == false) { $ds_exists = db_fetch_cell_prepared('SELECT id FROM data_local WHERE id = ?', [$local_data_id]); // Check for a Data Source that has been removed if ($ds_exists) { $valid_entry = boost_rrdtool_function_create($local_data_id, false, $rrdtool_pipe); } else { return 'OK'; } } if (cacti_version_compare(get_rrdtool_version(), '1.5', '>=')) { $update_options = '--skip-past-updates'; } else { $update_options = ''; } if ($valid_entry) { if ($rrd_update_template != '') { boost_debug("update $rrd_path $update_options --template $rrd_update_template $rrd_update_values"); $result = rrdtool_execute('update ' . cacti_escapeshellarg($rrd_path) . " $update_options --template $rrd_update_template $rrd_update_values", false, RRDTOOL_OUTPUT_STDOUT, $rrdtool_pipe, 'BOOST'); } else { boost_debug("update $rrd_path $update_options $rrd_update_values"); $result = rrdtool_execute('update ' . cacti_escapeshellarg($rrd_path) . " $update_options $rrd_update_values", false, RRDTOOL_OUTPUT_STDOUT, $rrdtool_pipe, 'BOOST'); } if ($result === false || preg_match('/(?:^|\b)(?:ERROR|Error)(?::|\b)/', trim((string) $result))) { // A timestamp at or behind the last update means the sample is already // in the RRD, which is what a retried shard looks like. Reporting that // as a failure would retain the shard and reprocess it forever, so the // same rows would fail again on every pass and boost would never drain. // rrdtool 1.5 and later avoid this with --skip-past-updates; older // releases raise it, and a retry after a partial pass raises it on any // version. if (is_string($result) && preg_match('/illegal attempt to update using time|minimum one second step/i', $result)) { return 'OK'; } return is_string($result) && $result !== '' ? $result : 'ERROR: RRDtool did not acknowledge the update'; } return 'OK'; } return 'OK'; } /** * Adjusts the PHP memory limit based on the configuration option 'boost_poller_mem_limit'. * * @return void */ function boost_memory_limit() : void { $memory_limit = read_config_option('boost_poller_mem_limit'); if ($memory_limit != '-1') { ini_set('memory_limit', $memory_limit . 'M'); } else { ini_set('memory_limit', -1); } } /** * Executes the Boost poller bottom process. * * This function is responsible for initiating the Boost poller process if the * Boost RRD update feature is enabled in the configuration. It performs the * following tasks: * - Reads configuration options to determine if Boost is enabled and to fetch * necessary paths and settings. * - Updates SNMP statistics using the `boost_update_snmp_statistics` function. * - Validates the Boost log file and directory for writability if debugging is enabled. * - Constructs the command string to execute the Boost poller script (`poller_boost.php`), * including debug options and log redirection if applicable. * - Executes the Boost poller script in the background. * * Configuration options used: * - `boost_rrd_update_enable`: Determines if Boost RRD updates are enabled. * - `path_boost_log`: Path to the Boost log file. * - `boost_debug_enabled`: Enables or disables Boost debugging. * - `path_php_binary`: Path to the PHP binary. * * @return void */ function boost_poller_bottom() : void { if (read_config_option('boost_rrd_update_enable') == 'on') { include_once(CACTI_PATH_LIBRARY . '/poller.php'); chdir(CACTI_PATH_BASE); $redirect_args = ''; boost_update_snmp_statistics(); $boost_log = read_config_option('path_boost_log'); $boost_debug = read_config_option('boost_debug_enabled') == 'on' ? true : false; $boost_logdir = dirname($boost_log); if ($boost_debug && $boost_log != '') { if (!is_writable($boost_log) || !is_dir($boost_logdir) || !is_writable($boost_logdir)) { boost_debug("WARNING: Boost log '$boost_log' does not exist or is not writable!"); cacti_log("WARNING: Boost log '$boost_log' is not writable!", false, 'BOOST'); $boost_log = ''; } } $command_string = cacti_escapeshellcmd((string) read_config_option('path_php_binary')); if ($boost_debug && $boost_log != '') { // exec_background() passes redirect_args to the shell unescaped, so // the admin-set path must be quoted here or it becomes command // injection (issue#7476, forward-port of the release/1.2.31 fix). $safe_log = cacti_escapeshellarg($boost_log); if (CACTI_SERVER_OS == 'unix') { $extra_args = '-q ' . CACTI_PATH_BASE . '/poller_boost.php --debug'; $redirect_args = '>> ' . $safe_log . ' 2>&1'; } else { $extra_args = '-q ' . CACTI_PATH_BASE . '/poller_boost.php --debug'; $redirect_args = '>> ' . $safe_log; } } else { $extra_args = '-q ' . CACTI_PATH_BASE . '/poller_boost.php'; } exec_background($command_string, $extra_args, $redirect_args); } } /** * Updates SNMP statistics for the Cacti Boost system. * * This function gathers information about the Boost table status, including * the number of pending and archived records, table size, and storage engine. * It calculates various statistics such as the total number of records, * average row length, and maximum record length. The gathered data is then * stored in the MIB cache for SNMP monitoring. * * @return void */ function boost_update_snmp_statistics() : void { $mc = new MibCache('CACTI-BOOST-MIB'); // get the boost table status $boost_table_status = db_fetch_assoc("SELECT * FROM information_schema.tables WHERE table_schema = SCHEMA() AND (table_name LIKE 'poller_output_boost_arch_%' OR table_name LIKE 'poller_output_boost')"); $total_data_sources = db_fetch_cell('SELECT COUNT(*) FROM poller_item'); $pending_records = 0; $arch_records = 0; $data_length = 0; $engine = ''; $max_data_length = 0; if (cacti_sizeof($boost_table_status)) { foreach ($boost_table_status as $table) { if ($table['TABLE_NAME'] == 'poller_output_boost') { $pending_records += $table['TABLE_ROWS']; } else { $arch_records += $table['TABLE_ROWS']; } $data_length += $table['DATA_LENGTH']; $data_length -= $table['DATA_FREE']; $engine = $table['ENGINE']; $max_data_length = $table['MAX_DATA_LENGTH']; } } $total_records = $pending_records + $arch_records; $avg_row_length = ($total_records ? intval($data_length / $total_records) : 0); if (strcmp($engine, 'MEMORY') == 0) { $max_length = db_fetch_cell('SELECT MAX(LENGTH(output)) FROM poller_output_boost'); $max_table_allowed = $max_data_length; $max_table_records = ($avg_row_length ? round($max_data_length / $avg_row_length, 0) : 0); } else { $max_length = '0'; $max_table_allowed = '-1'; $max_table_records = '0'; } $mc->object('boostApplStorageDatabaseEngine')->set($engine); $mc->object('boostApplStorageMaxTableSize')->set($max_table_allowed); $mc->object('boostApplStorageMaxRecords')->set($max_table_records); $mc->object('boostApplLastUpdate')->set(time()); $mc->object('boostStatsTotalsRecords')->set($total_records); $mc->object('boostStatsTotalsRecordsPending')->set($pending_records); $mc->object('boostStatsTotalsRecordsArchived')->set($arch_records); $mc->object('boostStatsStorageTableSize')->set($data_length); $mc->object('boostStatsStorageAverageRecordSize')->set($avg_row_length); $mc->object('boostStatsStorageMaxRecordLength')->set($max_length); $mc->object('boostStatsTotalsDataSources')->set($total_data_sources); $mc->object('boostStatsLastUpdate')->set(time()); } /** * Logs debug messages to the output or a log file based on the debug settings. * * @param string $string The message to be logged as a debug message. * * @return void */ function boost_debug(string $string) : void { global $debug, $boost_log, $boost_debug, $child; $string = 'DEBUG: ' . trim($string, " \n"); if ($debug || ($boost_log != '' && $boost_debug)) { print $string . PHP_EOL; } }