diff --git a/composer.json b/composer.json index 4f86390be9..45df5993b4 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,8 @@ "geoip2/geoip2": "^2.13", "jenssegers/agent": "^2.6", "php-di/php-di": "^6.4", - "twig/twig": "^3.0" + "twig/twig": "^3.0", + "monolog/monolog": "^3.9" }, "require-dev": { "phpstan/phpstan": "1.6.9", diff --git a/core/classes/Core/Config.php b/core/classes/Core/Config.php index b5e513132a..88a62c13a0 100644 --- a/core/classes/Core/Config.php +++ b/core/classes/Core/Config.php @@ -159,7 +159,7 @@ public static function setMultiple(array $values): void * Will log a warning if a legacy path (using `/` is used). * * @param string $path Path to parse. - * @return string|array Path split into sections or plain string if no section seperator was found. + * @return string|array Path split into sections or plain string if no section separator was found. */ private static function parsePath(string $path) { @@ -167,13 +167,6 @@ private static function parsePath(string $path) return explode('.', $path); } - // TODO: Remove for 2.1.0 - if (str_contains($path, '/')) { - ErrorHandler::logWarning("Legacy config path: {$path}. Please use periods to seperate paths."); - - return explode('/', $path); - } - return $path; } diff --git a/core/classes/Core/Module.php b/core/classes/Core/Module.php index 3aa60d2199..25a4b4d4d6 100644 --- a/core/classes/Core/Module.php +++ b/core/classes/Core/Module.php @@ -21,6 +21,7 @@ abstract class Module private string $_nameless_version; private array $_load_before; private array $_load_after; + protected Logger $_logger; public function __construct( Module $module, @@ -44,6 +45,8 @@ public function __construct( $this->_load_before = $load_before; $this->_load_after = $load_after; + + $this->_logger = new Logger($name); } /** @@ -229,4 +232,14 @@ public static function getNameFromId(int $id): ?string return null; } + + /** + * Get logger instance for module. + * + * @return Logger + */ + public function getLogger(): Logger + { + return $this->_logger; + } } diff --git a/core/classes/Core/User.php b/core/classes/Core/User.php index b0f101c82a..90139346e0 100644 --- a/core/classes/Core/User.php +++ b/core/classes/Core/User.php @@ -163,7 +163,10 @@ public function addGroup(int $group_id, int $expire = 0): bool $group = Group::find($group_id); if (!$group) { - ErrorHandler::logWarning('Could not add invalid group ' . $group_id . ' to user ' . $this->data()->id); + Logger::getDefaultLogger()->warning( + 'Could not add invalid group to user', + ['group_id' => $group_id, 'user_id' => $this->data()->id] + ); return false; } @@ -751,7 +754,10 @@ public function setGroup(int $group_id, int $expire = 0) { $group = Group::find($group_id); if (!$group) { - ErrorHandler::logWarning('Could not set invalid group ' . $group_id . ' to user ' . $this->data()->id); + Logger::getDefaultLogger()->warning( + 'Could not set invalid group for user', + ['group_id' => $group_id, 'user_id' => $this->data()->id] + ); return false; } diff --git a/core/classes/Logging/Logger.php b/core/classes/Logging/Logger.php new file mode 100644 index 0000000000..3a5ffd1542 --- /dev/null +++ b/core/classes/Logging/Logger.php @@ -0,0 +1,114 @@ +_name = $name; + + $this->_monolog = new MonologLogger($this->_name); + + // All logger instances must log to file + // It is possible for other modules to register custom handlers, however the file handler will always be present + $baseDir = implode(DIRECTORY_SEPARATOR, [ROOT_PATH, 'cache', 'logs', $this->_name]); + + // Debug log + if (defined('DEBUGGING') && DEBUGGING) { + $debugHandler = new MaxFileSizeLogHandler("$baseDir/debug.log", Level::Debug); + $debugFilter = new FilterHandler( + $debugHandler, + Level::Debug, + Level::Debug); + + $this->registerLogHandler($debugFilter); + } + + $infoHandler = new StreamHandler("$baseDir/info.log", Level::Info); + $infoFilter = new FilterHandler( + $infoHandler, + Level::Info, + Level::Warning); + + $errorHandler = new StreamHandler("$baseDir/error.log", Level::Error); + $errorFilter = new FilterHandler( + $errorHandler, + Level::Error, + Level::Critical); + + $this->registerLogHandler($infoFilter); + $this->registerLogHandler($errorFilter); + + // Debug Bar integration + if (defined('PHPDEBUGBAR')) { + DebugBarHelper::getInstance()->addMonologCollector($this->_monolog); + } + } + + public function registerLogHandler(HandlerInterface $handler): void + { + $this->_monolog->pushHandler($handler); + } + + public function debug(string $message, array $meta = []): void + { + $this->_monolog->debug($message, $meta); + } + + public function info(string $message, array $meta = []): void + { + $this->_monolog->info($message, $meta); + } + + public function notice(string $message, array $meta = []): void + { + $this->_monolog->notice($message, $meta); + } + + public function warning(string $message, array $meta = []): void + { + $this->_monolog->warning($message, $meta); + } + + public function error(string $message, array $meta = []): void + { + $this->_monolog->error($message, $meta); + } + + public function critical(string $message, array $meta = []): void + { + $this->_monolog->critical($message, $meta); + } + + public static function getDefaultLogger(): ?Logger + { + return self::$_defaultLogger; + } + + public static function setDefaultLogger(Logger $logger): void + { + if (isset(self::$_defaultLogger)) { + throw new Exception('Cannot change the default logger once it is set'); + } + + self::$_defaultLogger = $logger; + } +} diff --git a/core/classes/Logging/MaxFileSizeLogHandler.php b/core/classes/Logging/MaxFileSizeLogHandler.php new file mode 100644 index 0000000000..1e529e0dad --- /dev/null +++ b/core/classes/Logging/MaxFileSizeLogHandler.php @@ -0,0 +1,143 @@ +fileName = Utils::canonicalizePath($fileName); + $this->maxFiles = $maxFiles; + $this->maxFileSize = $maxFileSize; + + parent::__construct($this->getNewFileName(), $level, $bubble); + } + + public function close(): void + { + parent::close(); + + if ($this->mustRotate === true) { + $this->rotate(); + } + } + + protected function write(LogRecord $record): void + { + // If the log is new then we need to rotate such that the file will exist + if ($this->mustRotate === null) { + $this->mustRotate = $this->url === null || !file_exists($this->url); + } + + // Rotate now if the file size is too big + if (file_exists($this->url) && filesize($this->url) > $this->maxFileSize) { + $this->mustRotate = true; + + $this->close(); + } + + parent::write($record); + + if ($this->mustRotate === true) { + $this->close(); + } + } + + /** + * Rotates the files. + */ + protected function rotate(): void + { + $this->url = $this->getNewFileName(); + + $this->mustRotate = false; + + if ($this->maxFiles === 0) { + return; + } + + $logFiles = glob($this->getGlobPattern()); + + if ($logFiles === false) { + return; + } + + // If we have reached the maximum number of allowed files, delete older files + if ($this->maxFiles < count($logFiles)) { + usort($logFiles, function ($a, $b) { + if (filemtime($a) < filemtime($b)) { + return 1; + } + + return -1; + }); + + foreach (array_slice($logFiles, $this->maxFiles) as $file) { + if (is_writable($file)) { + unlink($file); + } + } + } + + // Rename the log file to -old-