From 458cbd50b595813fd11c2beacf40eb0f216ec009 Mon Sep 17 00:00:00 2001 From: "Bruno O. Notario" Date: Tue, 12 Mar 2024 01:38:29 -0300 Subject: [PATCH] Log improvements --- system/database/autoload.php | 2 +- system/engine/api/log.php | 127 ++++++++++++++ system/engine/log.php | 325 ++++++++++++++++++++++++++++++++++- system/system.php | 15 +- 4 files changed, 462 insertions(+), 7 deletions(-) diff --git a/system/database/autoload.php b/system/database/autoload.php index 2ad13fc..9cfc09c 100644 --- a/system/database/autoload.php +++ b/system/database/autoload.php @@ -267,7 +267,7 @@ class Database implements DatabaseApi { 'trace' => ($debugging) ? \Phacil\Framework\Debug::backtrace(true, false) : null ]; - $logger->write(($errorFormat == 'json') ? json_encode($debugStamp) : implode(PHP_EOL, array_map( + $logger->info(($errorFormat == 'json') ? json_encode($debugStamp) : implode(PHP_EOL, array_map( [\Phacil\Framework\Exception::class, 'convertArray'], $debugStamp, array_keys($debugStamp) diff --git a/system/engine/api/log.php b/system/engine/api/log.php index 93d2682..e1fe6ab 100644 --- a/system/engine/api/log.php +++ b/system/engine/api/log.php @@ -22,12 +22,35 @@ interface Log const CONFIGURATION_LOGS_CONST = 'DIR_LOGS'; + const LOG_FILE_EXTENSION = '.log'; + + const DEFAULT_FILE_NAME = 'error.log'; + + const DATE_TIME_FORMAT = 'Y-m-d\TH:i:s.v P'; + + const DATE_TIME_FORMAT_PHP5 = 'Y-m-d\TH:i:s.%\s P'; + + const EMERGENCY = 'EMERGENCY'; + CONST ALERT = 'ALERT'; + CONST CRITICAL = 'CRITICAL'; + CONST ERROR = 'ERROR'; + CONST WARNING = 'WARNING'; + CONST NOTICE = 'NOTICE'; + CONST INFO = 'INFO'; + CONST DEBUG = 'DEBUG'; + /** * @param string $filename (optional) The name of log file. The path is automatic defined in the DIR_LOGS constant in config file. Isn't not possible to change the path. The default name is error.log. * @return void */ public function __construct($filename = "error.log"); + /** + * @param string $name + * @return $this|\Phacil\Framework\Api\Log + */ + public function setLog($name); + /** * Write the error message in the log file * @@ -75,4 +98,108 @@ interface Log * @since 2.0.0 */ public function head($filepath = null, $lines = 10, $adaptive = true); + + /** + * System is unusable. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function emergency($message, array $context = array()); + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function alert($message, array $context = array()); + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function critical($message, array $context = array()); + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function error($message, array $context = array()); + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function warning($message, array $context = array()); + + /** + * Normal but significant events. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function notice($message, array $context = array()); + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function info($message, array $context = array()); + + /** + * Detailed debug information. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function debug($message, array $context = array()); + + /** + * Logs with an arbitrary level. + * + * @param string $level + * @param string $message + * @param array $context (Optional) + * + * @return int|false + * + * @throws \Phacil\Framework\Exception\InvalidArgumentException + */ + public function log($level, $message, array $context = array()); } \ No newline at end of file diff --git a/system/engine/log.php b/system/engine/log.php index d6f59a8..c4230b5 100644 --- a/system/engine/log.php +++ b/system/engine/log.php @@ -37,11 +37,28 @@ class Log implements \Phacil\Framework\Api\Log { */ private $filepath; + /** + * + * @var \Phacil\Framework\Api\Log[] + */ + private $extraLoggers = []; + + /** @var bool */ + protected $removeUsedContextFields = false; + + protected $allowInlineLineBreaks = true; + + /** + * Log output format + * @var string + */ + protected $format = "[%datetime%] %channel%.%level_name%: %message% %context% %extra% | %route%"; + /** * @param string $filename (optional) The name of log file. The path is automatic defined in the DIR_LOGS constant in config file. Isn't not possible to change the path. The default name is error.log. * @return void */ - public function __construct($filename = "error.log") { + public function __construct($filename = \Phacil\Framework\Api\Log::DEFAULT_FILE_NAME) { if(!defined(self::CONFIGURATION_LOGS_CONST)){ if(!defined('DIR_APPLICATION')) trigger_error('The '.self::CONFIGURATION_LOGS_CONST.' folder configuration is required!', E_USER_ERROR); @@ -58,10 +75,25 @@ class Log implements \Phacil\Framework\Api\Log { if(!is_writable(constant(self::CONFIGURATION_LOGS_CONST))){ trigger_error('The '. constant(self::CONFIGURATION_LOGS_CONST).' folder must to be writeable!', E_USER_ERROR); } - $this->fileobj = fopen($this->filepath, 'a+'); + //$this->fileobj = fopen($this->filepath, 'a+'); return; } + /** + * {@inheritdoc} + */ + public function setLog($name) { + if(($name.self::LOG_FILE_EXTENSION) == $this->filename){ + return $this; + } + + if(isset($this->extraLoggers[$name])){ + return $this->extraLoggers[$name]; + } + + return $this->extraLoggers[$name] = new self($name . self::LOG_FILE_EXTENSION); + } + /** * Write the error message in the log file * @@ -70,7 +102,33 @@ class Log implements \Phacil\Framework\Api\Log { * @since 1.0.0 */ public function write($message) { - return fwrite($this->fileobj, date('Y-m-d G:i:s') . ' - ' . print_r($message, true)." | ".$_SERVER['REQUEST_URI'] . PHP_EOL); + return $this->writeToFile('[' . $this->getDateTime() . '] - ' . print_r($message, true)." | ".$_SERVER['REQUEST_URI'] . " | " . \Phacil\Framework\startEngineExacTI::getRoute()); + } + + /** + * @param string $message + * @return int|false + */ + protected function writeToFile($message) { + if(!$this->fileobj){ + $this->fileobj = fopen($this->filepath, 'a+'); + } + return fwrite($this->fileobj, $message . PHP_EOL); + } + + /** @return string */ + protected function getDateTime() { + if (version_compare(phpversion(), "7.2.0", "<")) { + // Obtém o timestamp atual com precisão de microssegundos + $microtime = microtime(true); + + // Arredonda os milissegundos para três casas decimais + $milissegundos = round(($microtime - floor($microtime)) * 1000); + + return sprintf(date(self::DATE_TIME_FORMAT_PHP5), (string)$milissegundos); + } + + return (new \DateTimeImmutable())->format(self::DATE_TIME_FORMAT); } /** @@ -80,7 +138,9 @@ class Log implements \Phacil\Framework\Api\Log { * @return void */ public function __destruct() { - fclose($this->fileobj); + if ($this->fileobj) { + fclose($this->fileobj); + } } /** @@ -204,4 +264,261 @@ class Log implements \Phacil\Framework\Api\Log { fclose($f); return trim($output); } + + /** + * System is unusable. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function emergency($message, array $context = array()) + { + $this->log(self::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(self::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(self::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $this->log(self::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(self::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(self::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(self::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(self::DEBUG, $message, $context); + } + + /** + * {@inheritdoc} + */ + public function log($level = self::INFO, $message = null, array $context = array()) { + $record = [ + 'message' => $message, + 'context' => $context, + 'level' => $level, + 'level_name' => $level, + 'channel' => str_replace(self::LOG_FILE_EXTENSION, '', $this->filename), + 'datetime' => $this->getDateTime(), + 'extra' => [], + 'route' => \Phacil\Framework\startEngineExacTI::getRoute(), + ]; + + $trated = $this->logTreatment($record); + + $log = $this->interpolate($this->format, $trated); + + return $this->writeToFile($log); + } + + /** + * @param array $record + * @return array + * @throws \RuntimeException + */ + protected function logTreatment(array $record) + { + if (false === strpos($record['message'], '{')) { + return $record; + } + + try { + $replacements = []; + foreach ($record['context'] as $key => $val) { + $placeholder = '{' . $key . '}'; + if (strpos($record['message'], $placeholder) === false) { + continue; + } + + if (is_null($val) || is_scalar($val) || (is_object($val) && method_exists($val, "__toString"))) { + $replacements[$placeholder] = $val; + } elseif ($val instanceof \DateTimeInterface) { + $replacements[$placeholder] = $val->format(self::DATE_TIME_FORMAT); + } elseif ($val instanceof \UnitEnum) { + $replacements[$placeholder] = $val instanceof \BackedEnum ? $val->value : $val->name; + } elseif (is_object($val)) { + $replacements[$placeholder] = '[object ' . \get_class($val) . ']'; + } elseif (is_array($val)) { + $replacements[$placeholder] = 'array' . $this->toJson($val); + } else { + $replacements[$placeholder] = '[' . gettype($val) . ']'; + } + + if ($this->removeUsedContextFields) { + unset($record['context'][$key]); + } + } + + $record['message'] = strtr($record['message'], $replacements); + + } catch (\Exception $th) { + //throw $th; + $this->warning($th->getMessage()); + } + + return $record; + } + + /** + * @param mixed $data + */ + protected function convertToString($data) + { + if (null === $data || is_bool($data)) { + return var_export($data, true); + } + + if (is_scalar($data)) { + return (string) $data; + } + + return $this->toJson($data); + } + + /** + * Return the JSON representation of a value + * + * @param mixed $data + * @throws \RuntimeException if encoding fails and errors are not ignored + * @return string if encoding fails and ignoreErrors is true 'null' is returned + */ + protected function toJson($data) + { + return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION | JSON_PARTIAL_OUTPUT_ON_ERROR);; + } + + /** + * @param string $str + * @return string + * @throws \Phacil\Framework\Exception\RuntimeException + */ + protected function replaceNewlines($str) + { + if ($this->allowInlineLineBreaks) { + if (0 === strpos($str, '{')) { + $str = preg_replace('/(?replaceNewlines($this->convertToString($value)); + } + + protected function interpolate($format, $record) + { + $replace = []; + + foreach ($record as $key => $value) { + if (is_array($value)) { + $value = $this->convertToString($value); + } + $replace['%' . $key . '%'] = $value; + } + + return strtr($format, $replace); + } } diff --git a/system/system.php b/system/system.php index b92a5e1..95ef812 100644 --- a/system/system.php +++ b/system/system.php @@ -14,6 +14,7 @@ namespace Phacil\Framework; * @since 1.0.0 * @package Phacil\Framework * @property \Phacil\Framework\Api\Database $db + * @property \Phacil\Framework\Api\Log $log */ final class startEngineExacTI { @@ -497,26 +498,36 @@ if(!$engine->config->get('config_error_filename')){ // Error Handler set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$engine){ - + $showPrepend = false; switch ($errno) { case E_NOTICE: case E_USER_NOTICE: $error = 'Notice'; + $logFunction = 'notice'; break; case E_WARNING: case E_USER_WARNING: $error = 'Warning'; + $logFunction = 'warning'; break; case E_ERROR: + $error = 'Fatal Error'; + $logFunction = 'critical'; + break; case E_USER_ERROR: $error = 'Fatal Error'; + $logFunction = 'error'; break; case E_DEPRECATED: case E_USER_DEPRECATED: $error = 'Deprecated'; + $logFunction = 'warning'; + $showPrepend = true; break; default: $error = $engine->constantName($errno, 'Core'); + $logFunction = 'write'; + $showPrepend = true; break; } @@ -525,7 +536,7 @@ set_error_handler(function ($errno, $errstr, $errfile, $errline) use (&$engine){ } if ($engine->config->get('config_error_log')) { - $engine->log->write( $error . ': ' . $errstr . ' in ' . $errfile . ' on line ' . $errline.' | Phacil '.$engine->version(). ' on PHP '.$engine->phpversion); + $engine->log->$logFunction(($showPrepend ? $error . ': ' : '') . $errstr . ' in ' . $errfile . ' on line ' . $errline.' | Phacil '.$engine->version(). ' on PHP '.$engine->phpversion); } return true;