diff --git a/application/Espo/Core/Loaders/Log.php b/application/Espo/Core/Loaders/Log.php index bba598ea51..a72bfef0a8 100644 --- a/application/Espo/Core/Loaders/Log.php +++ b/application/Espo/Core/Loaders/Log.php @@ -30,51 +30,22 @@ namespace Espo\Core\Loaders; use Espo\Core\{ - Utils\Config, + Log\LogLoader, Utils\Log as LogService, }; -use Espo\Core\Utils\Log\Monolog\Handler\{ - EspoRotatingFileHandler, - EspoFileHandler, -}; - -use Monolog\ErrorHandler as MonologErrorHandler; - class Log implements Loader { - protected $config; + protected $logLoader; - public function __construct(Config $config) + public function __construct(LogLoader $logLoader) { - $this->config = $config; + $this->logLoader = $logLoader; } public function load() : LogService { - $config = $this->config; - - $path = $config->get('logger.path', 'data/logs/espo.log'); - $rotation = $config->get('logger.rotation', true); - - $log = new LogService('Espo'); - - $levelCode = $log::toMonologLevel($config->get('logger.level', 'WARNING')); - - if ($rotation) { - $maxFileNumber = $config->get('logger.maxFileNumber', 30); - - $handler = new EspoRotatingFileHandler($path, $maxFileNumber, $levelCode); - } else { - $handler = new EspoFileHandler($path, $levelCode); - } - - $log->pushHandler($handler); - - $errorHandler = new MonologErrorHandler($log); - - $errorHandler->registerExceptionHandler(null, false); - $errorHandler->registerErrorHandler([], false); + $log = $this->logLoader->load(); $GLOBALS['log'] = $log; diff --git a/application/Espo/Core/Log/DefaultHandlerLoader.php b/application/Espo/Core/Log/DefaultHandlerLoader.php new file mode 100644 index 0000000000..17285b1a18 --- /dev/null +++ b/application/Espo/Core/Log/DefaultHandlerLoader.php @@ -0,0 +1,115 @@ +createInstance($className, $params); + + $formatter = $this->loadFormatter($data); + + if ($formatter) { + $handler->setFormatter($formatter); + } + + return $handler; + } + + protected function loadFormatter(array $data) : ?FormatterInterface + { + $formatterData = $data['formatter'] ?? null; + + if (!$formatterData || !is_array($formatterData)) { + return null; + } + + $className = $formatterData['className'] ?? null; + + if (!$className) { + return null; + } + + $params = $formatterData['params'] ?? []; + + return $this->createInstance($className, $params); + } + + protected function createInstance(string $className, array $params) : object + { + $class = new ReflectionClass($className); + + $constructor = $class->getConstructor(); + + $argumentList = []; + + foreach ($constructor->getParameters() as $parameter) { + $name = $parameter->getName(); + + if (array_key_exists($name, $params)) { + $value = $params[$name]; + } + else if ($parameter->isDefaultValueAvailable()) { + $value = $parameter->getDefaultValue(); + } else { + continue; + } + + $argumentList[] = $value; + } + + return $class->newInstanceArgs($argumentList); + } +} diff --git a/application/Espo/Core/Log/EspoRotatingFileHandlerLoader.php b/application/Espo/Core/Log/EspoRotatingFileHandlerLoader.php new file mode 100644 index 0000000000..1524770e18 --- /dev/null +++ b/application/Espo/Core/Log/EspoRotatingFileHandlerLoader.php @@ -0,0 +1,50 @@ +fileManager = new FileManager(); } - protected function getFileManager() - { - return $this->fileManager; - } - - protected function write(array $record) + protected function write(array $record): void { if (!$this->url) { throw new LogicException( - 'Missing logger path, the stream can not be opened. Please check logger options in the data/config.php.' + 'Missing logger path. Check logger params in the data/config.php.' ); } $this->errorMessage = null; if (!is_writable($this->url)) { - $this->getFileManager()->checkCreateFile($this->url); + $this->fileManager->checkCreateFile($this->url); } if (is_writable($this->url)) { set_error_handler([$this, 'customErrorHandler']); - $this->getFileManager()->appendContents($this->url, $this->pruneMessage($record)); + $this->fileManager->appendContents($this->url, $this->pruneMessage($record)); restore_error_handler(); } if (isset($this->errorMessage)) { throw new UnexpectedValueException( - sprintf('The stream or file "%s" could not be opened: '.$this->errorMessage, $this->url) + sprintf('File "%s" could not be opened: ' . $this->errorMessage, $this->url) ); } } @@ -100,4 +97,4 @@ class EspoFileHandler extends MonologStreamHandler return (string) $record['formatted']; } -} \ No newline at end of file +} diff --git a/application/Espo/Core/Utils/Log/Monolog/Handler/EspoRotatingFileHandler.php b/application/Espo/Core/Log/Handler/EspoRotatingFileHandler.php similarity index 84% rename from application/Espo/Core/Utils/Log/Monolog/Handler/EspoRotatingFileHandler.php rename to application/Espo/Core/Log/Handler/EspoRotatingFileHandler.php index 81b88373a0..e8a89f2a7a 100644 --- a/application/Espo/Core/Utils/Log/Monolog/Handler/EspoRotatingFileHandler.php +++ b/application/Espo/Core/Log/Handler/EspoRotatingFileHandler.php @@ -27,7 +27,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -namespace Espo\Core\Utils\Log\Monolog\Handler; +namespace Espo\Core\Log\Handler; use Monolog\Logger; @@ -41,7 +41,7 @@ class EspoRotatingFileHandler extends EspoFileHandler protected $maxFiles; - public function __construct($filename, $maxFiles = 0, $level = Logger::DEBUG, $bubble = true) + public function __construct(string $filename, int $maxFiles = 0, $level = Logger::DEBUG, bool $bubble = true) { $this->filename = $filename; $this->maxFiles = (int) $maxFiles; @@ -51,12 +51,6 @@ class EspoRotatingFileHandler extends EspoFileHandler $this->rotate(); } - public function setFilenameFormat($filenameFormat, $dateFormat) - { - $this->filenameFormat = $filenameFormat; - $this->dateFormat = $dateFormat; - } - protected function rotate() { if (0 === $this->maxFiles) { @@ -64,18 +58,17 @@ class EspoRotatingFileHandler extends EspoFileHandler } $filePattern = $this->getFilePattern(); - $dirPath = $this->getFileManager()->getDirName($this->filename); - $logFiles = $this->getFileManager()->getFileList($dirPath, false, $filePattern, true); + $dirPath = $this->fileManager->getDirName($this->filename); + $logFiles = $this->fileManager->getFileList($dirPath, false, $filePattern, true); if (!empty($logFiles) && count($logFiles) > $this->maxFiles) { - usort($logFiles, function ($a, $b) { return strcmp($b, $a); }); $logFilesToBeRemoved = array_slice($logFiles, $this->maxFiles); - $this->getFileManager()->removeFile($logFilesToBeRemoved, $dirPath); + $this->fileManager->removeFile($logFilesToBeRemoved, $dirPath); } } @@ -114,4 +107,4 @@ class EspoRotatingFileHandler extends EspoFileHandler return $glob; } -} \ No newline at end of file +} diff --git a/application/Espo/Core/Log/HandlerListLoader.php b/application/Espo/Core/Log/HandlerListLoader.php new file mode 100644 index 0000000000..c5b666daf6 --- /dev/null +++ b/application/Espo/Core/Log/HandlerListLoader.php @@ -0,0 +1,92 @@ +injectableFactory = $injectableFactory; + $this->defaultLoader = $defaultLoader; + } + + public function load(array $dataList, ?string $defaultLevel = null) : array + { + $handlerList = []; + + foreach ($dataList as $item) { + $handler = $this->loadHandler($item, $defaultLevel); + + if (!$handler) { + continue; + } + + $handlerList[] = $handler; + } + + return $handlerList; + } + + protected function loadHandler(array $data, ?string $defaultLevel = null) : ?HandlerInterface + { + $params = $data['params'] ?? []; + + $level = $data['level'] ?? $defaultLevel; + + if ($level) { + $params['level'] = Logger::toMonologLevel($level); + } + + $loaderClassName = $data['loaderClassName'] ?? null; + + if ($loaderClassName) { + $loader = $this->injectableFactory->create($loaderClassName); + + $handler = $loader->load($params); + + return $handler; + } + + $handler = $this->defaultLoader->load($data, $defaultLevel); + + return $handler; + } +} diff --git a/application/Espo/Core/Log/HandlerLoader.php b/application/Espo/Core/Log/HandlerLoader.php new file mode 100644 index 0000000000..4c0fedd4f4 --- /dev/null +++ b/application/Espo/Core/Log/HandlerLoader.php @@ -0,0 +1,39 @@ +config = $config; + $this->injectableFactory = $injectableFactory; + } + + public function load() : Log + { + $log = new Log('Espo'); + + $handlerDataList = $this->config->get('logger.handlerList') ?? null; + + if ($handlerDataList) { + $level = $this->config->get('logger.level'); + + $loader = $this->injectableFactory->create(HandlerListLoader::class); + + $handlerList = $loader->load($handlerDataList, $level); + } + else { + $handler = $this->createDefaultHandler(); + + $handlerList = [$handler]; + } + + foreach ($handlerList as $handler) { + $log->pushHandler($handler); + } + + $errorHandler = new MonologErrorHandler($log); + + $errorHandler->registerExceptionHandler(null, false); + $errorHandler->registerErrorHandler([], false); + + return $log; + } + + protected function createDefaultHandler() : HandlerInterface + { + $path = $this->config->get('logger.path') ?? self::PATH; + $rotation = $this->config->get('logger.rotation') ?? true; + $level = $this->config->get('logger.level') ?? self::DEFAULT_LEVEL; + + $levelCode = Logger::toMonologLevel($level); + + if ($rotation) { + $maxFileNumber = $this->config->get('logger.maxFileNumber') ?? self::MAX_FILE_NUMBER; + + $handler = new EspoRotatingFileHandler($path, $maxFileNumber, $levelCode); + } else { + $handler = new EspoFileHandler($path, $levelCode); + } + + $formatter = new LineFormatter( + self::LINE_FORMAT, + self::DATE_FORMAT + ); + + $handler->setFormatter($formatter); + + return $handler; + } +} diff --git a/composer.json b/composer.json index 3fc0b6e5fa..8dd1374237 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "laminas/laminas-ldap": "2.*", "laminas/laminas-mime": "^2.7.1", "laminas/laminas-stdlib": "^3.1", - "monolog/monolog": "1.*", + "monolog/monolog": "2.*", "yzalis/identicon": "*", "zordius/lightncandy": "1.*", "composer/semver": "^3", diff --git a/composer.lock b/composer.lock index 1b38412511..d7a3ec7063 100644 --- a/composer.lock +++ b/composer.lock @@ -1,10 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f4230248c7de33c9a74a7fc1c6592a5c", + "content-hash": "e164d331463e4b99a826e2bab0006784", "packages": [ { "name": "cboden/ratchet", @@ -119,6 +119,20 @@ "validation", "versioning" ], + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], "time": "2020-05-26T18:22:04+00:00" }, { @@ -670,6 +684,12 @@ "cron", "schedule" ], + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], "time": "2020-08-21T02:30:13+00:00" }, { @@ -1453,55 +1473,58 @@ }, { "name": "monolog/monolog", - "version": "1.20.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "55841909e2bcde01b5318c35f2b74f8ecc86e037" + "reference": "f9eee5cec93dfb313a38b6b288741e84e53f02d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/55841909e2bcde01b5318c35f2b74f8ecc86e037", - "reference": "55841909e2bcde01b5318c35f2b74f8ecc86e037", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f9eee5cec93dfb313a38b6b288741e84e53f02d5", + "reference": "f9eee5cec93dfb313a38b6b288741e84e53f02d5", "shasum": "" }, "require": { - "php": ">=5.3.0", - "psr/log": "~1.0" + "php": ">=7.2", + "psr/log": "^1.0.1" }, "provide": { "psr/log-implementation": "1.0.0" }, "require-dev": { - "aws/aws-sdk-php": "^2.4.9", + "aws/aws-sdk-php": "^2.4.9 || ^3.0", "doctrine/couchdb": "~1.0@dev", - "graylog2/gelf-php": "~1.0", - "jakub-onderka/php-parallel-lint": "0.9", + "elasticsearch/elasticsearch": "^6.0", + "graylog2/gelf-php": "^1.4.2", "php-amqplib/php-amqplib": "~2.4", "php-console/php-console": "^3.1.3", - "phpunit/phpunit": "~4.5", - "phpunit/phpunit-mock-objects": "2.3.0", + "php-parallel-lint/php-parallel-lint": "^1.0", + "phpspec/prophecy": "^1.6.1", + "phpunit/phpunit": "^8.5", + "predis/predis": "^1.1", + "rollbar/rollbar": "^1.3", "ruflin/elastica": ">=0.90 <3.0", - "sentry/sentry": "^0.13", - "swiftmailer/swiftmailer": "~5.3" + "swiftmailer/swiftmailer": "^5.3|^6.0" }, "suggest": { "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", - "ext-mongo": "Allow sending log messages to a MongoDB server", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", - "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", "php-console/php-console": "Allow sending log messages to Google Chrome", "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server", - "sentry/sentry": "Allow sending log messages to a Sentry server" + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { @@ -1527,7 +1550,17 @@ "logging", "psr-3" ], - "time": "2016-07-02T14:02:10+00:00" + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2020-07-23T08:41:23+00:00" }, { "name": "nesbot/carbon", @@ -2813,6 +2846,16 @@ "micro", "router" ], + "funding": [ + { + "url": "https://opencollective.com/slimphp", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slim/slim", + "type": "tidelift" + } + ], "time": "2020-04-14T20:49:48+00:00" }, { @@ -2922,6 +2965,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-03-30T14:07:33+00:00" }, { @@ -4545,16 +4602,16 @@ }, { "name": "phpunit/phpunit", - "version": "8.5.3", + "version": "8.5.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "67750516bc02f300e2742fed2f50177f8f37bedf" + "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/67750516bc02f300e2742fed2f50177f8f37bedf", - "reference": "67750516bc02f300e2742fed2f50177f8f37bedf", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/34c18baa6a44f1d1fbf0338907139e9dce95b997", + "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997", "shasum": "" }, "require": { @@ -4624,7 +4681,17 @@ "testing", "xunit" ], - "time": "2020-03-31T08:52:04+00:00" + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-22T07:06:58+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -5407,5 +5474,6 @@ "ext-curl": "*", "ext-exif": "*" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "1.1.0" } diff --git a/tests/unit/Espo/Core/Log/HandlerListLoaderTest.php b/tests/unit/Espo/Core/Log/HandlerListLoaderTest.php new file mode 100644 index 0000000000..a84b7ec9ea --- /dev/null +++ b/tests/unit/Espo/Core/Log/HandlerListLoaderTest.php @@ -0,0 +1,125 @@ +injectableFactory = $this->getMockBuilder(InjectableFactory::class) + ->disableOriginalConstructor() + ->getMock(); + } + + public function testLoad1() + { + $defaultLoader = $this->getMockBuilder(DefaultHandlerLoader::class) + ->disableOriginalConstructor() + ->getMock(); + + $listLoader = new HandlerListLoader($this->injectableFactory, $defaultLoader); + + $dataList = [ + [ + 'className' => 'Espo\\Core\\Log\\Handler\\EspoRotatingFileHandler', + 'params' => [ + 'filename' => 'data/logs/test-1.log', + ], + 'level' => 'DEBUG', + 'formatter' => [ + 'className' => 'Monolog\\Formatter\\LineFormatter', + 'params' => [ + 'dateFormat' => 'Y-m-d H:i:s', + ], + ] + ], + [ + 'loaderClassName' => EspoRotatingFileHandlerLoader::class, + 'params' => [ + 'filename' => 'data/logs/test-2.log', + ], + 'level' => 'NOTICE', + ], + ]; + + $handler1 = $this->getMockBuilder(EspoRotatingFileHandler::class) + ->disableOriginalConstructor() + ->getMock(); + + $defaultLoader + ->expects($this->once()) + ->method('load') + ->with($dataList[0], 'NOTICE') + ->willReturn($handler1); + + $loader = $this->getMockBuilder(EspoRotatingFileHandlerLoader::class) + ->disableOriginalConstructor() + ->getMock(); + + $handler = $this->getMockBuilder(EspoRotatingFileHandler::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->injectableFactory + ->expects($this->once()) + ->method('create') + ->with(EspoRotatingFileHandlerLoader::class) + ->willReturn($loader); + + $params = [ + 'filename' => 'data/logs/test-2.log', + 'level' => Logger::NOTICE, + ]; + + $loader + ->expects($this->once()) + ->method('load') + ->with($params) + ->willReturn($handler); + + $list = $listLoader->load($dataList, 'NOTICE'); + + $this->assertEquals(2, count($list)); + + $this->assertInstanceOf(EspoRotatingFileHandler::class, $list[0]); + } +}