404 lines
16 KiB
PHP
404 lines
16 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App;
|
|
|
|
use PDO;
|
|
use Throwable;
|
|
|
|
require_once __DIR__ . '/FirewallManager.php';
|
|
require_once __DIR__ . '/AmiClient.php';
|
|
|
|
class SystemMetrics
|
|
{
|
|
private const CACHE_FILE = __DIR__ . '/../storage/metrics_prev.json';
|
|
|
|
public static function collect(PDO $db): array
|
|
{
|
|
$now = microtime(true);
|
|
$prev = self::loadPrevSample();
|
|
|
|
// 1. CPU (/proc/stat + /proc/loadavg + /proc/cpuinfo)
|
|
$cpuStat = self::readCpuStat();
|
|
$cpuPercent = 0.0;
|
|
if (isset($prev['cpu'])) {
|
|
$dTotal = $cpuStat['total'] - $prev['cpu']['total'];
|
|
$dIdle = $cpuStat['idle'] - $prev['cpu']['idle'];
|
|
if ($dTotal > 0) {
|
|
$cpuPercent = round(max(0.0, min(100.0, (1.0 - ($dIdle / $dTotal)) * 100.0)), 1);
|
|
}
|
|
}
|
|
|
|
$loadAvg = sys_getloadavg() ?: [0.0, 0.0, 0.0];
|
|
$cpuCores = 1;
|
|
$cpuModel = 'Processeur Debian';
|
|
if (is_readable('/proc/cpuinfo')) {
|
|
$cpuinfo = (string)file_get_contents('/proc/cpuinfo');
|
|
$cpuCores = max(1, substr_count($cpuinfo, 'processor'));
|
|
if (preg_match('/model name\s*:\s*(.+)/i', $cpuinfo, $m)) {
|
|
$cpuModel = trim($m[1]);
|
|
}
|
|
}
|
|
|
|
// 2. Mémoire RAM & Swap (/proc/meminfo)
|
|
$mem = self::readMemInfo();
|
|
|
|
// 3. Disque HDD/SSD (/)
|
|
$diskTotal = (float)(@disk_total_space('/') ?: 1);
|
|
$diskFree = (float)(@disk_free_space('/') ?: 0);
|
|
$diskUsed = max(0.0, $diskTotal - $diskFree);
|
|
$diskPct = round(($diskUsed / $diskTotal) * 100.0, 1);
|
|
|
|
// 4. Réseau (/proc/net/dev)
|
|
$net = self::readNetDev();
|
|
$rxRateKbps = 0.0;
|
|
$txRateKbps = 0.0;
|
|
if (isset($prev['net'], $prev['timestamp']) && ($now - $prev['timestamp']) > 0.2) {
|
|
$dt = $now - $prev['timestamp'];
|
|
$dRx = max(0, $net['rx_bytes'] - $prev['net']['rx_bytes']);
|
|
$dTx = max(0, $net['tx_bytes'] - $prev['net']['tx_bytes']);
|
|
// En Ko/s (KB/s)
|
|
$rxRateKbps = round(($dRx / $dt) / 1024, 2);
|
|
$txRateKbps = round(($dTx / $dt) / 1024, 2);
|
|
}
|
|
|
|
// Sauvegarde de l'échantillon pour le calcul différentiel CPU & Réseau
|
|
self::savePrevSample([
|
|
'timestamp' => $now,
|
|
'cpu' => $cpuStat,
|
|
'net' => $net,
|
|
]);
|
|
|
|
// 5. Uptime Debian (/proc/uptime)
|
|
$uptimeSeconds = 0;
|
|
if (is_readable('/proc/uptime')) {
|
|
$parts = explode(' ', trim((string)file_get_contents('/proc/uptime')));
|
|
$uptimeSeconds = (int)floor((float)($parts[0] ?? 0));
|
|
}
|
|
|
|
// 6. Version Debian & État temps réel Asterisk via AMI (Postes enregistrés, latence RTT, appels actifs)
|
|
$debianVer = is_readable('/etc/debian_version') ? trim((string)file_get_contents('/etc/debian_version')) : '13';
|
|
$asteriskInfo = self::readAsteriskLiveStatus($db);
|
|
|
|
// 7. Statut des Services Debian & Fail2ban / Pare-feu
|
|
$fwStatus = FirewallManager::getStatus();
|
|
$services = self::readServicesStatus($asteriskInfo, $fwStatus);
|
|
|
|
return [
|
|
'timestamp' => date('H:i:s'),
|
|
'hostname' => gethostname() ?: 'debian-pbx',
|
|
'os_version' => "Debian {$debianVer}",
|
|
'uptime' => self::formatDuration($uptimeSeconds),
|
|
'cpu' => [
|
|
'percent' => $cpuPercent,
|
|
'cores' => $cpuCores,
|
|
'model' => $cpuModel,
|
|
'load_1' => round((float)$loadAvg[0], 2),
|
|
'load_5' => round((float)$loadAvg[1], 2),
|
|
'load_15' => round((float)$loadAvg[2], 2),
|
|
],
|
|
'memory' => $mem,
|
|
'disk' => [
|
|
'total_gb' => round($diskTotal / 1073741824, 2),
|
|
'used_gb' => round($diskUsed / 1073741824, 2),
|
|
'free_gb' => round($diskFree / 1073741824, 2),
|
|
'percent' => $diskPct,
|
|
],
|
|
'network' => [
|
|
'interface' => $net['interface'],
|
|
'ip' => $net['ip'],
|
|
'rx_kbps' => $rxRateKbps,
|
|
'tx_kbps' => $txRateKbps,
|
|
'rx_total_mb' => round($net['rx_bytes'] / 1048576, 1),
|
|
'tx_total_mb' => round($net['tx_bytes'] / 1048576, 1),
|
|
],
|
|
'asterisk' => $asteriskInfo,
|
|
'services' => $services,
|
|
'security' => [
|
|
'firewall_enabled' => (bool)($fwStatus['settings']['firewall_enabled'] ?? false),
|
|
'fail2ban_running' => (bool)($fwStatus['is_running'] ?? false),
|
|
'admin_port' => (int)($fwStatus['settings']['admin_https_port'] ?? 8443),
|
|
'strict_port80' => (bool)($fwStatus['settings']['certbot_strict_port80'] ?? false),
|
|
'total_banned' => (int)($fwStatus['total_banned'] ?? 0),
|
|
'banned_ips' => $fwStatus['banned_ips'] ?? [],
|
|
'jails' => $fwStatus['jails'] ?? [],
|
|
],
|
|
];
|
|
}
|
|
|
|
private static function readServicesStatus(array $astInfo, array $fwStatus): array
|
|
{
|
|
$out = trim((string)@shell_exec('systemctl is-active asterisk nginx php8.4-fpm fail2ban 2>/dev/null'));
|
|
$lines = preg_split('/\r?\n/', $out) ?: [];
|
|
|
|
$astActive = ($lines[0] ?? '') === 'active' || ($astInfo['uptime'] ?? '') !== 'Hors-ligne';
|
|
$nginxActive = ($lines[1] ?? '') === 'active';
|
|
$phpActive = ($lines[2] ?? '') === 'active';
|
|
$f2bActive = ($lines[3] ?? '') === 'active' || !empty($fwStatus['is_running']);
|
|
$fwEnabled = !empty($fwStatus['settings']['firewall_enabled']) && $nginxActive;
|
|
|
|
$adminPort = $fwStatus['settings']['admin_https_port'] ?? 8443;
|
|
$isStrictPort80 = !empty($fwStatus['settings']['certbot_strict_port80']);
|
|
|
|
return [
|
|
[
|
|
'id' => 'asterisk',
|
|
'name' => 'Asterisk 22 PBX',
|
|
'desc' => 'Moteur SIP PJSIP & Dialplan',
|
|
'port' => '5060 / 5061',
|
|
'active' => $astActive,
|
|
'status_text' => $astActive ? 'Opérationnel' : 'Arrêté',
|
|
'uptime' => $astInfo['uptime'] ?? 'Actif',
|
|
],
|
|
[
|
|
'id' => 'nginx',
|
|
'name' => 'Serveur Web Nginx',
|
|
'desc' => 'Portail GUI & Reverse Proxy',
|
|
'port' => "80 / 443 / {$adminPort}",
|
|
'active' => $nginxActive,
|
|
'status_text' => $nginxActive ? 'Opérationnel' : 'Arrêté',
|
|
'uptime' => 'En ligne',
|
|
],
|
|
[
|
|
'id' => 'php',
|
|
'name' => 'PHP 8.4 FPM',
|
|
'desc' => 'API FastCGI & SQLite',
|
|
'port' => 'Unix Socket',
|
|
'active' => $phpActive,
|
|
'status_text' => $phpActive ? 'Opérationnel' : 'Arrêté',
|
|
'uptime' => 'En ligne',
|
|
],
|
|
[
|
|
'id' => 'fail2ban',
|
|
'name' => 'Fail2ban IDS / IPS',
|
|
'desc' => 'Jails [asterisk, web, ssh]',
|
|
'port' => 'Surveillance logs',
|
|
'active' => $f2bActive,
|
|
'status_text' => $f2bActive ? 'En surveillance' : 'Arrêté',
|
|
'uptime' => $f2bActive ? 'Actif' : 'Arrêté',
|
|
],
|
|
[
|
|
'id' => 'firewall',
|
|
'name' => 'Pare-feu & Filtrage',
|
|
'desc' => $fwEnabled ? ($isStrictPort80 ? 'Mode Strict (Port 80 Isolé)' : 'Filtrage Nginx & Ports Actif') : 'Inactif (Tout autoriser)',
|
|
'port' => 'Filtrage IP & Nginx',
|
|
'active' => $fwEnabled,
|
|
'status_text' => $fwEnabled ? 'Filtrage Actif' : 'Inactif',
|
|
'uptime' => $fwEnabled ? 'Actif' : 'Inactif',
|
|
],
|
|
];
|
|
}
|
|
|
|
private static function readCpuStat(): array
|
|
{
|
|
if (!is_readable('/proc/stat')) {
|
|
return ['total' => 0, 'idle' => 0];
|
|
}
|
|
$firstLine = fgets(fopen('/proc/stat', 'r')) ?: '';
|
|
$parts = preg_split('/\s+/', trim($firstLine)) ?: [];
|
|
array_shift($parts); // retire "cpu"
|
|
$vals = array_map('intval', $parts);
|
|
$idle = ($vals[3] ?? 0) + ($vals[4] ?? 0); // idle + iowait
|
|
$total = array_sum($vals);
|
|
return ['total' => $total, 'idle' => $idle];
|
|
}
|
|
|
|
private static function readMemInfo(): array
|
|
{
|
|
$data = [];
|
|
if (is_readable('/proc/meminfo')) {
|
|
foreach (file('/proc/meminfo') ?: [] as $line) {
|
|
if (preg_match('/^(\w+):\s+(\d+)/', $line, $m)) {
|
|
$data[$m[1]] = (int)$m[2]; // en kB
|
|
}
|
|
}
|
|
}
|
|
$totalKb = max(1, $data['MemTotal'] ?? 1);
|
|
$availKb = $data['MemAvailable'] ?? (($data['MemFree'] ?? 0) + ($data['Buffers'] ?? 0) + ($data['Cached'] ?? 0));
|
|
$usedKb = max(0, $totalKb - $availKb);
|
|
$swapTotalKb = $data['SwapTotal'] ?? 0;
|
|
$swapFreeKb = $data['SwapFree'] ?? 0;
|
|
$swapUsedKb = max(0, $swapTotalKb - $swapFreeKb);
|
|
|
|
return [
|
|
'total_mb' => (int)round($totalKb / 1024),
|
|
'used_mb' => (int)round($usedKb / 1024),
|
|
'free_mb' => (int)round($availKb / 1024),
|
|
'percent' => round(($usedKb / $totalKb) * 100.0, 1),
|
|
'swap_total_mb' => (int)round($swapTotalKb / 1024),
|
|
'swap_used_mb' => (int)round($swapUsedKb / 1024),
|
|
];
|
|
}
|
|
|
|
private static function readNetDev(): array
|
|
{
|
|
$iface = 'eth0';
|
|
$rxBytes = 0;
|
|
$txBytes = 0;
|
|
|
|
if (is_readable('/proc/net/dev')) {
|
|
$lines = file('/proc/net/dev') ?: [];
|
|
foreach ($lines as $line) {
|
|
if (!str_contains($line, ':')) {
|
|
continue;
|
|
}
|
|
[$name, $rest] = explode(':', $line, 2);
|
|
$name = trim($name);
|
|
if ($name === 'lo') {
|
|
continue;
|
|
}
|
|
$cols = preg_split('/\s+/', trim($rest)) ?: [];
|
|
$iface = $name;
|
|
$rxBytes = (int)($cols[0] ?? 0);
|
|
$txBytes = (int)($cols[8] ?? 0);
|
|
break;
|
|
}
|
|
}
|
|
|
|
$ip = $_SERVER['SERVER_ADDR'] ?? '';
|
|
if ($ip === '' || $ip === '127.0.0.1') {
|
|
$out = @shell_exec("ip -4 -o addr show dev " . escapeshellarg($iface) . " 2>/dev/null");
|
|
if (is_string($out) && preg_match('/inet\s+([0-9.]+)/', $out, $m)) {
|
|
$ip = $m[1];
|
|
}
|
|
}
|
|
|
|
return [
|
|
'interface' => $iface,
|
|
'ip' => $ip ?: '127.0.0.1',
|
|
'rx_bytes' => $rxBytes,
|
|
'tx_bytes' => $txBytes,
|
|
];
|
|
}
|
|
|
|
private static function readAsteriskLiveStatus(PDO $db): array
|
|
{
|
|
$contacts = [];
|
|
$activeCalls = 0;
|
|
$astUptime = 'Actif';
|
|
$astVersion = 'Asterisk 22 • PJSIP';
|
|
$onlineCount = 0;
|
|
|
|
try {
|
|
$ami = AmiClient::fromDatabase($db);
|
|
$rawVer = $ami->runCommand('core show version');
|
|
if (preg_match('/Asterisk\s+([0-9]+\.[0-9]+(?:\.[0-9]+)?)/i', $rawVer, $vm)) {
|
|
$astVersion = "Asterisk {$vm[1]} • PJSIP";
|
|
}
|
|
$rawContacts = $ami->runCommand('pjsip show contacts');
|
|
$astDbRaw = $ami->runCommand('database show registrar/contact');
|
|
$contactMeta = [];
|
|
foreach (preg_split('/\r?\n/', $astDbRaw) ?: [] as $dbLine) {
|
|
if (preg_match('/^\/registrar\/contact\/([^;]+);@[a-f0-9]+:\s*(\{.*\})/i', $dbLine, $dbM)) {
|
|
$extKey = $dbM[1];
|
|
$parsedJson = json_decode($dbM[2], true);
|
|
if ($parsedJson) {
|
|
$contactMeta[$extKey] = $parsedJson;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (preg_split('/\r?\n/', $rawContacts) ?: [] as $line) {
|
|
// Contact: 101/sip:101@10.10.167.162:5060 5dd9a21be8 Avail 9.234
|
|
// or with -nan: Contact: 102/sip:102@10.10.168.228:5060;x-ast-txp=trans bb0686ce80 Unavail -nan
|
|
if (preg_match('/Contact:\s+([^\s\/]+)\/sip:([^\s]+)\s+([a-f0-9]+)\s+(\S+)\s+(-?[0-9.]+|nan|-nan)/i', $line, $m)) {
|
|
$extNum = $m[1];
|
|
$uri = $m[2];
|
|
$status = $m[4];
|
|
$isAvail = (stripos($status, 'Avail') !== false && stripos($status, 'Unavail') === false) || stripos($status, 'NonQual') !== false;
|
|
if ($isAvail) {
|
|
$onlineCount++;
|
|
}
|
|
|
|
$meta = $contactMeta[$extNum] ?? [];
|
|
$fullUri = $meta['uri'] ?? ("sip:" . $uri);
|
|
$userAgent = $meta['user_agent'] ?? '';
|
|
|
|
// Détection précise du transport utilisé en direct
|
|
$activeTransport = 'udp';
|
|
if (preg_match('/transport=(tls|wss|ws|tcp|udp)/i', $fullUri, $tm)) {
|
|
$activeTransport = strtolower($tm[1]);
|
|
} elseif (preg_match('/x-ast-txp=transport-(tls|wss|ws|tcp|udp)/i', $fullUri, $tm)) {
|
|
$activeTransport = strtolower($tm[1]);
|
|
} elseif (str_starts_with($fullUri, 'sips:')) {
|
|
$activeTransport = 'tls';
|
|
}
|
|
|
|
$contacts[] = [
|
|
'extension' => $extNum,
|
|
'uri' => $fullUri,
|
|
'user_agent' => $userAgent,
|
|
'active_transport' => $activeTransport,
|
|
'status' => $status,
|
|
'rtt_ms' => is_numeric($m[5]) ? round((float)$m[5], 1) : null,
|
|
];
|
|
}
|
|
}
|
|
|
|
$rawChannels = $ami->runCommand('core show channels count');
|
|
if (preg_match('/(\d+)\s+active\s+call/i', $rawChannels, $cm)) {
|
|
$activeCalls = (int)$cm[1];
|
|
}
|
|
|
|
$rawUptime = $ami->runCommand('core show uptime');
|
|
if (preg_match('/System uptime:\s*(.+)/i', $rawUptime, $um)) {
|
|
$astUptime = trim($um[1]);
|
|
}
|
|
|
|
$registrations = [];
|
|
$rawRegs = $ami->runCommand('pjsip show registrations');
|
|
foreach (preg_split('/\r?\n/', $rawRegs) ?: [] as $line) {
|
|
// trunk-ovh-reg/sip:siptrunk.ovh.net:5060 trunk-ovh-auth Registered
|
|
if (preg_match('/^\s*(trunk-[^\s\/]+)-reg\/(\S+)\s+(\S+)\s+(\S+)/i', $line, $rm)) {
|
|
$registrations[$rm[1]] = [
|
|
'server_uri' => $rm[2],
|
|
'auth' => $rm[3],
|
|
'status' => $rm[4],
|
|
];
|
|
}
|
|
}
|
|
} catch (Throwable) {
|
|
$astUptime = 'Hors-ligne';
|
|
$registrations = [];
|
|
}
|
|
|
|
return [
|
|
'version' => $astVersion,
|
|
'online_contacts' => $onlineCount,
|
|
'active_calls' => $activeCalls,
|
|
'uptime' => $astUptime,
|
|
'contacts' => $contacts,
|
|
'registrations' => $registrations ?? [],
|
|
];
|
|
}
|
|
|
|
private static function loadPrevSample(): array
|
|
{
|
|
if (!is_readable(self::CACHE_FILE)) {
|
|
return [];
|
|
}
|
|
$raw = @file_get_contents(self::CACHE_FILE);
|
|
return $raw ? (json_decode($raw, true) ?: []) : [];
|
|
}
|
|
|
|
private static function savePrevSample(array $sample): void
|
|
{
|
|
@file_put_contents(self::CACHE_FILE, json_encode($sample));
|
|
}
|
|
|
|
private static function formatDuration(int $seconds): string
|
|
{
|
|
$days = intdiv($seconds, 86400);
|
|
$hours = intdiv($seconds % 86400, 3600);
|
|
$mins = intdiv($seconds % 3600, 60);
|
|
if ($days > 0) {
|
|
return "{$days}j {$hours}h {$mins}m";
|
|
}
|
|
if ($hours > 0) {
|
|
return "{$hours}h {$mins}m";
|
|
}
|
|
return "{$mins} min";
|
|
}
|
|
}
|
|
|