0, 'DEBUG' => 1, 'INFO' => 2, 'WARNING' => 3, 'ERROR' => 4, 'FATAL' => 5, ); /* * Current log level * @var string|null */ protected static $level = null; /* * Log PHP errors levels (as specified to set_error_handler()) * Default (set in self::init() method): * - In TRACE or DEBUG: E_ALL & ~E_STRICT * - Otherwise: E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED */ protected static $php_errors_levels = null; // Custom fatal error handler protected static $fatal_error_handler = null; /** * Initialization * @return void */ public static function init() { // Set config default values App :: set_default( 'log', array( 'level' => 'WARNING', 'file_path' => null, 'cli_file_path' => '${log.file_path}', 'cli_console' => false, 'default_locale' => null, 'error_log_fallback' => true, 'php_errors_levels' => array(), // Depend of effective log level, see below ) ); self :: $filepath = App::get( php_sapi_name() == 'cli'?'log.cli_file_path':'log.file_path' ); // PHP error_log() fallback self :: $error_log_fallback = App::get('log.error_log_fallback', null, 'bool'); // Set log level: // Note: in Phpstan context, force FATAL level if (defined('__PHPSTAN_RUNNING__') && constant('__PHPSTAN_RUNNING__')) self :: set_level('FATAL'); else self :: set_level(App::get('log.level', null, 'string')); App :: set_default( 'log.php_errors_levels', in_array(self :: $level, array('DEBUG', 'TRACE'))? array('E_ALL', '~E_STRICT'): array('E_ALL', '~E_STRICT', '~E_NOTICE', '~E_DEPRECATED') ); // Log PHP errors $levels = App::get('log.php_errors_levels', array(), 'array'); if ($levels) { self :: $php_errors_levels = E_ALL; $code = 'self :: $php_errors_levels = '; while($level = array_shift($levels)) { if (!is_string($level)) continue; if (!defined($level) || !is_int(constant($level))) continue; $code .= $level; break; } foreach($levels as $level) { if (!is_string($level)) continue; $combine_operator = '&'; if (in_array($level[0], array('|', '^', '&'))) { $combine_operator = $level[0]; $level = substr($level, 1); } $not = false; if (in_array($level[0], array('!', '~'))) { $not = $level[0]; $level = substr($level, 1); } if (!defined($level) || !is_int(constant($level))) continue; $code .= " $combine_operator "; if ($not) $code .= "$not"; $code .= $level; } $code .= ";"; eval($code); set_error_handler(array('EesyPHP\\Log', 'on_php_error'), self :: $php_errors_levels); } // Log uncatched exceptions set_exception_handler(array('EesyPHP\\Log', 'exception')); } /** * Get remote IP address * @return string */ public static function get_remote_addr(){ if (array_key_exists('HTTP_X_FORWARDED_FOR', $_SERVER)) return $_SERVER["HTTP_X_FORWARDED_FOR"]; if (array_key_exists('REMOTE_ADDR', $_SERVER)) return $_SERVER["REMOTE_ADDR"]; if (array_key_exists('HTTP_CLIENT_IP', $_SERVER)) return $_SERVER["HTTP_CLIENT_IP"]; return ''; } /** * Log a message * @param string $level The message level (key of self :: $levels) * @param string $message The message to log * @param array $extra_args Extra arguments to use to compute message using sprintf * @return true */ public static function log($level, $message, ...$extra_args) { global $argv; if (!array_key_exists($level, self :: $levels)) { self :: warning( "Invalid log level specified logging the message %s, use 'WARNING':\n%s", $message, self :: get_debug_backtrace_context() ); $level = 'WARNING'; } if (!self :: $level) self :: $level = ( App::initialized()? App::get('log.level', 'WARNING', 'string'): 'WARNING' ); if (self :: $levels[$level] < self :: $levels[self :: $level]) return true; if(self :: $filepath && is_null(self :: $file_fd)) { self :: $file_fd = fopen(self :: $filepath, 'a'); if (self :: $file_fd === false) self :: error('Fail to open log file (%s)', self :: $filepath); } // Extra arguments passed, format message using sprintf if ($extra_args) { $message = call_user_func_array( 'sprintf', array_merge(array($message), $extra_args) ); } if (php_sapi_name() == "cli") { $msg = implode(' - ', array( date('Y/m/d H:i:s'), basename($argv[0]), $level, $message ))."\n"; } else { $msg = array( date('Y/m/d H:i:s'), $_SERVER['REQUEST_URI'], self :: get_remote_addr(), ); if (Auth::enabled()) $msg[] = (Auth::user()?Auth::user():'anonymous'); $msg[] = $level; $msg[] = $message; $msg = implode(' - ', $msg)."\n"; } if (!self :: $file_fd || php_sapi_name() == 'cli-server') error_log(rtrim($msg, "\n")); else fwrite(self :: $file_fd, $msg); if ($level == 'FATAL') if (!is_null(self :: $fatal_error_handler)) call_user_func(self :: $fatal_error_handler, $message); elseif (function_exists('fatal_error')) fatal_error($message); else die("\n$message\n\n"); elseif ( self :: $file_fd // When no log file, message already printed on console by error_log() && php_sapi_name() == "cli" && App::initialized() && App::get('log.cli_console', null, 'bool') ) echo $msg; return true; } /** * Log a trace message * @param string $message The message to log * @param array $extra_args Extra arguments to use to compute message using sprintf * @return true */ public static function trace($message, ...$extra_args) { return call_user_func_array( array('EesyPHP\\Log', 'log'), array_merge(array('TRACE', $message), $extra_args) ); } /** * Log a debug message * @param string $message The message to log * @param array $extra_args Extra arguments to use to compute message using sprintf * @return true */ public static function debug($message, ...$extra_args) { return call_user_func_array( array('EesyPHP\\Log', 'log'), array_merge(array('DEBUG', $message), $extra_args) ); } /** * Log an info message * @param string $message The message to log * @param array $extra_args Extra arguments to use to compute message using sprintf * @return true */ public static function info($message, ...$extra_args) { return call_user_func_array( array('EesyPHP\\Log', 'log'), array_merge(array('INFO', $message), $extra_args) ); } /** * Log an warning message * @param string $message The message to log * @param array $extra_args Extra arguments to use to compute message using sprintf * @return true */ public static function warning($message, ...$extra_args) { return call_user_func_array( array('EesyPHP\\Log', 'log'), array_merge(array('WARNING', $message), $extra_args) ); } /** * Log an error message * @param string $message The message to log * @param array $extra_args Extra arguments to use to compute message using sprintf * @return true */ public static function error($message, ...$extra_args) { return call_user_func_array( array('EesyPHP\\Log', 'log'), array_merge(array('ERROR', $message), $extra_args) ); } /** * Log an fatal message * @param string $message The message to log * @param array $extra_args Extra arguments to use to compute message using sprintf * @return true */ public static function fatal($message, ...$extra_args) { return call_user_func_array( array('EesyPHP\\Log', 'log'), array_merge(array('FATAL', $message), $extra_args) ); } /** * Register a contextual fatal error handler * @param null|callable $handler The fatal error handler (set as null to reset) * @return void */ public static function register_fatal_error_handler($handler) { // @phpstan-ignore-next-line if ($handler && !is_callable($handler)) self :: fatal('Invalid fatal error handler provided: it is not callable !'); self :: $fatal_error_handler = ($handler?$handler:null); } /** * Change of current log file * @param string $file The new log file path * @return bool */ public static function change_filepath($file) { if ($file == self :: $filepath) return True; if (self :: $file_fd) { fclose(self :: $file_fd); self :: $file_fd = null; } self :: $filepath = $file; return True; } /** * Get of current log file * @return string|null Current log file path */ public static function filepath() { return self :: $filepath; } /** * Set current log level * @param string $level The new log level to set * @return void */ public static function set_level($level=null) { // Set default log level (if not defined or invalid) if (is_null($level)) { self :: $level = App::get_default('log.level', null, 'string'); } elseif (!array_key_exists($level, self :: $levels)) { self :: $level = App::get_default('log.level', null, 'string'); self :: warning( "Invalid log level value found in configuration (%s). ". "Set as default (%s).", vardump($level), self :: $level); } else { self :: $level = $level; } } /* ******************************************************************************* * Handle exception logging ******************************************************************************* */ /** * Get the current backtrace * @param int $ignore_last The number of last levels to ignore * @return string */ public static function get_debug_backtrace_context($ignore_last=0) { $traces = debug_backtrace(); // Also ignore this function it self $ignore_last++; if (!is_array($traces) || count($traces) <= $ignore_last) return ""; $msg = array(); for ($i=$ignore_last; $i < count($traces); $i++) { $trace = array("#$i"); if (isset($traces[$i]['file'])) $trace[] = $traces[$i]['file'].(isset($traces[$i]['line'])?":".$traces[$i]['line']:""); // @phpstan-ignore-next-line if (isset($traces[$i]['class']) && isset($traces[$i]['function'])) $trace[] = implode(" ", array( $traces[$i]['class'], $traces[$i]['type'], $traces[$i]['function']. "()")); elseif (isset($traces[$i]['function'])) $trace[] = $traces[$i]['function']. "()"; $msg[] = implode(" - ", $trace); } return implode("\n", $msg); } /** * Log an exception * @param Throwable $exception * @param string|null $prefix The prefix of the log message * (optional, default: "An exception occured") * @param array $extra_args Extra arguments to use to compute prefix using sprintf * @return void */ public static function exception($exception, $prefix=null, ...$extra_args) { SentryIntegration :: log($exception); // If extra arguments passed, format prefix message using sprintf if ($prefix && $extra_args) { $prefix = call_user_func_array( 'sprintf', array_merge(array($prefix), $extra_args) ); } self :: error( "%s:\n%s\n## %s:%d : %s", ($prefix?$prefix:"An exception occured"), $exception->getTraceAsString(), $exception->getFile(), $exception->getLine(), $exception->getMessage()); } /* ******************************************************************************* * Handle PHP error logging ******************************************************************************* */ /** * Convert PHP error number to the corresponding label * @param int $errno * @return string */ public static function errno2type($errno) { $constants = get_defined_constants(); if (is_array($constants)) foreach($constants as $label => $value) if ($value == $errno && preg_match('/^E_(.*)$/', $label, $m)) return $m[1]; return 'UNKNOWN ERROR #'.$errno; } /** * Log a PHP error * Note: method design to be used as callable by set_error_handler() * @param int $errno The error number * @param string $errstr The error message * @param string $errfile The filename that the error was raised in * @param int $errline The line number where the error was raised * @return false Return false to let the normal error handler continues. */ public static function on_php_error($errno, $errstr, $errfile, $errline) { self :: error( "A PHP error occured : [%s] %s\nFile : %s (line : %d)", self :: errno2type($errno), $errstr, $errfile, $errline ); return False; } } # vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab