logPath)) { mkdir($this->logPath, 0755, true); } // Set global context $this->setGlobalContext(); } /** * Set global context information */ private function setGlobalContext() { $this->context = [ 'timestamp' => date('Y-m-d H:i:s'), 'request_id' => $this->generateRequestId(), 'ip' => $this->getClientIP(), 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown', 'request_uri' => $_SERVER['REQUEST_URI'] ?? '', 'request_method' => $_SERVER['REQUEST_METHOD'] ?? '', 'user_id' => $_SESSION['USER_ID'] ?? null, 'session_id' => session_id() ?: null, 'memory_usage' => memory_get_usage(true), 'peak_memory' => memory_get_peak_usage(true), 'execution_time' => microtime(true) - ($_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true)) ]; // Expose request id to clients for correlation if (!headers_sent()) { header('X-Request-ID: ' . $this->context['request_id']); } } /** * Generate unique request ID for tracking */ private function generateRequestId() { return uniqid('req_', true); } /** * Get real client IP address */ private function getClientIP() { $ipKeys = ['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR']; foreach ($ipKeys as $key) { if (array_key_exists($key, $_SERVER) === true) { $ip = $_SERVER[$key]; if (strpos($ip, ',') !== false) { $ip = explode(',', $ip)[0]; } $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $ip; } } } return $_SERVER['REMOTE_ADDR'] ?? 'unknown'; } /** * Log message with specified level */ public function log($level, $message, $context = []) { $logData = array_merge($this->context, [ 'level' => $level, 'message' => $message, 'context' => $context, 'backtrace' => $this->getBacktrace() ]); // Write to appropriate log files $this->writeToFile($level, $logData); // Write to database if configured $this->writeToDatabase($level, $logData); // Send alerts for critical errors if (in_array($level, [self::EMERGENCY, self::ALERT, self::CRITICAL, self::ERROR])) { $this->sendAlert($level, $logData); } } /** * Get formatted backtrace */ private function getBacktrace() { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10); $formattedTrace = []; foreach ($trace as $i => $frame) { if ($i === 0) continue; // Skip current function $formattedTrace[] = [ 'file' => $frame['file'] ?? 'unknown', 'line' => $frame['line'] ?? 0, 'function' => $frame['function'] ?? 'unknown', 'class' => $frame['class'] ?? null ]; } return $formattedTrace; } /** * Write log to file */ private function writeToFile($level, $logData) { $filename = $this->logPath . date('Y-m-d') . '_' . $level . '.log'; // Rotate log if too large if (file_exists($filename) && filesize($filename) > $this->maxFileSize) { $this->rotateLogFile($filename); } $logLine = json_encode($logData, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n"; file_put_contents($filename, $logLine, FILE_APPEND | LOCK_EX); } /** * Rotate log files */ private function rotateLogFile($filename) { for ($i = $this->maxFiles - 1; $i > 0; $i--) { $oldFile = $filename . '.' . $i; $newFile = $filename . '.' . ($i + 1); if (file_exists($oldFile)) { if ($i === $this->maxFiles - 1) { unlink($oldFile); } else { rename($oldFile, $newFile); } } } if (file_exists($filename)) { rename($filename, $filename . '.1'); } } /** * Write to database (optional) */ private function writeToDatabase($level, $logData) { global $db, $cfg; // Only log to database if enabled in config (supports old/new keys) $dbLogging = $cfg['logging_database_logging'] ?? ($cfg['database_logging'] ?? false); if (!$dbLogging) { return; } try { $sql = "INSERT INTO `db_logs` (`level`, `message`, `context`, `request_id`, `user_id`, `ip`, `user_agent`, `request_uri`, `created_at`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())"; $db->Execute($sql, [ $level, $logData['message'], json_encode($logData['context']), $logData['request_id'], $logData['user_id'], $logData['ip'], $logData['user_agent'] ?? null, $logData['request_uri'] ?? null ]); } catch (Exception $e) { // Fallback to file logging if database fails error_log("Database logging failed: " . $e->getMessage()); } } /** * Send alerts for critical errors */ private function sendAlert($level, $logData) { global $cfg; // Only send alerts if configured if (!isset($cfg['error_alerts']) || !$cfg['error_alerts']) { return; } // Rate limit alerts to prevent spam $alertKey = 'alert_' . md5($logData['message']); if (!$this->checkAlertRateLimit($alertKey)) { return; } $subject = "EasyStream {$level}: " . substr($logData['message'], 0, 50); $body = $this->formatAlertEmail($logData); // Send email alert (implement based on your email system) if (isset($cfg['admin_email']) && !empty($cfg['admin_email'])) { $this->sendEmailAlert($cfg['admin_email'], $subject, $body); } // Optional webhook alerting $webhookEnabled = $cfg['logging_error_webhook'] ?? false; $webhookUrl = $cfg['logging_webhook_url'] ?? ''; $levels = $cfg['logging_webhook_levels'] ?? ['emergency','alert','critical','error']; if ($webhookEnabled && $webhookUrl && in_array($level, $levels)) { $this->sendWebhook($webhookUrl, $level, $logData); } } /** * Check alert rate limiting */ private function checkAlertRateLimit($key, $maxAlerts = 5, $timeWindow = 3600) { // Prefer Redis counter with TTL; fallback to PHP session $redis = $this->getRedis(); if ($redis) { $rkey = 'alert_limit:' . $key; try { $cnt = $redis->incr($rkey); if ((int)$cnt === 1) { $redis->expire($rkey, (int)$timeWindow); } return (int)$cnt <= (int)$maxAlerts; } catch (\Exception $e) { // fallthrough to session fallback } } if (!isset($_SESSION)) { session_start(); } $now = time(); $alertKey = 'alert_limit_' . $key; if (!isset($_SESSION[$alertKey])) { $_SESSION[$alertKey] = []; } $_SESSION[$alertKey] = array_filter($_SESSION[$alertKey], function($timestamp) use ($now, $timeWindow) { return ($now - $timestamp) < $timeWindow; }); if (count($_SESSION[$alertKey]) >= $maxAlerts) { return false; } $_SESSION[$alertKey][] = $now; return true; } /** * Optional Redis client for rate limiting */ private function getRedis() { static $r = null; if ($r === false) return null; if ($r instanceof \Redis) return $r; $host = getenv('REDIS_HOST') ?: ($GLOBALS['cfg']['redis_host'] ?? null); $port = (int) (getenv('REDIS_PORT') ?: ($GLOBALS['cfg']['redis_port'] ?? 6379)); $db = (int) (getenv('REDIS_DB') ?: ($GLOBALS['cfg']['redis_db'] ?? 0)); if (!$host || !class_exists('Redis')) { $r = false; return null; } try { $cli = new \Redis(); if (!$cli->connect($host, $port, 1.5)) { $r = false; return null; } if ($db) $cli->select($db); $r = $cli; return $cli; } catch (\Exception $e) { $r = false; return null; } } /** * Send webhook with log payload */ private function sendWebhook($url, $level, $logData) { $payload = json_encode(['level' => $level, 'log' => $logData]); $opts = [ 'http' => [ 'method' => 'POST', 'header' => "Content-Type: application/json\r\n", 'content' => $payload, 'timeout' => 2, ] ]; @file_get_contents($url, false, stream_context_create($opts)); } /** * Format alert email */ private function formatAlertEmail($logData) { $body = "Error Details:\n\n"; $body .= "Level: " . strtoupper($logData['level']) . "\n"; $body .= "Message: " . $logData['message'] . "\n"; $body .= "Request ID: " . $logData['request_id'] . "\n"; $body .= "Time: " . $logData['timestamp'] . "\n"; $body .= "IP: " . $logData['ip'] . "\n"; $body .= "User ID: " . ($logData['user_id'] ?: 'Guest') . "\n"; $body .= "URI: " . $logData['request_uri'] . "\n"; $body .= "Method: " . $logData['request_method'] . "\n"; $body .= "User Agent: " . $logData['user_agent'] . "\n\n"; if (!empty($logData['context'])) { $body .= "Context:\n" . json_encode($logData['context'], JSON_PRETTY_PRINT) . "\n\n"; } if (!empty($logData['backtrace'])) { $body .= "Stack Trace:\n"; foreach ($logData['backtrace'] as $i => $frame) { $body .= "#{$i} {$frame['file']}:{$frame['line']} "; if ($frame['class']) { $body .= "{$frame['class']}::{$frame['function']}()\n"; } else { $body .= "{$frame['function']}()\n"; } } } return $body; } /** * Send email alert */ private function sendEmailAlert($to, $subject, $body) { // Use your existing email system or implement basic mail $headers = "From: noreply@" . ($_SERVER['HTTP_HOST'] ?? 'localhost') . "\r\n"; $headers .= "Content-Type: text/plain; charset=UTF-8\r\n"; mail($to, $subject, $body, $headers); } // Convenience methods for different log levels public function emergency($message, $context = []) { $this->log(self::EMERGENCY, $message, $context); } public function alert($message, $context = []) { $this->log(self::ALERT, $message, $context); } public function critical($message, $context = []) { $this->log(self::CRITICAL, $message, $context); } public function error($message, $context = []) { $this->log(self::ERROR, $message, $context); } public function warning($message, $context = []) { $this->log(self::WARNING, $message, $context); } public function notice($message, $context = []) { $this->log(self::NOTICE, $message, $context); } public function info($message, $context = []) { $this->log(self::INFO, $message, $context); } public function debug($message, $context = []) { $this->log(self::DEBUG, $message, $context); } /** * Log database errors with query information */ public function logDatabaseError($error, $query = '', $params = []) { $this->error('Database Error: ' . $error, [ 'query' => $query, 'parameters' => $params, 'database_error' => true ]); } /** * Log security events */ public function logSecurityEvent($event, $context = []) { $this->warning('Security Event: ' . $event, array_merge($context, [ 'security_event' => true ])); } /** * Log performance issues */ public function logPerformanceIssue($message, $executionTime, $context = []) { $this->notice('Performance Issue: ' . $message, array_merge($context, [ 'execution_time' => $executionTime, 'performance_issue' => true ])); } /** * Get recent logs for admin dashboard */ public function getRecentLogs($level = null, $limit = 100, $offset = 0) { global $db, $cfg; $logs = []; $dbLogging = $cfg['logging_database_logging'] ?? ($cfg['database_logging'] ?? false); if ($dbLogging && isset($db)) { try { $sql = "SELECT `level`, `message`, `context`, `request_id`, `user_id`, `ip`, `user_agent`, `request_uri`, `created_at` FROM `db_logs`"; $params = []; if (!empty($level)) { $sql .= " WHERE `level` = ?"; $params[] = $level; } $sql .= " ORDER BY `created_at` DESC LIMIT " . (int)$limit . " OFFSET " . (int)$offset; $res = empty($params) ? $db->Execute($sql) : $db->Execute($sql, $params); if ($res) { while (!$res->EOF) { $ctx = json_decode($res->fields['context'] ?: '{}', true); $logs[] = [ 'level' => $res->fields['level'], 'message' => $res->fields['message'], 'context' => is_array($ctx) ? $ctx : [], 'request_id' => $res->fields['request_id'], 'user_id' => $res->fields['user_id'], 'ip' => $res->fields['ip'], 'user_agent' => $res->fields['user_agent'], 'request_uri' => $res->fields['request_uri'], 'timestamp' => $res->fields['created_at'], ]; $res->MoveNext(); } } return $logs; } catch (\Exception $e) {} } // File fallback: aggregate recent lines then slice by offset/limit $pattern = $this->logPath . date('Y-m-d') . '_' . ($level ?: '*') . '.log'; foreach (glob($pattern) as $file) { $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($lines as $line) { $logEntry = json_decode($line, true); if ($logEntry) { $logs[] = $logEntry; } } } usort($logs, function($a, $b) { return strtotime($b['timestamp']) - strtotime($a['timestamp']); }); return array_slice($logs, (int)$offset, (int)$limit); } }