mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 19:47:06 +09:00
* PHPStan Level 8 - Phase 1 Conversion
Reduces the real (no-baseline) PHPStan level 8 error count from ~2,724 to 425
and regenerates phpstan-baseline.neon accordingly (8,941 -> 1,921 lines).
Core type-safety changes
- lib/database.php: narrow db_fetch_row()/db_fetch_row_prepared() to return
array (was bool|array) and db_fetch_assoc()/db_fetch_assoc_prepared() to
return array (was mixed); the _prepared workers coerce the connection-failure
path to an empty array so the declared type is honest. empty()/cacti_sizeof()
checks are unaffected.
- lib/database.php: narrow $db_conn guards from is_object() to instanceof PDO in
the transaction/quote/insert-id helpers so the PDO calls resolve.
- lib/mib_cache.php: the fluent MibCache methods mib()/object()/table()/row()
now return self instead of a bare object so chained calls type-check.
Type-safety casts and guards (mechanical, behaviour-preserving)
- Cast string|false/|null builtin results feeding string parameters
(json_decode, base64_encode, trim, htmle, preg_replace, explode, etc.).
- Cast int|string / int|false values feeding int id/pid parameters.
- Guard resource|false fopen()/popen() handles before fwrite/fclose/fread, and
wrap foreach over array|false/list|false results with `?: []`.
Genuine bugs found and fixed
- cli/add_tree.php / lib/api_tree.php: site tree nodes could not be created via
the CLI - the --site-id option was never parsed, $nodeTypes had no 'site'
entry (passing null to api_tree_item_save(int $type) -> TypeError), and
api_tree_item_save() never persisted site_id. All three fixed.
- tree.php: leaves_exist() is typed : int but returned db_fetch_assoc_prepared()
(an array of rows) for a SELECT COUNT(*); now uses db_fetch_cell_prepared()
and returns the scalar count.
- lib/mib_cache.php: $entries_per_object was a float (division / ceil) used as
an array key (PHP 8.1 implicit float-to-int key deprecation); now cast to int.
Config
- .phpstan.neon: add notIdentical.alwaysTrue and booleanAnd.alwaysFalse to the
existing always-true/false ignore family (same "PHPStan gets confused"
dynamic-constant cause already covered by identical.alwaysTrue,
notEqual.alwaysTrue, booleanAnd.leftAlwaysTrue, etc.).
- phpstan-baseline.neon: regenerated; the remaining 425 entries are the
untouched long tail for Phase 2.
* Remove redundant @phpstan-ignore comments
Follow-up cleanup for the level 8 Phase 1 work. Removes 14 inline
@phpstan-ignore comments that no longer suppress any reported error:
- cli/repair_database.php: `@phpstan-ignore booleanAnd.alwaysFalse` is now
covered by the global booleanAnd.alwaysFalse ignore added to .phpstan.neon.
- 13 others (host.php, include/global.php, include/session.php,
lib/api_automation.php x2, lib/functions.php x2, lib/html_utility.php x2,
lib/installer.php, lib/package.php, lib/reports.php, lib/rrd.php) no longer
match any reported error after the type-safety fixes (e.g. the
db_fetch_row_prepared() return-type narrowing removed the offset-assign
errors the api_automation ignores were suppressing).
Verified with reportUnmatchedIgnoredErrors enabled: 0 unmatched ignores
remain and the full level 8 run still reports 0 errors.
* ci: satisfy validator ratchet and refresh security sink baselines
- lib/api_tree.php: give the new tree-item site_id a real numeric
constraint ('^[-0-9]+$', matching html_reports.php) instead of an
empty regex, so the validator constraint ratchet passes without
bumping its baseline.
- tests/security/baselines: refresh the three sink/hotspot signatures
whose code text changed from the (string) casts added in this branch
(lib/poller.php setsid, cli/rrdresize.php json_encode, lib/import.php
simplexml_load_string).
* ci(js): allow DOMPurify 3.4.16 and update sync_js pin test
The dependabot bump (#8134) moved package.json/package-lock.json to
DOMPurify 3.4.16 on develop but left tests/js/sync_js.test.mjs pinned to
3.4.15, so the PR merge commit fails the 'DOMPurify is pinned' contract.
Bring package.json/package-lock.json to 3.4.16 (matching develop) and
update the pin test to expect 3.4.16.
* test: update sql.php null-handling assertion for the (string) cast
This branch changed scripts/sql.php from trim(\ ?? '') to
trim((string) \) (the phpstan-safe form that coerces both null and
false from shell_exec()). Update SqlScriptsTest's null-return assertion
to match the new cast instead of the removed '?? ''.
* fix: address Copilot review - ownership IDs, fopen guards, mib_cache bound
- chown()/chgrp(): pass integer UID/GID from fileowner()/filegroup()
(guarding the false lookup) instead of string-casting them, which made
them resolve account/group names like '1001' and fail. Covers
lib/api_device.php, lib/boost.php and poller_maintenance.php (x2).
- fopen() failure now short-circuits instead of reporting success:
lib/package.php returns false, lib/clog_webapi.php and utilities.php
render an error, and snmpagent_mibcachechild.php skips the cache write
and sentinel unlink so a concurrent writer's lock is preserved.
- lib/mib_cache.php: keep the float quotient as the loop bound and cast
only the array offset, so a non-even entries/columns split no longer
truncates the loop and drops a cached row.
* revert: keep db_fetch_* returning false on error; baseline Level 8 instead
Per review, restoring the intentional fail-safe: db_fetch_row[_prepared]()
return bool|array and db_fetch_assoc[_prepared]() return mixed again, so a
genuine SQL error (not just a missing connection) is signalled as false and
the === false checks in cdef_is_in_use(), boost_process_* and aggregate.php
work as before. Dropped the is_array() ? : [] coercion.
Level 8 stays green by regenerating phpstan-baseline.neon to capture the
re-introduced bool|array/mixed call-site errors (the ~1,850 the narrowing
had eliminated). Tightening those callers to drop them from the baseline is
Phase 2 work.
---------
Co-authored-by: TheWitness <copilot@users.noreply.github.com>
814 lines
28 KiB
PHP
814 lines
28 KiB
PHP
<?php
|
|
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| This program is free software; you can redistribute it and/or |
|
|
| modify it under the terms of the GNU General Public License |
|
|
| as published by the Free Software Foundation; either version 2 |
|
|
| of the License, or (at your option) any later version. |
|
|
| |
|
|
| This program is distributed in the hope that it will be useful, |
|
|
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
| GNU General Public License for more details. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
| This code is designed, written, and maintained by the Cacti Group. See |
|
|
| about.php and/or the AUTHORS file for specific developer information. |
|
|
+-------------------------------------------------------------------------+
|
|
| http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
/**
|
|
* nth_percentile - given a data source, calculate the Nth percentile for a given over a time period
|
|
*
|
|
* @param mixed $local_data_ids The data source array to perform the Nth percentile calculation
|
|
* @param int $start_seconds Start seconds of time range
|
|
* @param int $end_seconds Stop seconds of time range
|
|
* @param int $percentile Nth Percentile to calculate, integer between 1 and 99
|
|
* @param int $resolution The accuracy of the data measured in seconds
|
|
* @param bool $peak Include the peak values in the stats array
|
|
*
|
|
* @return mixed Returns a string, int or float containing the requested percentile
|
|
*/
|
|
function nth_percentile(mixed $local_data_ids, int $start_seconds, int $end_seconds, int $percentile = 95,
|
|
int $resolution = 0, bool $peak = false) : mixed {
|
|
$stats = json_decode(rrdtool_function_stats($local_data_ids, $start_seconds, $end_seconds, $percentile, $resolution, $peak), true);
|
|
|
|
if ($peak) {
|
|
/* rrdtool_function_stats() emits an empty peak when the RRDfile
|
|
* lacks a MAX consolidation function, so treat it as absent and
|
|
* fall back to the AVERAGE based statistics */
|
|
if (array_key_exists('peak', $stats) && cacti_sizeof($stats['peak'])) {
|
|
return $stats['peak'];
|
|
}
|
|
|
|
if (array_key_exists('avg', $stats)) {
|
|
return $stats['avg'];
|
|
} else {
|
|
return $stats;
|
|
}
|
|
} else {
|
|
if (array_key_exists('avg', $stats)) {
|
|
return $stats['avg'];
|
|
}
|
|
|
|
if (array_key_exists('peak', $stats)) {
|
|
return $stats['peak'];
|
|
} else {
|
|
return $stats;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* rrdtool_function_stats - given a data source, calculate a number of statistics for an RRDfile or files
|
|
* over a specified time period
|
|
*
|
|
* @param mixed $local_data_ids The data source array to perform the Nth percentile calculation
|
|
* @param int $start_seconds Start seconds of time range
|
|
* @param int $end_seconds Stop seconds of time range
|
|
* @param int $percentile Nth Percentile to calculate, integer between 1 and 99
|
|
* @param int $resolution The accuracy of the data measured in seconds
|
|
* @param bool $peak Include peak values in stats
|
|
* @param mixed $rrdtool_pipe The RRDtool socket connection if there is one
|
|
*
|
|
* @return string JSON data containing each data source item, and its 95th percentile
|
|
*/
|
|
function rrdtool_function_stats(mixed $local_data_ids, int $start_seconds, int $end_seconds, int $percentile = 95, int $resolution = 0,
|
|
bool $peak = false, mixed $rrdtool_pipe = null) : string {
|
|
include_once(CACTI_PATH_LIBRARY . '/rrd.php');
|
|
|
|
if (!is_array($local_data_ids)) {
|
|
return (string) json_encode([]);
|
|
}
|
|
|
|
// initialize some variables
|
|
$sum_array = [];
|
|
$fetch_array_avg = [];
|
|
$fetch_array_max = [];
|
|
$good_data = false;
|
|
|
|
/* Do a fetch for each data source and discard
|
|
* nth_percentile_maximum and any invalid data sources
|
|
*
|
|
* After this loop is finished, all data will have
|
|
* been retrieved from the RRDfiles.
|
|
*/
|
|
foreach ($local_data_ids as $ldi => $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 '<pre>';print_r($asum_array);print '</pre>';
|
|
|
|
// 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));
|
|
}
|
|
}
|