$data_source_name) { global $user_time, $system_time, $real_time; global $total_user, $total_system, $total_real; // more error checking for invalid data if ($ldi == 0) { continue; } if (empty($total_user)) { $total_user = 0.0; } if (empty($total_system)) { $total_system = 0.0; } if (empty($total_real)) { $total_real = 0.0; } // See if the RRDfile contains the MAX consolidation function, if so prime the array with the fetch data if ($peak) { if (rrdtool_function_contains_cf($ldi, 'MAX', $rrdtool_pipe)) { $total_user += $user_time; $total_system += $system_time; $total_real += $real_time; $fetch_array_max[$ldi] = rrdtool_function_fetch($ldi, $start_seconds, $end_seconds, $resolution, false, '', 'MAX', $rrdtool_pipe); $total_user += $user_time; $total_system += $system_time; $total_real += $real_time; } } // See if the RRDfile contains the AVERAGE consolidation function, if so prime the array with the fetch data if (rrdtool_function_contains_cf($ldi, 'AVERAGE', $rrdtool_pipe)) { $total_user += $user_time; $total_system += $system_time; $total_real += $real_time; $fetch_array_avg[$ldi] = rrdtool_function_fetch($ldi, $start_seconds, $end_seconds, $resolution, false, '', 'AVERAGE', $rrdtool_pipe); $total_user += $user_time; $total_system += $system_time; $total_real += $real_time; } // clean up unwanted data source items from the AVERAGE cf data if (isset($fetch_array_avg[$ldi]) && cacti_sizeof($fetch_array_avg[$ldi])) { if (isset($fetch_array_avg[$ldi]['data_source_names'])) { $good_data = true; } else { unset($fetch_array_avg[$ldi]); continue; } // discard the unused data sources, we will figure it out ourselves foreach ($fetch_array_avg[$ldi]['data_source_names'] as $index => $name) { // clean up DS items that aren't defined on the graph if (!in_array($name, $local_data_ids[$ldi], true)) { if (isset($fetch_array_avg[$ldi]['data_source_names'][$index])) { unset($fetch_array_avg[$ldi]['data_source_names'][$index]); } if (isset($fetch_array_avg[$ldi]['values'][$index])) { unset($fetch_array_avg[$ldi]['values'][$index]); } } } } // clean up unwanted data source items from the MAX cf data if (isset($fetch_array_max[$ldi]) && cacti_sizeof($fetch_array_max[$ldi])) { if (isset($fetch_array_max[$ldi]['data_source_names'])) { $good_data = true; } else { unset($fetch_array_max[$ldi]); continue; } // discard the unused data sources, we will figure it out ourselves foreach ($fetch_array_max[$ldi]['data_source_names'] as $index => $name) { // clean up DS items that aren't defined on the graph if (!in_array($name, $local_data_ids[$ldi], true)) { if (isset($fetch_array_max[$ldi]['data_source_names'][$index])) { unset($fetch_array_max[$ldi]['data_source_names'][$index]); } if (isset($fetch_array_max[$ldi]['values'][$index])) { unset($fetch_array_max[$ldi]['values'][$index]); } } } } } /* Do a sanity check, return right away if we don't have * good data. Else prepare a new array with summary data. */ if (!$good_data) { return (string) json_encode([]); } $stats = []; if (cacti_sizeof($fetch_array_avg)) { $stats['avg'] = nth_percentile_fetch_statistics($percentile, $local_data_ids, $fetch_array_avg, 'AVERAGE'); } if ($peak && cacti_sizeof($fetch_array_max)) { $stats['peak'] = nth_percentile_fetch_statistics($percentile, $local_data_ids, $fetch_array_max, 'MAX'); } else { $stats['peak'] = []; } return (string) json_encode($stats); } function nth_percentile_fetch_statistics(int $percentile, array &$local_data_ids, array &$fetch_array, string $cf) : array { // start by summing the data across local data ids, for the average cf $asum_array = []; foreach ($local_data_ids as $ldi => $data_source_name) { if (cacti_sizeof($fetch_array[$ldi]['data_source_names'])) { foreach ($fetch_array[$ldi]['data_source_names'] as $index => $ds_name) { if (isset($fetch_array[$ldi]['values'][$index]) && cacti_sizeof($fetch_array[$ldi]['values'][$index])) { foreach ($fetch_array[$ldi]['values'][$index] as $timestamp => $data) { // rrdtool emits 'U'/NaN for gaps; skip them so they do not poison the sum if (!is_numeric($data) || !is_finite((float) $data)) { continue; } if (isset($asum_array[$ds_name]) && isset($asum_array[$ds_name][$timestamp])) { $asum_array[$ds_name][$timestamp] += $data; } else { $asum_array[$ds_name][$timestamp] = $data; } } } } } } // print '
';print_r($asum_array);print '
'; // next get the max values of all the data sources $max_values_array = []; if (cacti_sizeof($asum_array)) { foreach ($asum_array as $sum_by_timestamp) { foreach ($sum_by_timestamp as $timestamp => $data) { if (!isset($max_values_array[$timestamp])) { $max_values_array[$timestamp] = $data; } else { $max_values_array[$timestamp] = max($data, $max_values_array[$timestamp]); } } } } // store some known information for legacy cacti behavior $asum_array['nth_percentile_maximum'] = $max_values_array; // get the sum data now across all data sources $sum_values_array = []; if (cacti_sizeof($asum_array)) { foreach ($asum_array as $ds_name => $sum_by_timestamp) { if ($ds_name == 'nth_percentile_maximum') { continue; } foreach ($sum_by_timestamp as $timestamp => $data) { if (!isset($sum_values_array[$timestamp])) { $sum_values_array[$timestamp] = $data; } else { $sum_values_array[$timestamp] += $data; } } } } // store some known information for legacy cacti behavior $asum_array['nth_percentile_sum'] = $sum_values_array; // get some nice analytical statistics about the data $stats = []; $agg_total = 0; foreach ($asum_array as $ds_name => $data_by_timestamp) { $cstats['stats_' . $ds_name] = cacti_stats_calc($data_by_timestamp, $percentile); $stats[$ds_name] = $cstats['stats_' . $ds_name]['p' . $percentile . 'n']; // scan all non built-in data sources for aggregate total data if ($ds_name != 'nth_percentile_sum' && $ds_name != 'nth_percentile_maximum') { if ($agg_total < $cstats['stats_' . $ds_name]['p' . $percentile . 'n']) { $agg_total = $cstats['stats_' . $ds_name]['p' . $percentile . 'n']; } } } // store some known information for legacy cacti behavior $stats['nth_percentile_aggregate_total'] = $agg_total; $stats += $cstats; return $stats; } function cacti_percentile_index(int $elements, int $percentile) : int { if ($elements <= 0 || $percentile <= 0 || $percentile >= 100) { return 0; } // Values are sorted descending. rrdtool's VDEF PERCENT selects the ascending // rank round(percentile/100 * N), so the matching zero-based descending index // is N - round(percentile/100 * N). PHP round() is half-away-from-zero, which // matches rrdtool's C round() on the .5 ties. Verified against rrdtool for // 28/29/30/31-day months (N = 8064/8352/8640/8928). $index = $elements - (int) round($elements * $percentile / 100); return max(0, min($elements - 1, $index)); } function cacti_stats_calc(array $array, int $ptile = 95) : array { rsort($array, SORT_NUMERIC); $elements = cacti_sizeof($array); if ($elements == 0) { $results = [ 'p95n' => 0, 'p90n' => 0, 'p75n' => 0, 'p50n' => 0, 'p25n' => 0, 'average' => 0, 'peak' => 0, 'sum' => 0, 'elements' => 0, 'variance' => 0, 'stddev' => 0 ]; $results['p' . $ptile . 'n'] = 0; return $results; } $rsquared = 0; $sum = array_sum($array); $peak = max($array); $average = $sum / $elements; $var = 'p' . $ptile . 'n'; if ($var == 'p95n') { $var = ''; } foreach ($array as $number) { $rsquared += ($number - $average) ** 2; } $variance = $rsquared / $elements; $ptile_index = cacti_percentile_index($elements, $ptile); $p95n_index = cacti_percentile_index($elements, 95); $p90n_index = cacti_percentile_index($elements, 90); $p75n_index = cacti_percentile_index($elements, 75); $p50n_index = cacti_percentile_index($elements, 50); $p25n_index = cacti_percentile_index($elements, 25); $results = [ 'p95n' => $array[$p95n_index] ?? 0, 'p90n' => $array[$p90n_index] ?? 0, 'p75n' => $array[$p75n_index] ?? 0, 'p50n' => $array[$p50n_index] ?? 0, 'p25n' => $array[$p25n_index] ?? 0, 'average' => $average, 'peak' => $peak, 'sum' => $sum, 'elements' => $elements, 'variance' => $variance, 'stddev' => sqrt($rsquared / $elements) ]; if ($var != '') { $results[$var] = $array[$ptile_index] ?? 0; } return $results; } /** * bandwidth_summation - given a data source, sums all data in the rrd for a given * time period * * @param int $local_data_id The data source to perform the summation for * @param int $start_time The start time to use for the data calculation. this value can * either be absolute (unix timestamp) or relative (to now) * @param int $end_time The end time to use for the data calculation. this value can * either be absolute (unix timestamp) or relative (to now) * @param int $rra_steps The accuracy of the data measured in seconds * @param int $ds_steps How many periods each sample in the RRA counts for, values above '1' * result in an averaged summation * * @return array An array containing each data source item, and its sum */ function bandwidth_summation(int $local_data_id, int $start_time, int $end_time, int $rra_steps, int $ds_steps) : array { $fetch_array = @rrdtool_function_fetch($local_data_id, $start_time, $end_time, $rra_steps * $ds_steps); $return_array = []; if (!isset($fetch_array['data_source_names']) || cacti_count($fetch_array['data_source_names']) == 0) { return $return_array; } // loop through each regexp determined above (or each data source) for ($i = 0; $i < cacti_count($fetch_array['data_source_names']); $i++) { if (isset($fetch_array['values'][$i])) { $sum = array_sum($fetch_array['values'][$i]); if (cacti_count($fetch_array['values'][$i]) > 0) { $requested_step = $ds_steps * $rra_steps; $effective_step = $fetch_array['timestamp']['step'] ?? 0; $sum *= $effective_step > 0 ? $effective_step : $requested_step; } else { $sum = 0; } // collect summation values in this array so we can return them $return_array[$fetch_array['data_source_names'][$i]] = $sum; } } return $return_array; } function is_graphable_item(string $item) : bool { if (preg_match('/(AREA|STACK|LINE[123])/', $item)) { return true; } else { return false; } } /** * variable_nth_percentile - given a Nth percentile variable, calculate the Nth percentile * and format it for display on the graph * * @param array $regexp_match_array The array that contains each argument in the Nth percentile variable. it * should be formatted like so: * $arr[0] // full variable string * $arr[1] // Nth percentile * $arr[2] // bits or bytes * $arr[3] // power of 10 divisor * $arr[4] // current, total, max, total_peak, all_max_current, all_max_peak * $arr[5] // digits of floating point precision * @param array $graph An array that contains the current graph data * @param array $graph_item An array that contains the current graph item * @param array $graph_items An array that contains all graph items * @param int $graph_start The start time to use for the data calculation. this value can * either be absolute (unix timestamp) or relative (to now) * @param int $graph_end The end time to use for the data calculation. this value can * either be absolute (unix timestamp) or relative (to now) * @param int $resolution Effective RRA resolution selected for the graph * * @return string - a string containing the Nth percentile suitable for placing on the graph */ function variable_nth_percentile(array &$regexp_match_array, array &$graph, array &$graph_item, array &$graph_items, int $graph_start, int $graph_end, int $resolution = 0) : string { global $graph_item_types; $nth_cache = []; $local_data_array = []; if (cacti_sizeof($regexp_match_array) == 0) { return '0'; } if ($graph['base_value'] == 1024) { /* the divisor must be 1024^(power/3), the binary analogue of 10^power; dividing by 10.24^power printed values about 1.024^(2*power/3) too low (#7211) */ $base = 2; $exp_scale = 10 / 3; } else { $base = 10; $exp_scale = 1; } // Convert the regex matches to human readable // Unexploded format is |95:bits:6:max:2| $percentile = $regexp_match_array[1]; $bytebit = $regexp_match_array[2]; $power = $regexp_match_array[3]; $type = $regexp_match_array[4]; // determine the floating point precision if (is_numeric($regexp_match_array[5])) { $round_to = $regexp_match_array[5]; } else { $round_to = 2; } // error Nth Percentile variable is incorrect if ($percentile < 1 || $percentile > 99) { return '-1'; } if (empty($graph_item['local_data_id'])) { $graph_item['local_data_id'] = 0; } $gi = []; if (cacti_sizeof($graph_items)) { foreach ($graph_items as $item) { if ($item['local_data_id'] > 0 && $item['data_source_name'] != '') { if (!empty($item['data_template_rrd_id']) && !empty($item['local_data_id']) && is_graphable_item($graph_item_types[$item['graph_type_id']])) { $gi[$item['data_source_name'] . '|||' . $item['local_data_id']] = true; } } } foreach ($gi as $data_source => $true) { [$data_source_name, $local_data_id] = explode('|||', $data_source); $local_data_array[$local_data_id][] = $data_source_name; } } // Get the Nth percentile values if (!cacti_sizeof($nth_cache)) { switch ($type) { case 'current': // Query data for the individual case $local_data_array = array_intersect_key($local_data_array, array_flip([$graph_item['local_data_id']])); $nth_cache = nth_percentile($local_data_array, $graph_start, $graph_end, $percentile, $resolution); break; case 'max': // Query data for the individual case $local_data_array = array_intersect_key($local_data_array, array_flip([$graph_item['local_data_id']])); $nth_cache = nth_percentile($local_data_array, $graph_start, $graph_end, $percentile, $resolution, true); break; case 'total': case 'all_max_current': if (cacti_sizeof($local_data_array)) { $nth_cache = nth_percentile($local_data_array, $graph_start, $graph_end, $percentile, $resolution); } break; case 'total_peak': case 'all_max_peak': if (cacti_sizeof($local_data_array)) { $nth_cache = nth_percentile($local_data_array, $graph_start, $graph_end, $percentile, $resolution, true); } break; case 'aggregate': case 'aggregate_sum': if (cacti_sizeof($local_data_array)) { $nth_cache = nth_percentile($local_data_array, $graph_start, $graph_end, $percentile, $resolution); } break; case 'aggregate_peak': case 'aggregate_max': case 'aggregate_sum_peak': if (cacti_sizeof($local_data_array)) { $nth_cache = nth_percentile($local_data_array, $graph_start, $graph_end, $percentile, $resolution, true); } break; case 'aggregate_current': case 'aggregate_current_peak': $local_data_array = []; if ($graph_item['data_source_name'] != '') { foreach ($graph_items as $graph_element) { if ($graph_item['data_source_name'] == $graph_element['data_source_name'] && !empty($graph_element['data_template_rrd_id']) && !empty($graph_element['local_data_id']) && is_graphable_item($graph_item_types[$graph_element['graph_type_id']])) { $local_data_array[$graph_element['local_data_id']][] = $graph_element['data_source_name']; } } if ($type == 'aggregate_current') { if (cacti_sizeof($local_data_array)) { $nth_cache = nth_percentile($local_data_array, $graph_start, $graph_end, $percentile, $resolution); } } else { if (cacti_sizeof($local_data_array)) { $nth_cache = nth_percentile($local_data_array, $graph_start, $graph_end, $percentile, $resolution, true); } } } break; } } $nth = 0; // format the output according to args passed to the variable switch($type) { case 'current': // Total of current data source from AVERAGE or MAX consolidation function if (!empty($nth_cache[$graph_item['data_source_name']])) { $nth = $nth_cache[$graph_item['data_source_name']]; $nth = ($bytebit == 'bits') ? $nth * 8 : $nth; $nth /= $base ** ($power * $exp_scale); } break; case 'total': // Total of the current data source name case 'total_peak': case 'aggregate_sum': case 'aggregate_sum_peak': if (!empty($nth_cache['nth_percentile_sum'])) { $nth = $nth_cache['nth_percentile_sum']; $nth = ($bytebit == 'bits') ? $nth * 8 : $nth; $nth /= $base ** ($power * $exp_scale); } break; case 'all_max_current': // Max of all data sources case 'all_max_peak': case 'aggregate_max': case 'aggregate_peak': case 'aggregate_current_peak': case 'max': if (!empty($nth_cache['nth_percentile_maximum'])) { $nth = $nth_cache['nth_percentile_maximum']; $nth = ($bytebit == 'bits') ? $nth * 8 : $nth; $nth /= $base ** ($power * $exp_scale); } break; case 'aggregate': case 'aggregate_current': if (!empty($nth_cache['nth_percentile_aggregate_total'])) { $nth = $nth_cache['nth_percentile_aggregate_total']; $nth = ($bytebit == 'bits') ? $nth * 8 : $nth; $nth /= $base ** ($power * $exp_scale); } break; } return sprintf('%s', round($nth, $round_to)); } /** * variable_bandwidth_summation - given a bandwidth summation variable, calculate the summation * and format it for display on the graph * * @param array $regexp_match_array The array that contains each argument in the bandwidth summation variable. it * should be formatted like so: * $arr[0] // full variable string * $arr[1] // power of 10 divisor or 'auto' * $arr[2] // current, total * $arr[3] // digits of floating point precision * $arr[4] // seconds to perform the calculation for or 'auto' * @param array $graph An array that contains the current graph data * @param array $graph_item An array that contains the current graph item * @param array $graph_items An array that contains all graph items * @param int $graph_start The start time to use for the data calculation. this value can * either be absolute (unix timestamp) or relative (to now) * @param int $graph_end The end time to use for the data calculation. this value can * either be absolute (unix timestamp) or relative (to now) * @param int $rra_step How many periods each sample in the RRA counts for, values * above '1' result in an averaged summation * @param int $ds_step How many seconds each period represents * * @return string A string containing the bandwidth summation suitable for placing on the graph */ function variable_bandwidth_summation(array &$regexp_match_array, array &$graph, array &$graph_item, array &$graph_items, int $graph_start, int $graph_end, int $rra_step, int $ds_step) : string { global $graph_item_types; if (cacti_sizeof($regexp_match_array) == 0) { return '0'; } if (empty($graph_item['local_data_id'])) { $graph_item['local_data_id'] = 0; $regexp_match_array[2] = 'total'; } if ($graph['base_value'] == 1024) { /* the divisor must be 1024^(power/3), the binary analogue of 10^power; the auto path below already uses true powers of 1024 */ $base = 2; $exp_scale = 10 / 3; } else { $base = 10; $exp_scale = 1; } if (is_numeric($regexp_match_array[4])) { $summation_timespan_start = -$regexp_match_array[4]; } else { $summation_timespan_start = $graph_start; } $summation_cache = []; switch($regexp_match_array[2]) { case 'current': $summation_cache[$graph_item['local_data_id']] = bandwidth_summation($graph_item['local_data_id'], (int) $summation_timespan_start, $graph_end, $rra_step, $ds_step); break; case 'total': foreach ($graph_items as $graph_element) { if (!empty($graph_element['data_template_rrd_id']) && !empty($graph_element['local_data_id']) && is_graphable_item($graph_item_types[$graph_element['graph_type_id']])) { $summation_cache[$graph_element['local_data_id']] = bandwidth_summation($graph_element['local_data_id'], (int) $summation_timespan_start, $graph_end, $rra_step, $ds_step); } } break; case 'atomic': $summation_cache[$graph_item['local_data_id']] = bandwidth_summation($graph_item['local_data_id'], (int) $summation_timespan_start, $graph_end, $rra_step, 1); break; } $summation = 0; // format the output according to args passed to the variable switch ($regexp_match_array[2]) { case 'current': case 'atomic': if (isset($summation_cache[$graph_item['local_data_id']][$graph_item['data_source_name']])) { $summation = $summation_cache[$graph_item['local_data_id']][$graph_item['data_source_name']]; } break; case 'total': foreach ($graph_items as $graph_element) { if (!empty($graph_element['data_template_rrd_id']) && !empty($graph_element['local_data_id']) && isset($summation_cache[$graph_element['local_data_id']][$graph_element['data_source_name']]) && is_graphable_item($graph_item_types[$graph_element['graph_type_id']])) { $summation += $summation_cache[$graph_element['local_data_id']][$graph_element['data_source_name']]; } } break; } if (preg_match('/\d+/', $regexp_match_array[1])) { $summation /= $base ** ($regexp_match_array[1] * $exp_scale); } elseif ($regexp_match_array[1] == 'auto') { if ($graph['base_value'] == 1000) { if ($summation < 1000) { $summation_label = 'B'; } elseif ($summation < 1000000) { $summation_label = 'KB'; $summation /= 1000; } elseif ($summation < 1000000000) { $summation_label = 'MB'; $summation /= 1000000; } elseif ($summation < 1000000000000) { $summation_label = 'GB'; $summation /= 1000000000; } else { $summation_label = 'TB'; $summation /= 1000000000000; } } else { if ($summation < 1024) { $summation_label = 'iB'; } elseif ($summation < 1048576) { $summation_label = 'KiB'; $summation /= 1024; } elseif ($summation < 1073741824) { $summation_label = 'MiB'; $summation /= 1048576; } elseif ($summation < 1099511627776) { $summation_label = 'GiB'; $summation /= 1073741824; } else { $summation_label = 'TiB'; $summation /= 1099511627776; } } } // determine the floating point precision if (is_numeric($regexp_match_array[3])) { $round_to = $regexp_match_array[3]; } else { $round_to = 2; } // substitute in the final result and round off to two decimal digits if (isset($summation_label)) { return sprintf('%10s', number_format_i18n($summation, $round_to) . " $summation_label"); } else { return sprintf('%10s', number_format_i18n($summation, $round_to)); } }