eesyphp/src/Log.php
2024-01-23 19:23:10 +01:00

467 lines
14 KiB
PHP

<?php
namespace EesyPHP;
use Throwable;
class Log {
/*
* Log file path
* @var string|null
*/
protected static $filepath = null;
/*
* Log file descriptor
* @var resource|null
*/
protected static $file_fd = null;
/**
* PHP error_log() fallback if no filepath configured or fail to open log file
* @var bool
*/
protected static $error_log_fallback = true;
/*
* Log Levels
* @var array(string,int)
*/
protected static $levels = array(
'TRACE' => 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__')) // @phpstan-ignore-line
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 ($level is "FATAL" ? never : 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 never
*/
public static function fatal($message, ...$extra_args) {
// @phpstan-ignore-next-line
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 occurred")
* @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 occurred"),
$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 occurred : [%s] %s\nFile : %s (line : %d)",
self :: errno2type($errno), $errstr, $errfile, $errline
);
return False;
}
}
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab