feat: Add comprehensive documentation suite and reorganize project structure
- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
This commit is contained in:
492
f_core/f_classes/class.logger.php
Normal file
492
f_core/f_classes/class.logger.php
Normal file
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
/*******************************************************************************************************************
|
||||
| Software Name : EasyStream
|
||||
| Software Description : High End YouTube Clone Script with Videos, Shorts, Streams, Images, Audio, Documents, Blogs
|
||||
| Software Author : (c) Sami Ahmed
|
||||
|*******************************************************************************************************************
|
||||
|
|
||||
|*******************************************************************************************************************
|
||||
| This source file is subject to the EasyStream Proprietary License Agreement.
|
||||
|
|
||||
| By using this software, you acknowledge having read this Agreement and agree to be bound thereby.
|
||||
|*******************************************************************************************************************
|
||||
| Copyright (c) 2025 Sami Ahmed. All rights reserved.
|
||||
|*******************************************************************************************************************/
|
||||
|
||||
defined('_ISVALID') or header('Location: /error');
|
||||
|
||||
/**
|
||||
* Enhanced Logging System for Error Tracking and Debugging
|
||||
*/
|
||||
class VLogger
|
||||
{
|
||||
const EMERGENCY = 'emergency';
|
||||
const ALERT = 'alert';
|
||||
const CRITICAL = 'critical';
|
||||
const ERROR = 'error';
|
||||
const WARNING = 'warning';
|
||||
const NOTICE = 'notice';
|
||||
const INFO = 'info';
|
||||
const DEBUG = 'debug';
|
||||
|
||||
private static $instance = null;
|
||||
private $logPath = 'f_data/logs/';
|
||||
private $maxFileSize = 10485760; // 10MB
|
||||
private $maxFiles = 5;
|
||||
private $context = [];
|
||||
|
||||
public static function getInstance()
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
// Ensure log directory exists
|
||||
if (!is_dir($this->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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user