Files
biiproject-kit-v1/app/Services/Monitoring/SystemMonitoringService.php
T

711 lines
24 KiB
PHP

<?php
/**
* ============================================================
*
* @project biiproject
*
* @author Andika Debi Putra
*
* @email andikadebiputra@gmail.com
*
* @website https://biiproject.com
*
* @copyright Copyright (c) 2026 Andika Debi Putra
* @license Proprietary - All Rights Reserved
*
* @version 1.0.0
*
* @created 2026-05-01
* ============================================================
*
* Unauthorized copying, modification, distribution, or use
* of this file is strictly prohibited without prior written
* permission from the author.
* ============================================================
*/
namespace App\Services\Monitoring;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage;
class SystemMonitoringService
{
// ========================
// CPU Usage
// ========================
public function getCpuUsage()
{
if (PHP_OS_FAMILY === 'Windows') {
$output = shell_exec('wmic cpu get loadpercentage /value');
if (! $output) {
return 0;
}
preg_match('/LoadPercentage=(\d+)/', $output, $matches);
return isset($matches[1]) ? (int) $matches[1] : 0;
}
// Linux / Unix
$load = sys_getloadavg()[0] ?? 0;
$cores = (int) shell_exec('nproc') ?: 1;
return (int) round(($load / $cores) * 100);
}
// ========================
// RAM Usage
// ========================
public function getRamUsage()
{
return $this->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'),
];
});
}
}