mirror of
https://github.com/fusionpbx/fusionpbx.git
synced 2025-12-30 00:53:50 +00:00
Add cpu line graph (#7403)
* add cpu line graph * add cpu line graph * add cpu line graph * add cpu line graph * add cpu line graph * Add FreeBSD CPU status * Add new dashboard type Line * Add missing public declaration * Allow new dashboard type Line to be selected option * Use new Line type for graphing instead of doughnut
This commit is contained in:
@@ -52,4 +52,85 @@ class bsd_system_information extends system_information {
|
||||
public function get_uptime() {
|
||||
return shell_exec('uptime');
|
||||
}
|
||||
|
||||
public function get_cpu_percent_per_core(): array {
|
||||
static $last = [];
|
||||
$results = [];
|
||||
|
||||
// Read the raw CPU time ticks from sysctl (returns flat array of cores)
|
||||
$raw = trim(shell_exec('sysctl -n kern.cp_times'));
|
||||
if (!$raw)
|
||||
return [];
|
||||
|
||||
$parts = array_map('intval', preg_split('/\s+/', $raw));
|
||||
$num_cores = count($parts) / 5;
|
||||
|
||||
for ($core = 0; $core < $num_cores; $core++) {
|
||||
$offset = $core * 5;
|
||||
$user = $parts[$offset];
|
||||
$nice = $parts[$offset + 1];
|
||||
$sys = $parts[$offset + 2];
|
||||
$intr = $parts[$offset + 3];
|
||||
$idle = $parts[$offset + 4];
|
||||
|
||||
$total = $user + $nice + $sys + $intr + $idle;
|
||||
|
||||
if (!isset($last[$core])) {
|
||||
$last[$core] = ['total' => $total, 'idle' => $idle];
|
||||
$results[$core] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
$delta_total = $total - $last[$core]['total'];
|
||||
$delta_idle = $idle - $last[$core]['idle'];
|
||||
|
||||
$usage = $delta_total > 0 ? (1 - ($delta_idle / $delta_total)) * 100 : 0;
|
||||
$results[$core] = round($usage, 2);
|
||||
|
||||
$last[$core] = ['total' => $total, 'idle' => $idle];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @staticvar array $last
|
||||
* @param string $interface
|
||||
* @return array
|
||||
* @depends FreeBSD Version 12
|
||||
*/
|
||||
public function get_network_speed(string $interface = 'em0'): array {
|
||||
static $last = [];
|
||||
|
||||
// Run netstat for the interface
|
||||
$output = shell_exec("netstat -bI {$interface} 2>/dev/null");
|
||||
if (!$output)
|
||||
return ['rx_bps' => 0, 'tx_bps' => 0];
|
||||
|
||||
$lines = explode("\n", trim($output));
|
||||
if (count($lines) < 2)
|
||||
return ['rx_bps' => 0, 'tx_bps' => 0];
|
||||
|
||||
$cols = preg_split('/\s+/', $lines[1]);
|
||||
$rx_bytes = (int) $cols[6]; // Ibytes
|
||||
$tx_bytes = (int) $cols[9]; // Obytes
|
||||
$now = microtime(true);
|
||||
|
||||
if (!isset($last[$interface])) {
|
||||
$last[$interface] = ['rx' => $rx_bytes, 'tx' => $tx_bytes, 'time' => $now];
|
||||
return ['rx_bps' => 0, 'tx_bps' => 0];
|
||||
}
|
||||
|
||||
$delta_time = $now - $last[$interface]['time'];
|
||||
$delta_rx = $rx_bytes - $last[$interface]['rx'];
|
||||
$delta_tx = $tx_bytes - $last[$interface]['tx'];
|
||||
|
||||
$last[$interface] = ['rx' => $rx_bytes, 'tx' => $tx_bytes, 'time' => $now];
|
||||
|
||||
return [
|
||||
'rx_bps' => $delta_rx / $delta_time,
|
||||
'tx_bps' => $delta_tx / $delta_time
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ class linux_system_information extends system_information {
|
||||
$core_count = 0;
|
||||
|
||||
foreach ($lines1 as $i => $line1) {
|
||||
if (strpos($line1, 'cpu') !== 0 || $line1 === 'cpu') continue;
|
||||
if (strpos($line1, 'cpu') !== 0 || $line1 === 'cpu')
|
||||
continue;
|
||||
|
||||
$parts1 = preg_split('/\s+/', $line1);
|
||||
$parts2 = preg_split('/\s+/', $lines2[$i]);
|
||||
@@ -75,4 +76,68 @@ class linux_system_information extends system_information {
|
||||
public function get_uptime() {
|
||||
return shell_exec('uptime');
|
||||
}
|
||||
|
||||
public function get_cpu_percent_per_core(): array {
|
||||
static $last = [];
|
||||
|
||||
$lines = file('/proc/stat');
|
||||
$results = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^cpu(\d+)\s+(.+)$/', $line, $matches)) {
|
||||
$core = (int) $matches[1];
|
||||
$parts = preg_split('/\s+/', trim($matches[2]));
|
||||
$total = array_sum($parts);
|
||||
$idle = $parts[3] ?? 0;
|
||||
|
||||
if (!isset($last[$core])) {
|
||||
$last[$core] = ['total' => $total, 'idle' => $idle];
|
||||
$results[$core] = 0;
|
||||
} else {
|
||||
$delta_total = $total - $last[$core]['total'];
|
||||
$delta_idle = $idle - $last[$core]['idle'];
|
||||
$usage = $delta_total > 0 ? (1 - ($delta_idle / $delta_total)) * 100 : 0;
|
||||
|
||||
$results[$core] = round($usage, 2);
|
||||
$last[$core] = ['total' => $total, 'idle' => $idle];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function get_network_speed(string $interface = 'eth0'): array {
|
||||
static $last = [];
|
||||
|
||||
// Read network stats
|
||||
$data = file('/proc/net/dev');
|
||||
foreach ($data as $line) {
|
||||
if (strpos($line, $interface . ':') !== false) {
|
||||
$parts = preg_split('/\s+/', trim(str_replace(':', ' ', $line)));
|
||||
$rx_bytes = (int) $parts[1];
|
||||
$tx_bytes = (int) $parts[9];
|
||||
|
||||
$now = microtime(true);
|
||||
|
||||
if (!isset($last[$interface])) {
|
||||
$last[$interface] = ['rx' => $rx_bytes, 'tx' => $tx_bytes, 'time' => $now];
|
||||
return ['rx_bps' => 0, 'tx_bps' => 0];
|
||||
}
|
||||
|
||||
$delta_time = $now - $last[$interface]['time'];
|
||||
$delta_rx = $rx_bytes - $last[$interface]['rx'];
|
||||
$delta_tx = $tx_bytes - $last[$interface]['tx'];
|
||||
|
||||
$last[$interface] = ['rx' => $rx_bytes, 'tx' => $tx_bytes, 'time' => $now];
|
||||
|
||||
return [
|
||||
'rx_bps' => $delta_rx / $delta_time,
|
||||
'tx_bps' => $delta_tx / $delta_time
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return ['rx_bps' => 0, 'tx_bps' => 0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,28 +87,40 @@ class system_dashboard_service extends base_websocket_system_service {
|
||||
}
|
||||
|
||||
public function on_cpu_status($message = null): void {
|
||||
// Get the CPU status
|
||||
$cpu_percent = self::$system_information->get_cpu_percent();
|
||||
// Get total and per-core CPU usage
|
||||
$cpu_percent_total = self::$system_information->get_cpu_percent();
|
||||
$cpu_percent_per_core = self::$system_information->get_cpu_percent_per_core();
|
||||
|
||||
// Send the response
|
||||
// Prepare response
|
||||
$response = new websocket_message();
|
||||
$response
|
||||
->payload([self::CPU_STATUS_TOPIC => $cpu_percent])
|
||||
->payload([
|
||||
self::CPU_STATUS_TOPIC => [
|
||||
'total' => $cpu_percent_total,
|
||||
'per_core' => array_values($cpu_percent_per_core)
|
||||
]
|
||||
])
|
||||
->service_name(self::get_service_name())
|
||||
->topic(self::CPU_STATUS_TOPIC);
|
||||
|
||||
// Check if we are responding
|
||||
// Include message ID if responding to a request
|
||||
if ($message !== null && $message instanceof websocket_message) {
|
||||
$response->id($message->id());
|
||||
}
|
||||
|
||||
// Log the broadcast
|
||||
$this->debug("Broadcasting CPU percent '$cpu_percent'");
|
||||
// Log for debugging
|
||||
$this->debug(sprintf(
|
||||
"Broadcasting CPU total %.2f%% with %d cores",
|
||||
$cpu_percent_total,
|
||||
count($cpu_percent_per_core)
|
||||
));
|
||||
|
||||
// Send to subscribers
|
||||
// Send the broadcast
|
||||
$this->respond($response);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static function get_service_name(): string {
|
||||
return "dashboard.system.information";
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@ abstract class system_information {
|
||||
abstract public function get_cpu_cores(): int;
|
||||
abstract public function get_uptime();
|
||||
abstract public function get_cpu_percent(): float;
|
||||
abstract public function get_cpu_percent_per_core(): array;
|
||||
abstract public function get_network_speed(string $interface = 'eth0'): array;
|
||||
|
||||
public function get_load_average() {
|
||||
return sys_getloadavg();
|
||||
|
||||
@@ -60,8 +60,7 @@
|
||||
|
||||
subscriber::save_token($token, [system_dashboard_service::get_service_name()]);
|
||||
|
||||
//add half doughnut chart
|
||||
if (!isset($dashboard_chart_type) || $dashboard_chart_type == "doughnut"): ?>
|
||||
if ($dashboard_chart_type === 'line') { ?>
|
||||
<div class='hud_chart' style='width: 175px;'><canvas id='system_cpu_status_chart'></canvas></div>
|
||||
|
||||
<script>
|
||||
@@ -105,7 +104,148 @@
|
||||
|
||||
// Function is called automatically by the websocket_client.js when there is a CPU status update
|
||||
function update_cpu_chart(payload) {
|
||||
let cpu_status = payload.cpu_status;
|
||||
const cores = payload.cpu_status?.per_core;
|
||||
if (!Array.isArray(cores) || cores.length !== num_cores) return;
|
||||
|
||||
const chart = window.system_cpu_status_chart;
|
||||
if (!chart) return;
|
||||
|
||||
// Store into ring buffer
|
||||
cores.forEach((val, i) => {
|
||||
cpu_history[i][cpu_index] = Math.round(val);
|
||||
});
|
||||
|
||||
cpu_index = (cpu_index + 1) % max_points;
|
||||
|
||||
// Rotate each dataset's ring buffer to match chart order
|
||||
chart.data.datasets.forEach((dataset, i) => {
|
||||
const rotated = cpu_history[i].slice(cpu_index).concat(cpu_history[i].slice(0, cpu_index));
|
||||
dataset.data = rotated;
|
||||
});
|
||||
|
||||
chart.update();
|
||||
|
||||
// Optional: update total CPU %
|
||||
const td_cpu_status = document.getElementById('td_system_cpu_status_chart');
|
||||
if (td_cpu_status && payload.cpu_status?.total !== undefined) {
|
||||
td_cpu_status.textContent = `${Math.round(payload.cpu_status.total)}%`;
|
||||
}
|
||||
}
|
||||
|
||||
// Set chart options
|
||||
const max_points = 60;
|
||||
const num_cores = <?= $cpu_cores ?>;
|
||||
|
||||
let cpu_history = Array.from({ length: num_cores }, () => new Array(max_points).fill(null));
|
||||
let cpu_index = 0;
|
||||
|
||||
// Color palette (distinct and visually stacked)
|
||||
const cpu_colors = ['#00bcd4', '#8bc34a', '#ffc107', '#e91e63'];
|
||||
|
||||
// Initialize the chart
|
||||
window.system_cpu_status_chart = new Chart(
|
||||
document.getElementById('system_cpu_status_chart').getContext('2d'),
|
||||
{
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: Array.from({ length: max_points }, (_, i) => i + 1),
|
||||
datasets: Array.from({ length: num_cores }, (_, i) => ({
|
||||
label: `CPU ${i}`,
|
||||
data: [...cpu_history[i]],
|
||||
fill: true,
|
||||
borderColor: cpu_colors[i % cpu_colors.length],
|
||||
backgroundColor: cpu_colors[i % cpu_colors.length],
|
||||
tension: 0.3,
|
||||
pointRadius: 0
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
animation: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
stacked: true,
|
||||
min: 0,
|
||||
max: num_cores * 100,
|
||||
ticks: {
|
||||
stepSize: 100
|
||||
}
|
||||
},
|
||||
x: {
|
||||
ticks: {
|
||||
autoSkip: true,
|
||||
callback: function (val, index) {
|
||||
return (index % 100 === 0 ? ' ' : ' ');
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
drawOnChartArea: false
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
connect_cpu_status_websocket();
|
||||
</script>
|
||||
|
||||
<?php }
|
||||
//add half doughnut chart
|
||||
if (!isset($dashboard_chart_type) || $dashboard_chart_type == "doughnut") { ?>
|
||||
<div class='hud_chart' style='width: 175px;'><canvas id='system_cpu_status_chart'></canvas></div>
|
||||
|
||||
<script>
|
||||
const cpu_status_auth_token = {
|
||||
name: "<?= $token['name']; ?>",
|
||||
hash: "<?= $token['hash']; ?>"
|
||||
}
|
||||
|
||||
const cpu_status_subject = '<?php echo system_dashboard_service::CPU_STATUS_TOPIC; ?>';
|
||||
const dashboard_cpu_usage_chart_main_color = [
|
||||
'<?php echo ($settings->get('theme', 'dashboard_cpu_usage_chart_main_color')[0] ?? '#03c04a'); ?>',
|
||||
'<?php echo ($settings->get('theme', 'dashboard_cpu_usage_chart_main_color')[1] ?? '#ff9933'); ?>',
|
||||
'<?php echo ($settings->get('theme', 'dashboard_cpu_usage_chart_main_color')[2] ?? '#ea4c46'); ?>'
|
||||
];
|
||||
|
||||
function connect_cpu_status_websocket() {
|
||||
client = new ws_client(`wss://${window.location.hostname}/websockets/`, cpu_status_auth_token);
|
||||
client.ws.addEventListener("open", async () => {
|
||||
try {
|
||||
console.log('Connected');
|
||||
console.log('Requesting authentication');
|
||||
|
||||
// Wait until we are authenticated
|
||||
await client.request('authentication');
|
||||
console.log('authenticated');
|
||||
|
||||
// Bind event handler so websocket_client.js can call the function when it
|
||||
// receives the cpu_status event
|
||||
client.onEvent(cpu_status_subject, update_cpu_chart);
|
||||
|
||||
} catch (err) {
|
||||
console.error("WS setup failed: ", err);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
client.ws.addEventListener("close", async () => {
|
||||
console.warn("Websocket Disconnected");
|
||||
});
|
||||
}
|
||||
|
||||
// Function is called automatically by the websocket_client.js when there is a CPU status update
|
||||
function update_cpu_chart(payload) {
|
||||
let cpu_status = Math.round(payload.cpu_status.total);
|
||||
const chart = window.system_cpu_status_chart;
|
||||
|
||||
if (!chart) return;
|
||||
@@ -185,8 +325,7 @@
|
||||
|
||||
connect_cpu_status_websocket();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
<?php }
|
||||
if ($dashboard_chart_type == "number") {
|
||||
echo "<span class='hud_stat'>".round($percent_cpu)."%</span>";
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
if (!progress) return;
|
||||
|
||||
// Update progress bar
|
||||
cpu_percent = Math.round(payload.cpu_status);
|
||||
cpu_percent = Math.round(payload.cpu_status.total);
|
||||
progress.style.width = `${cpu_percent}%`;
|
||||
progress.innerHTML = `${cpu_percent}%`;
|
||||
}
|
||||
|
||||
@@ -648,6 +648,33 @@ $text['label-doughnut']['zh-cn'] = "油炸圈饼";
|
||||
$text['label-doughnut']['ja-jp'] = "ドーナツ";
|
||||
$text['label-doughnut']['ko-kr'] = "도넛";
|
||||
|
||||
$text['label-line']['en-us'] = "Line";
|
||||
$text['label-line']['en-gb'] = "Line";
|
||||
$text['label-line']['ar-eg'] = "خط";
|
||||
$text['label-line']['de-at'] = "Leitung";
|
||||
$text['label-line']['de-ch'] = "Leitung";
|
||||
$text['label-line']['de-de'] = "Leitung";
|
||||
$text['label-line']['el-gr'] = "Γραμμή";
|
||||
$text['label-line']['es-cl'] = "Línea";
|
||||
$text['label-line']['es-mx'] = "Línea";
|
||||
$text['label-line']['fr-ca'] = "Ligne";
|
||||
$text['label-line']['fr-fr'] = "Ligne";
|
||||
$text['label-line']['he-il'] = "קו";
|
||||
$text['label-line']['it-it'] = "Linea";
|
||||
$text['label-line']['ka-ge'] = "ხაზი";
|
||||
$text['label-line']['nl-nl'] = "Lijn";
|
||||
$text['label-line']['pl-pl'] = "Linia";
|
||||
$text['label-line']['pt-br'] = "Linha";
|
||||
$text['label-line']['pt-pt'] = "Linha";
|
||||
$text['label-line']['ro-ro'] = "Linie";
|
||||
$text['label-line']['ru-ru'] = "Линия";
|
||||
$text['label-line']['sv-se'] = "Linje";
|
||||
$text['label-line']['uk-ua'] = "Лінія";
|
||||
$text['label-line']['tr-tr'] = "Hat";
|
||||
$text['label-line']['zh-cn'] = "线路";
|
||||
$text['label-line']['ja-jp'] = "回線";
|
||||
$text['label-line']['ko-kr'] = "회선";
|
||||
|
||||
$text['label-progress_bar']['en-us'] = "Progress Bar";
|
||||
$text['label-progress_bar']['en-gb'] = "Progress Bar";
|
||||
$text['label-progress_bar']['ar-eg'] = "شريط التقدم";
|
||||
|
||||
@@ -828,6 +828,11 @@
|
||||
echo "<td class='vtable' style='position: relative;' align='left'>\n";
|
||||
echo " <select name='dashboard_chart_type' class='formfld'>\n";
|
||||
echo " <option value='doughnut'>".$text['label-doughnut']."</option>\n";
|
||||
if ($dashboard_chart_type === 'line') {
|
||||
echo " <option value='line' selected='selected'>".$text['label-line']."</option>\n";
|
||||
} else {
|
||||
echo " <option value='line'>".$text['label-line']."</option>\n";
|
||||
}
|
||||
if ($dashboard_chart_type == "icon" || in_array($dashboard_path, ['domains/domains', 'xml_cdr/missed_calls', 'voicemails/voicemails', 'xml_cdr/recent_calls', 'registrations/registrations'])) {
|
||||
echo " <option value='icon' ".($dashboard_chart_type == "icon" ? "selected='selected'" : null).">".$text['label-icon']."</option>\n";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user