Add PHP container stats to admin Infra page
Shows per-container: hostname, PHP version, SAPI, uptime, CPU cores, CPU usage % (sampled from cgroup), load averages (1/5/15m), RAM used/ total/free with usage %. Color-coded: green <70%, yellow <90%, red >=90%. Reads from cgroup v2 (fallback v1) and /proc for container-level stats. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -663,14 +663,150 @@ class AdminController extends AbstractController
|
||||
$pgInfo = $this->getPostgresInfo($em);
|
||||
$pgbouncerInfo = $this->getPgBouncerInfo($databaseUrl);
|
||||
|
||||
$phpContainers = $this->getPhpContainerInfo();
|
||||
|
||||
return $this->render('admin/infra.html.twig', [
|
||||
'redis_global' => $redisGlobal,
|
||||
'redis_dbs' => [$redisMessenger, $redisSession, $redisCache],
|
||||
'postgres' => $pgInfo,
|
||||
'pgbouncer' => $pgbouncerInfo,
|
||||
'php_containers' => $phpContainers,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function getPhpContainerInfo(): array
|
||||
{
|
||||
$container = $this->readContainerStats();
|
||||
$container['hostname'] = gethostname() ?: '?';
|
||||
$container['php_version'] = \PHP_VERSION;
|
||||
$container['sapi'] = \PHP_SAPI;
|
||||
|
||||
return [$container];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function readContainerStats(): array
|
||||
{
|
||||
$stats = ['connected' => true];
|
||||
|
||||
// CPU load
|
||||
$load = sys_getloadavg();
|
||||
$stats['load_1m'] = $load ? round($load[0], 2) : '?';
|
||||
$stats['load_5m'] = $load ? round($load[1], 2) : '?';
|
||||
$stats['load_15m'] = $load ? round($load[2], 2) : '?';
|
||||
|
||||
// CPU cores
|
||||
$stats['cpu_cores'] = 1;
|
||||
if (is_readable('/proc/cpuinfo')) {
|
||||
$cpuinfo = file_get_contents('/proc/cpuinfo');
|
||||
$stats['cpu_cores'] = substr_count($cpuinfo, 'processor');
|
||||
}
|
||||
|
||||
// CPU usage from cgroup
|
||||
$stats['cpu_usage_percent'] = $this->readCgroupCpuPercent();
|
||||
|
||||
// Memory from cgroup v2
|
||||
$memCurrent = $this->readCgroupFile('/sys/fs/cgroup/memory.current');
|
||||
$memMax = $this->readCgroupFile('/sys/fs/cgroup/memory.max');
|
||||
|
||||
// Fallback to cgroup v1
|
||||
if (null === $memCurrent) {
|
||||
$memCurrent = $this->readCgroupFile('/sys/fs/cgroup/memory/memory.usage_in_bytes');
|
||||
}
|
||||
if (null === $memMax) {
|
||||
$memMax = $this->readCgroupFile('/sys/fs/cgroup/memory/memory.limit_in_bytes');
|
||||
}
|
||||
|
||||
if (null !== $memCurrent) {
|
||||
$stats['ram_used'] = $this->formatBytes((int) $memCurrent);
|
||||
$stats['ram_used_bytes'] = (int) $memCurrent;
|
||||
} else {
|
||||
$stats['ram_used'] = round(memory_get_usage(true) / 1048576, 1).' MB';
|
||||
$stats['ram_used_bytes'] = memory_get_usage(true);
|
||||
}
|
||||
|
||||
if (null !== $memMax && $memMax < 9_000_000_000_000_000_000) {
|
||||
$stats['ram_total'] = $this->formatBytes((int) $memMax);
|
||||
$stats['ram_total_bytes'] = (int) $memMax;
|
||||
$stats['ram_usage_percent'] = round($stats['ram_used_bytes'] / $stats['ram_total_bytes'] * 100, 1);
|
||||
$stats['ram_free'] = $this->formatBytes($stats['ram_total_bytes'] - $stats['ram_used_bytes']);
|
||||
} else {
|
||||
// No limit set, read system memory
|
||||
$meminfo = is_readable('/proc/meminfo') ? file_get_contents('/proc/meminfo') : '';
|
||||
if (preg_match('/MemTotal:\s+(\d+)\s+kB/', $meminfo, $m)) {
|
||||
$totalBytes = (int) $m[1] * 1024;
|
||||
$stats['ram_total'] = $this->formatBytes($totalBytes);
|
||||
$stats['ram_total_bytes'] = $totalBytes;
|
||||
$stats['ram_usage_percent'] = round($stats['ram_used_bytes'] / $totalBytes * 100, 1);
|
||||
$stats['ram_free'] = $this->formatBytes($totalBytes - $stats['ram_used_bytes']);
|
||||
} else {
|
||||
$stats['ram_total'] = '?';
|
||||
$stats['ram_usage_percent'] = '?';
|
||||
$stats['ram_free'] = '?';
|
||||
}
|
||||
}
|
||||
|
||||
// Uptime
|
||||
if (is_readable('/proc/uptime')) {
|
||||
$uptime = (int) explode(' ', file_get_contents('/proc/uptime'))[0];
|
||||
$stats['uptime'] = $this->formatSeconds($uptime);
|
||||
} else {
|
||||
$stats['uptime'] = '?';
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private function readCgroupCpuPercent(): string
|
||||
{
|
||||
// cgroup v2
|
||||
$usagePath = '/sys/fs/cgroup/cpu.stat';
|
||||
if (is_readable($usagePath)) {
|
||||
$content = file_get_contents($usagePath);
|
||||
if (preg_match('/usage_usec\s+(\d+)/', $content, $m)) {
|
||||
$usageUsec1 = (int) $m[1];
|
||||
usleep(100_000); // 100ms sample
|
||||
$content2 = file_get_contents($usagePath);
|
||||
if (preg_match('/usage_usec\s+(\d+)/', $content2, $m2)) {
|
||||
$usageUsec2 = (int) $m2[1];
|
||||
$deltaUsec = $usageUsec2 - $usageUsec1;
|
||||
$percent = round($deltaUsec / 100_000 * 100, 1); // delta / sample_time * 100
|
||||
|
||||
return $percent.'%';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
private function readCgroupFile(string $path): ?int
|
||||
{
|
||||
if (!is_readable($path)) {
|
||||
return null;
|
||||
}
|
||||
$val = trim(file_get_contents($path));
|
||||
|
||||
return is_numeric($val) ? (int) $val : null;
|
||||
}
|
||||
|
||||
private function formatBytes(int $bytes): string
|
||||
{
|
||||
if ($bytes >= 1_073_741_824) {
|
||||
return round($bytes / 1_073_741_824, 1).' GB';
|
||||
}
|
||||
if ($bytes >= 1_048_576) {
|
||||
return round($bytes / 1_048_576, 1).' MB';
|
||||
}
|
||||
|
||||
return round($bytes / 1024, 1).' KB';
|
||||
}
|
||||
|
||||
private function connectRedis(string $dsn): \Redis
|
||||
{
|
||||
$parsed = parse_url(str_replace('redis://', 'http://', $dsn));
|
||||
|
||||
@@ -5,6 +5,38 @@
|
||||
{% block body %}
|
||||
<h1 class="text-3xl font-black uppercase tracking-tighter italic heading-page mb-8">Infrastructure</h1>
|
||||
|
||||
{# PHP Containers #}
|
||||
<h2 class="text-xl font-black uppercase tracking-tighter italic heading-page mb-4">PHP</h2>
|
||||
<div class="flex flex-wrap gap-6 mb-8">
|
||||
{% for container in php_containers %}
|
||||
<div class="flex-1 min-w-[300px]">
|
||||
<div class="admin-card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest">{{ container.hostname }}</h2>
|
||||
<span class="admin-badge-green text-xs font-black uppercase">PHP {{ container.php_version }}</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0">
|
||||
{{ _self.row('SAPI', container.sapi) }}
|
||||
{{ _self.row('Uptime', container.uptime) }}
|
||||
{{ _self.row('CPU Cores', container.cpu_cores) }}
|
||||
|
||||
<div class="border-t border-gray-200 my-2"></div>
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">CPU</p>
|
||||
{{ _self.row_color('Utilisation', container.cpu_usage_percent, container.cpu_usage_percent != 'N/A' and container.cpu_usage_percent|replace({'%': ''})|number_format < 70 ? 'green' : (container.cpu_usage_percent == 'N/A' ? 'gray' : (container.cpu_usage_percent|replace({'%': ''})|number_format < 90 ? 'yellow' : 'red'))) }}
|
||||
{{ _self.row('Load 1m / 5m / 15m', container.load_1m ~ ' / ' ~ container.load_5m ~ ' / ' ~ container.load_15m) }}
|
||||
|
||||
<div class="border-t border-gray-200 my-2"></div>
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Memoire</p>
|
||||
{{ _self.row('Utilisee', container.ram_used) }}
|
||||
{{ _self.row('Total', container.ram_total) }}
|
||||
{{ _self.row('Libre', container.ram_free) }}
|
||||
{{ _self.row_color('Utilisation', container.ram_usage_percent ~ (container.ram_usage_percent != '?' ? '%' : ''), container.ram_usage_percent != '?' and container.ram_usage_percent < 70 ? 'green' : (container.ram_usage_percent == '?' ? 'gray' : (container.ram_usage_percent < 90 ? 'yellow' : 'red'))) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# PostgreSQL & PgBouncer #}
|
||||
<h2 class="text-xl font-black uppercase tracking-tighter italic heading-page mb-4">PostgreSQL & PgBouncer</h2>
|
||||
<div class="flex flex-wrap gap-6 mb-8">
|
||||
|
||||
Reference in New Issue
Block a user