getFormattedRamUsage()['percentage']; } public function getFormattedRamUsage() { if (PHP_OS_FAMILY === 'Windows') { $output = shell_exec('wmic OS get FreePhysicalMemory,TotalVisibleMemorySize /Value'); if ($output) { preg_match('/TotalVisibleMemorySize=(\d+)/', $output, $total); preg_match('/FreePhysicalMemory=(\d+)/', $output, $free); if (isset($total[1], $free[1])) { $totalBytes = (float) $total[1] * 1024; $freeBytes = (float) $free[1] * 1024; $usedBytes = $totalBytes - $freeBytes; return [ 'total' => MonitoringFormatter::bytes($totalBytes), 'free' => MonitoringFormatter::bytes($freeBytes), 'used' => MonitoringFormatter::bytes($usedBytes), 'percentage' => (int) round(($usedBytes / $totalBytes) * 100), ]; } } } else { $info = @file_get_contents('/proc/meminfo'); if ($info) { preg_match('/MemTotal:\s+(\d+)/', $info, $total); preg_match('/MemAvailable:\s+(\d+)/', $info, $available); preg_match('/SwapTotal:\s+(\d+)/', $info, $swapTotal); preg_match('/SwapFree:\s+(\d+)/', $info, $swapFree); if (isset($total[1], $available[1])) { $totalBytes = (float) $total[1] * 1024; $availableBytes = (float) $available[1] * 1024; $usedBytes = $totalBytes - $availableBytes; $swapTotalBytes = isset($swapTotal[1]) ? (float) $swapTotal[1] * 1024 : 0; $swapFreeBytes = isset($swapFree[1]) ? (float) $swapFree[1] * 1024 : 0; $swapUsedBytes = $swapTotalBytes - $swapFreeBytes; return [ 'total' => MonitoringFormatter::bytes($totalBytes), 'free' => MonitoringFormatter::bytes($availableBytes), 'used' => MonitoringFormatter::bytes($usedBytes), 'percentage' => (int) round(($usedBytes / $totalBytes) * 100), 'swap' => [ 'total' => MonitoringFormatter::bytes($swapTotalBytes), 'used' => MonitoringFormatter::bytes($swapUsedBytes), 'percentage' => $swapTotalBytes > 0 ? (int) round(($swapUsedBytes / $swapTotalBytes) * 100) : 0, ], ]; } } } return [ 'total' => 'Unknown', 'free' => 'Unknown', 'used' => 'Unknown', 'percentage' => 0, ]; } // ======================== // Disk Usage // ======================== public function getDiskUsage() { return $this->getFormattedDiskUsage()['percentage']; } public function getFormattedDiskUsage() { $path = PHP_OS_FAMILY === 'Windows' ? 'C:' : '/'; $total = @disk_total_space($path); $free = @disk_free_space($path); if (! $total) { return [ 'total' => 'Unknown', 'free' => 'Unknown', 'used' => 'Unknown', 'percentage' => 0, ]; } $used = $total - $free; return [ 'total' => MonitoringFormatter::bytes($total), 'free' => MonitoringFormatter::bytes($free), 'used' => MonitoringFormatter::bytes($used), 'percentage' => (int) round(($used / $total) * 100), ]; } // ======================== // Active Users (Laravel Session) // ======================== public function getActiveUsers() { return Cache::remember('monitoring_active_users', 60, function () { $driver = config('session.driver'); $data = ['total' => 0, 'authenticated' => 0]; try { if ($driver === 'database') { if (! DB::getSchemaBuilder()->hasTable('sessions')) { return $data; } $data['total'] = DB::table('sessions')->count(); $data['authenticated'] = DB::table('sessions')->whereNotNull('user_id')->count(); return $data; } if ($driver === 'redis') { $connection = config('session.connection') ?? 'default'; $redis = Redis::connection($connection); $sessionCookie = config('session.cookie', 'laravel_session'); $patterns = [ $sessionCookie.':*', str_replace('-', '_', $sessionCookie).':*', str_replace('_', '-', $sessionCookie).':*', '*_session:*', ]; $count = 0; foreach ($patterns as $pattern) { $cursor = '0'; do { $result = $redis->scan($cursor, ['match' => $pattern, 'count' => 100]); $cursor = $result[0]; $count += count($result[1]); } while ($cursor !== '0'); } $data['total'] = $count; $data['authenticated'] = 'N/A'; // Redis scan doesn't expose user_id easily return $data; } if ($driver === 'file') { $path = config('session.files'); if (! file_exists($path)) { return $data; } $data['total'] = count(glob($path.'/*')); $data['authenticated'] = 'N/A'; return $data; } } catch (\Exception $e) { return $data; } return $data; }); } public function getRegisteredUsers() { try { return User::count(); } catch (\Exception $e) { return 0; } } // ======================== // System Info // ======================== public function getPhpVersion() { return PHP_VERSION; } public function getDatabaseVersion() { try { return DB::connection()->getPdo()->getAttribute(\PDO::ATTR_SERVER_VERSION); } catch (\Exception $e) { return 'Unknown'; } } public function getServerIp() { return request()->server('SERVER_ADDR') ?? gethostbyname(gethostname()); } public function getUptime() { if (PHP_OS_FAMILY === 'Windows') { $output = shell_exec('wmic os get lastbootuptime /value'); if ($output && preg_match('/LastBootUpTime=(\d+)/', $output, $matches)) { $bootTime = $matches[1]; // Format: YYYYMMDDHHMMSS.MMMMMM+UUU $year = substr($bootTime, 0, 4); $month = substr($bootTime, 4, 2); $day = substr($bootTime, 6, 2); $hour = substr($bootTime, 8, 2); $minute = substr($bootTime, 10, 2); $second = substr($bootTime, 12, 2); $bootTimestamp = strtotime("$year-$month-$day $hour:$minute:$second"); $diff = time() - $bootTimestamp; return MonitoringFormatter::duration($diff); } } else { $uptime = @file_get_contents('/proc/uptime'); if ($uptime) { $uptime = explode(' ', $uptime)[0]; return MonitoringFormatter::duration((int) $uptime); } } return 'Unknown'; } // ======================== // Queue Stats // ======================== public function getQueueStats() { try { // Use Cache Heartbeat (updated by WorkerHeartbeatJob) $lastHeartbeat = Cache::get('queue_worker_heartbeat'); $workerRunning = $lastHeartbeat && (now()->timestamp - $lastHeartbeat) < 300; // 5 minute threshold // Fallback to PS if heartbeat is missing (optional, but keep for robustness) if (! $workerRunning && PHP_OS_FAMILY !== 'Windows') { $output = @shell_exec('ps aux | grep "queue:work" | grep -v grep'); $workerRunning = ! empty($output); } // Throughput Estimation (Activities in last 5 minutes) $throughput = 0; if (DB::getSchemaBuilder()->hasTable('activity_log')) { $throughput = DB::table('activity_log') ->where('created_at', '>=', now()->subMinutes(5)) ->count(); } return [ 'pending' => DB::table('jobs')->count(), 'failed' => DB::table('failed_jobs')->count(), 'worker_active' => (bool) $workerRunning, 'throughput' => $throughput, 'load_factor' => $throughput > 0 ? round($throughput / 5, 1) : 0, // tasks per minute ]; } catch (\Exception $e) { return [ 'pending' => 0, 'failed' => 0, 'worker_active' => false, ]; } } // ======================== // Database Info // ======================== public function getDatabaseInfo() { return Cache::remember('monitoring_database_info', 300, function () { try { $driver = DB::getDriverName(); $tables = 0; $size = 0; $topTables = []; // Performance Check $start = microtime(true); DB::select('SELECT 1'); $latency = (int) round((microtime(true) - $start) * 1000); if ($driver === 'pgsql') { $tableCount = DB::select("SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'"); $tables = $tableCount[0]->count ?? 0; $dbSize = DB::select('SELECT pg_database_size(current_database()) AS size'); $size = $dbSize[0]->size ?? 0; // Technical detail: Top 5 tables by size $topTables = DB::select(' SELECT relname AS table, pg_size_pretty(pg_total_relation_size(relid)) AS size_pretty, pg_total_relation_size(relid) AS size_bytes FROM pg_stat_user_tables ORDER BY pg_total_relation_size(relid) DESC LIMIT 5 '); } else { $tableCount = DB::select('SHOW TABLES'); $tables = count($tableCount); $dbSize = DB::select('SELECT SUM(data_length + index_length) AS size FROM information_schema.TABLES WHERE table_schema = DATABASE()'); $size = $dbSize[0]->size ?? 0; } return [ 'tables' => $tables, 'size' => MonitoringFormatter::bytes($size), 'size_bytes' => $size, 'top_tables' => $topTables, 'latency' => $latency.'ms', 'status' => $latency < 100 ? 'STABLE' : 'DEGRADED', ]; } catch (\Exception $e) { return [ 'tables' => 0, 'size' => 'Unknown', 'size_bytes' => 0, 'top_tables' => [], 'latency' => '0ms', 'status' => 'OFFLINE', ]; } }); } // ======================== // Redis Info // ======================== public function getRedisStats() { try { $redis = Redis::connection(); $start = microtime(true); $redis->ping(); $latency = (int) round((microtime(true) - $start) * 1000); $info = $redis->info(); // Flatten if grouped (Predis often returns sections as sub-arrays) $flatInfo = []; foreach ($info as $key => $value) { if (is_array($value)) { $flatInfo = array_merge($flatInfo, $value); } else { $flatInfo[$key] = $value; } } return [ 'status' => 'connected', 'version' => $flatInfo['redis_version'] ?? 'Unknown', 'memory_used' => $flatInfo['used_memory_human'] ?? 'Unknown', 'clients' => $flatInfo['connected_clients'] ?? 0, 'latency' => $latency.'ms', 'uptime' => MonitoringFormatter::duration((int) ($flatInfo['uptime_in_seconds'] ?? 0)), ]; } catch (\Exception $e) { return [ 'status' => 'disconnected', 'error' => 'Connection failed', 'version' => 'Unknown', 'memory_used' => 'Unknown', 'clients' => 0, 'latency' => '0ms', 'uptime' => 'Unknown', ]; } } public function getRedisClients() { try { $redis = Redis::connection(); $clientsRaw = $redis->executeRaw(['CLIENT', 'LIST']); if (! is_string($clientsRaw)) { return ['clients' => [], 'has_reverb' => false]; } $lines = explode("\n", trim($clientsRaw)); $clients = []; $hasReverb = false; foreach ($lines as $line) { if (empty($line)) { continue; } $props = []; foreach (explode(' ', $line) as $prop) { if (strpos($prop, '=') !== false) { [$k, $v] = explode('=', $prop, 2); $props[$k] = $v; } } // Identify client based on name or source $addr = $props['addr'] ?? 'Unknown'; $name = $props['name'] ?: ($props['cmd'] === 'subscribe' ? 'Reverb' : 'Worker'); if (strpos($addr, '127.0.0.1') !== false) { $type = 'Local Node'; } elseif ($props['cmd'] === 'subscribe') { $type = 'WebSocket'; $hasReverb = true; } else { $type = 'Background'; } $clients[] = [ 'id' => $props['id'] ?? '?', 'name' => $name, 'type' => $type, 'addr' => $addr, 'age' => MonitoringFormatter::duration((int) ($props['age'] ?? 0)), 'idle' => MonitoringFormatter::duration((int) ($props['idle'] ?? 0)), 'db' => $props['db'] ?? '0', 'cmd' => $props['cmd'] ?? 'N/A', ]; } return [ 'clients' => $clients, 'has_reverb' => $hasReverb, ]; } catch (\Exception $e) { return ['clients' => [], 'has_reverb' => false]; } } public function getQueueDetails() { try { return DB::table('jobs') ->select('id', 'queue', 'payload', 'available_at') ->latest('available_at') ->take(10) ->get() ->map(function ($job) { $payload = json_decode($job->payload); return [ 'id' => $job->id, 'name' => $payload->displayName ?? 'Unknown Job', 'queue' => $job->queue, 'time' => Carbon::createFromTimestamp($job->available_at)->diffForHumans(), ]; }); } catch (\Exception $e) { return []; } } // ======================== // App Health // ======================== public function getAppHealth() { return [ 'maintenance' => app()->isDownForMaintenance(), 'storage_link' => file_exists(public_path('storage')), 'env_safe' => ! config('app.debug'), // In production, debug should be false 'cache_active' => $this->checkCache(), 'reverb_active' => $this->checkReverbConnection(), 'logs_size' => MonitoringFormatter::bytes(file_exists(storage_path('logs/laravel.log')) ? filesize(storage_path('logs/laravel.log')) : 0), ]; } private function checkCache() { try { Cache::put('monitoring_check', true, 5); return Cache::get('monitoring_check') === true; } catch (\Exception $e) { return false; } } public function checkReverbConnection() { $host = config('reverb.servers.reverb.host', '127.0.0.1'); $port = config('reverb.servers.reverb.port', 8080); return Cache::remember('monitoring_reverb_status', 30, function () use ($host, $port) { // If localhost fails, try 'reverb' container name as fallback for Docker $targets = [$host]; if ($host === 'localhost' || $host === '127.0.0.1') { $targets[] = 'reverb'; } foreach ($targets as $target) { try { $connection = @fsockopen($target, $port, $errno, $errstr, 0.2); // Reduced timeout to 0.2s if (is_resource($connection)) { fclose($connection); return true; } } catch (\Exception $e) { continue; } } return false; }); } // ======================== // Recent Activity // ======================== public function getRecentActivity() { if (! DB::getSchemaBuilder()->hasTable('activity_log')) { return []; } return DB::table('activity_log') ->leftJoin('users', 'activity_log.causer_id', '=', 'users.id') ->select('activity_log.*', 'users.name as causer_name') ->latest() ->take(5) ->get() ->map(function ($item) { return [ 'description' => ucfirst(str_replace('_', ' ', $item->description)), 'subject' => $item->subject_type ? class_basename($item->subject_type) : 'System', 'causer' => $item->causer_name ?? 'System', 'time' => Carbon::parse($item->created_at)->diffForHumans(), ]; }); } // ======================== // Backup Info // ======================== public function getBackupStatus() { $backupDisk = config('backup.backup.destination.disks.0', 'local'); $backupName = config('backup.backup.name', 'laravel-backup'); return Cache::remember('monitoring_backup_status', 1800, function () use ($backupDisk, $backupName) { try { $files = Storage::disk($backupDisk)->files($backupName); if (empty($files)) { return ['last_backup' => 'Never', 'count' => 0]; } $lastFile = end($files); $timestamp = Storage::disk($backupDisk)->lastModified($lastFile); return [ 'last_backup' => Carbon::createFromTimestamp($timestamp)->diffForHumans(), 'count' => count($files), 'latest_file' => basename($lastFile), ]; } catch (\Exception $e) { return ['last_backup' => 'Unknown', 'count' => 0]; } }); } // ======================== // SAP RFC Stats // ======================== public function getSapStatus() { $possiblePaths = [ base_path('dev_rfc.trc'), public_path('dev_rfc.trc'), storage_path('logs/dev_rfc.trc'), ]; $exists = false; $size = 0; foreach ($possiblePaths as $path) { if (file_exists($path)) { $exists = true; $size = filesize($path); break; } } return [ 'active' => $exists, 'size' => MonitoringFormatter::bytes($size), 'status' => $exists ? 'TRACKING' : 'IDLE', ]; } // ======================== // Mobile Stats // ======================== public function getMobileStats() { $logFile = storage_path('logs/mobile.log'); $count = 0; if (file_exists($logFile)) { // Optimization: Use shell grep -c to count entries without loading file into memory $count = (int) shell_exec("grep -c '^\[[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}' ".escapeshellarg($logFile)); } return [ 'total_logs' => $count, 'last_activity' => file_exists($logFile) ? Carbon::createFromTimestamp(filemtime($logFile))->diffForHumans() : 'Never', ]; } // ======================== // Bundle All Metrics // ======================== public function getAll() { return Cache::remember('monitoring_full_bundle', 10, function () { $redisData = $this->getRedisClients(); return [ 'os' => PHP_OS_FAMILY, 'maintenance' => app()->isDownForMaintenance(), 'hostname' => gethostname(), 'ip' => $this->getServerIp(), 'php' => $this->getPhpVersion(), 'db' => $this->getDatabaseVersion(), 'uptime' => $this->getUptime(), 'cpu' => $this->getCpuUsage(), 'ram' => $this->getFormattedRamUsage(), 'disk' => $this->getFormattedDiskUsage(), 'users' => $this->getActiveUsers(), 'total_users' => $this->getRegisteredUsers(), 'queues' => $this->getQueueStats(), 'db_stats' => $this->getDatabaseInfo(), 'redis' => $this->getRedisStats(), 'health' => $this->getAppHealth(), 'activity' => $this->getRecentActivity(), 'backup' => $this->getBackupStatus(), 'sap' => $this->getSapStatus(), 'mobile' => $this->getMobileStats(), 'redis_clients' => $redisData['clients'] ?? [], 'has_reverb' => $redisData['has_reverb'] ?? false, 'queue_details' => $this->getQueueDetails(), 'last_update' => now()->format('H:i:s'), ]; }); } }