From 3c49d3eee4aa6b4b33bda2a02c2b5516c21eb5d6 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Tue, 11 Jun 2024 16:47:23 -0400 Subject: [PATCH 1/5] Use DB log target --- .../m240611_134740_create_logs_table.php | 50 ++++ src/services/Logs.php | 274 ++++++------------ 2 files changed, 143 insertions(+), 181 deletions(-) create mode 100644 src/migrations/m240611_134740_create_logs_table.php diff --git a/src/migrations/m240611_134740_create_logs_table.php b/src/migrations/m240611_134740_create_logs_table.php new file mode 100644 index 00000000..670cc125 --- /dev/null +++ b/src/migrations/m240611_134740_create_logs_table.php @@ -0,0 +1,50 @@ +db->tableExists(self::LOG_TABLE)) { + return true; + } + + // @see \craft\feedme\migrations\m240611_134740_create_logs_table + $this->createTable(self::LOG_TABLE, [ + 'id' => $this->bigPrimaryKey(), + 'level' => $this->integer(), + 'category' => $this->string(), + 'log_time' => $this->double(), + 'prefix' => $this->text(), + 'message' => $this->text(), + ]); + + $this->createIndex('idx_log_level', self::LOG_TABLE, 'level'); + $this->createIndex('idx_log_category', self::LOG_TABLE, 'category'); + + return true; + } + + /** + * @inheritdoc + */ + public function safeDown(): bool + { + if ($this->db->tableExists(self::LOG_TABLE)) { + $this->dropTable(self::LOG_TABLE); + } + + return true; + } +} diff --git a/src/services/Logs.php b/src/services/Logs.php index 490cdafb..3b1b9a40 100644 --- a/src/services/Logs.php +++ b/src/services/Logs.php @@ -4,12 +4,16 @@ use Craft; use craft\base\Component; +use craft\db\Query; use craft\feedme\Plugin; use craft\helpers\App; -use craft\helpers\FileHelper; -use craft\helpers\Json; -use DateTime; +use craft\helpers\Db; use Exception; +use Illuminate\Support\Collection; +use samdark\log\PsrMessage; +use yii\base\InvalidArgumentException; +use yii\log\DbTarget; +use yii\log\Logger; class Logs extends Component { @@ -51,6 +55,16 @@ class Logs extends Component */ public mixed $logFile = null; + public const LOG_CATEGORY = 'feed-me'; + public const LOG_TABLE = '{{%feedme_logs}}'; + + public const LOG_LEVEL_MAP = [ + Logger::LEVEL_ERROR => 'error', + Logger::LEVEL_WARNING => 'warning', + Logger::LEVEL_INFO => 'info', + Logger::LEVEL_TRACE => 'trace', + Logger::LEVEL_PROFILE => 'profile', + ]; // Public Methods // ========================================================================= @@ -60,7 +74,18 @@ class Logs extends Component */ public function init(): void { - $this->logFile = Craft::$app->path->getLogPath() . '/feedme.log'; + Craft::$app->getLog()->targets['feed-me'] = Craft::createObject([ + 'class' => DbTarget::class, + 'logTable' => self::LOG_TABLE, + 'levels' => $this->getLogLevels(), + 'enabled' => $this->isEnabled(), + 'categories' => [self::LOG_CATEGORY], + 'prefix' => static function(array $message) { + /** @var PsrMessage $psrMessage */ + $psrMessage = unserialize($message[0]); + return $psrMessage->getContext()['feed'] ?? ''; + }, + ]); } /** @@ -72,35 +97,21 @@ public function init(): void */ public function log($method, $message, array $params = [], array $options = []): void { - $dateTime = new DateTime(); - $type = explode('::', $method)[1]; + $level = explode('::', $method)[1]; $message = Craft::t('feed-me', $message, $params); - // Make sure to check if we should log anything - if (!$this->_canLog($type)) { - return; - } - - // Always prepend the feed we're dealing with - if (Plugin::$feedName) { - $message = Plugin::$feedName . ': ' . $message; - } - - $options = array_merge([ - 'date' => $dateTime->format('Y-m-d H:i:s'), - 'type' => $type, - 'message' => $message, - ], $options); - - // If we're not explicitly sending a key for logging, check if we've started a feed. - // If we have, our $stepKey variable will have a value and can use it here. - if (!isset($options['key']) && Plugin::$stepKey) { - $options['key'] = Plugin::$stepKey; - } + $context = [ + 'feed' => Plugin::$feedName, + 'key' => $options['key'] ?? Plugin::$stepKey, + ]; - $options = Json::encode($options); + $psrMessage = new PsrMessage($message, $context); - $this->_export($options . PHP_EOL); + Craft::getLogger()->log( + serialize($psrMessage), + self::logLevelInt($level), + self::LOG_CATEGORY, + ); } /** @@ -108,7 +119,9 @@ public function log($method, $message, array $params = [], array $options = []): */ public function clear(): void { - $this->_clearLogFile($this->logFile); + Craft::$app->getDb()->createCommand() + ->truncateTable(self::LOG_TABLE) + ->execute(); } /** @@ -118,180 +131,79 @@ public function clear(): void */ public function getLogEntries($type = null): array { - $logEntries = []; - - App::maxPowerCaptain(); - - if (@file_exists(Craft::$app->path->getLogPath())) { - if (@file_exists($this->logFile)) { - // Split the log file's contents up into arrays where every line is a new item - $contents = @file_get_contents($this->logFile); - $lines = explode("\n", $contents); - - foreach ($lines as $line) { - $json = Json::decode($line); - - if (!$json) { - continue; - } - - if ($type && $json['type'] !== $type) { - continue; - } - - if (isset($json['date'])) { - $json['date'] = DateTime::createFromFormat('Y-m-d H:i:s', $json['date'])->format('Y-m-d H:i:s'); - } - - // Backward compatibility - $key = $json['key'] ?? count($logEntries); + $query = (new Query()) + ->select('*') + ->where(['category' => self::LOG_CATEGORY]) + ->orderBy(['log_time' => SORT_DESC]) + ->from(self::LOG_TABLE); + + if ($type) { + $query->andWhere(['level' => self::logLevelInt($type)]); + } - if (isset($logEntries[$key])) { - $logEntries[$key]['items'][] = $json; - } else { - $logEntries[$key] = $json; - } - } + $logEntries = $query->collect()->reduce(function(Collection $logs, array $row) { + $psrMessage = unserialize($row['message']); + $key = $psrMessage->getContext()['key'] ?? $logs->count(); + $log = [ + 'type' => self::logLevelName($row['level']), + 'date' => Db::prepareDateForDb($row['log_time']), + 'message' => $psrMessage->getMessage(), + 'key' => $key, + ]; + + if ($logs->has($key)) { + $parentLog = $logs->get($key); + $parentLog['items'][] = $log; + $logs->put($key, $parentLog); + } else { + $logs->put($key, $log); } - // Resort log entries: latest entries first - $logEntries = array_reverse($logEntries); - } + return $logs; + }, Collection::make()); - return $logEntries; + return $logEntries->all(); } // Private Methods // ========================================================================= - /** - * @param $type - * @return bool - */ - private function _canLog($type): bool + private function isEnabled(): bool { - $loggingConfig = Plugin::$plugin->service->getConfig('logging'); - - // parse the config value because it need to allow for strings too to support 'error' level - $logging = App::parseBooleanEnv($loggingConfig); - if ($logging === null) { - $logging = $loggingConfig; - } + $config = Plugin::$plugin->service->getConfig('logging'); - // If logging set to false, don't log anything - if ($logging === false) { - return false; - } - - if ($type === 'info' && $logging === 'error') { - return false; - } - - return true; + return App::parseBooleanEnv($config) ?? true; } - /** - * @param $text - * @throws \yii\base\Exception - */ - private function _export($text): void + private function getLogLevels(): array { - $logPath = dirname($this->logFile); - FileHelper::createDirectory($logPath, $this->dirMode, true); + $config = Plugin::$plugin->service->getConfig('logging'); - if (($fp = @fopen($this->logFile, 'ab')) === false) { - throw new Exception("Unable to append to log file: {$this->logFile}"); - } - @flock($fp, LOCK_EX); - if ($this->enableRotation) { - // clear stat cache to ensure getting the real current file size and not a cached one - // this may result in rotating twice when cached file size is used on subsequent calls - clearstatcache(); - } - if ($this->enableRotation && @filesize($this->logFile) > $this->maxFileSize * 1024) { - $this->_rotateFiles(); - @flock($fp, LOCK_UN); - @fclose($fp); - $writeResult = @file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX); - if ($writeResult === false) { - $error = error_get_last(); - throw new Exception("Unable to export log through file!: {$error['message']}"); - } - $textSize = strlen($text); - if ($writeResult < $textSize) { - throw new Exception("Unable to export whole log through file! Wrote $writeResult out of $textSize bytes."); - } - } else { - $writeResult = @fwrite($fp, $text); - if ($writeResult === false) { - $error = error_get_last(); - throw new Exception("Unable to export log through file!: {$error['message']}"); - } - $textSize = strlen($text); - if ($writeResult < $textSize) { - throw new Exception("Unable to export whole log through file! Wrote $writeResult out of $textSize bytes."); - } - @flock($fp, LOCK_UN); - @fclose($fp); - } - if ($this->fileMode !== null) { - @chmod($this->logFile, $this->fileMode); - } + return match ($config) { + 'error' => ['error'], + default => [], + }; } - /** - * - */ - private function _rotateFiles(): void + private function logLevelInt(string $level): int { - $file = $this->logFile; - for ($i = $this->maxLogFiles; $i >= 0; --$i) { - // $i == 0 is the original log file - $rotateFile = $file . ($i === 0 ? '' : '.' . $i); - if (is_file($rotateFile)) { - // suppress errors because it's possible multiple processes enter into this section - if ($i === $this->maxLogFiles) { - @unlink($rotateFile); - continue; - } - $newFile = $this->logFile . '.' . ($i + 1); - $this->rotateByCopy ? $this->_rotateByCopy($rotateFile, $newFile) : $this->_rotateByRename($rotateFile, $newFile); - if ($i === 0) { - $this->_clearLogFile($rotateFile); - } - } - } + return match ($level) { + 'error' => Logger::LEVEL_ERROR, + 'warning' => Logger::LEVEL_WARNING, + 'info' => Logger::LEVEL_INFO, + 'trace' => Logger::LEVEL_TRACE, + 'profile' => Logger::LEVEL_PROFILE, + }; } - /** - * @param $rotateFile - */ - private function _clearLogFile($rotateFile): void + private static function logLevelName(int $level): string { - if ($filePointer = @fopen($rotateFile, 'ab')) { - @ftruncate($filePointer, 0); - @fclose($filePointer); - } - } + $level = self::LOG_LEVEL_MAP[$level] ?? null; - /** - * @param $rotateFile - * @param $newFile - */ - private function _rotateByCopy($rotateFile, $newFile): void - { - @copy($rotateFile, $newFile); - if ($this->fileMode !== null) { - @chmod($newFile, $this->fileMode); + if ($level === null) { + throw new InvalidArgumentException("Invalid log level: $level"); } - } - /** - * @param $rotateFile - * @param $newFile - */ - private function _rotateByRename($rotateFile, $newFile): void - { - @rename($rotateFile, $newFile); + return $level; } } From 8295b436768cf9fc9a8d185dab56271ad64c4bbe Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Tue, 11 Jun 2024 17:19:53 -0400 Subject: [PATCH 2/5] Ditch Psr --- src/services/Logs.php | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/services/Logs.php b/src/services/Logs.php index 3b1b9a40..dea751d1 100644 --- a/src/services/Logs.php +++ b/src/services/Logs.php @@ -8,9 +8,9 @@ use craft\feedme\Plugin; use craft\helpers\App; use craft\helpers\Db; +use craft\helpers\Json; use Exception; use Illuminate\Support\Collection; -use samdark\log\PsrMessage; use yii\base\InvalidArgumentException; use yii\log\DbTarget; use yii\log\Logger; @@ -81,9 +81,9 @@ public function init(): void 'enabled' => $this->isEnabled(), 'categories' => [self::LOG_CATEGORY], 'prefix' => static function(array $message) { - /** @var PsrMessage $psrMessage */ - $psrMessage = unserialize($message[0]); - return $psrMessage->getContext()['feed'] ?? ''; + $log = Json::decodeIfJson($message[0]); + $feed = $log['feed'] ?? null; + return $feed ? "[$feed]" : ''; }, ]); } @@ -100,15 +100,14 @@ public function log($method, $message, array $params = [], array $options = []): $level = explode('::', $method)[1]; $message = Craft::t('feed-me', $message, $params); - $context = [ + $json = [ + 'message' => $message, 'feed' => Plugin::$feedName, 'key' => $options['key'] ?? Plugin::$stepKey, ]; - $psrMessage = new PsrMessage($message, $context); - Craft::getLogger()->log( - serialize($psrMessage), + Json::encode($json), self::logLevelInt($level), self::LOG_CATEGORY, ); @@ -142,12 +141,12 @@ public function getLogEntries($type = null): array } $logEntries = $query->collect()->reduce(function(Collection $logs, array $row) { - $psrMessage = unserialize($row['message']); - $key = $psrMessage->getContext()['key'] ?? $logs->count(); + $json = Json::decodeIfJson($row['message']); + $key = $json['key'] ?? $logs->count(); $log = [ 'type' => self::logLevelName($row['level']), 'date' => Db::prepareDateForDb($row['log_time']), - 'message' => $psrMessage->getMessage(), + 'message' => $json['message'] ?? $json, 'key' => $key, ]; From 361f66f19e71997ea8740837698006b902987c8d Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Wed, 12 Jun 2024 15:47:07 -0400 Subject: [PATCH 3/5] Install migration --- src/migrations/Install.php | 14 ++++++++++++++ .../m240611_134740_create_logs_table.php | 16 ++++------------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/migrations/Install.php b/src/migrations/Install.php index 8401f268..4cdd513a 100644 --- a/src/migrations/Install.php +++ b/src/migrations/Install.php @@ -52,10 +52,24 @@ protected function createTables(): void 'dateUpdated' => $this->dateTime()->notNull(), 'uid' => $this->uid(), ]); + + // @see \craft\feedme\migrations\m240611_134740_create_logs_table + $this->createTable('{{%feedme_logs}}', [ + 'id' => $this->bigPrimaryKey(), + 'level' => $this->integer(), + 'category' => $this->string(), + 'log_time' => $this->double(), + 'prefix' => $this->text(), + 'message' => $this->text(), + ]); + + $this->createIndex('idx_log_level', '{{%feedme_logs}}', 'level'); + $this->createIndex('idx_log_category', '{{%feedme_logs}}', 'category'); } protected function removeTables(): void { $this->dropTableIfExists('{{%feedme_feeds}}'); + $this->dropTableIfExists('{{%feedme_logs}}'); } } diff --git a/src/migrations/m240611_134740_create_logs_table.php b/src/migrations/m240611_134740_create_logs_table.php index 670cc125..53bd958e 100644 --- a/src/migrations/m240611_134740_create_logs_table.php +++ b/src/migrations/m240611_134740_create_logs_table.php @@ -9,19 +9,13 @@ */ class m240611_134740_create_logs_table extends Migration { - public const LOG_TABLE = '{{%feedme_logs}}'; - /** * @inheritdoc */ public function safeUp(): bool { - if ($this->db->tableExists(self::LOG_TABLE)) { - return true; - } - // @see \craft\feedme\migrations\m240611_134740_create_logs_table - $this->createTable(self::LOG_TABLE, [ + $this->createTable('{{%feedme_logs}}', [ 'id' => $this->bigPrimaryKey(), 'level' => $this->integer(), 'category' => $this->string(), @@ -30,8 +24,8 @@ public function safeUp(): bool 'message' => $this->text(), ]); - $this->createIndex('idx_log_level', self::LOG_TABLE, 'level'); - $this->createIndex('idx_log_category', self::LOG_TABLE, 'category'); + $this->createIndex('idx_log_level', '{{%feedme_logs}}', 'level'); + $this->createIndex('idx_log_category', '{{%feedme_logs}}', 'category'); return true; } @@ -41,9 +35,7 @@ public function safeUp(): bool */ public function safeDown(): bool { - if ($this->db->tableExists(self::LOG_TABLE)) { - $this->dropTable(self::LOG_TABLE); - } + $this->dropTableIfExists('{{%feedme_logs}}'); return true; } From df32f4b433ec49ac131a773a9934dcdef7f51d2e Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Wed, 12 Jun 2024 15:47:12 -0400 Subject: [PATCH 4/5] Logging example --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index d798504b..57c5bd0d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,33 @@ composer require craftcms/feed-me ./craft plugin/install feed-me ``` +## Customizing Logs + +As of version `5.6`/`6.2`, logging is handled by Craft's log component and stored in the database instead of the filesystem. +If you want them logged to files (or anywhere else), you can add your own log target in your `config/app.php` file: + +```php +return [ + 'components' => [ + 'log' => [ + 'monologTargetConfig' => [ + // optionally, omit from Craft's default logs + 'except' => ['feed-me'], + ], + + // add your own log target to write logs to file + 'targets' => [ + [ + 'class' => \yii\log\FileTarget::class, + 'logFile' => '@storage/logs/feed-me.log', + 'categories' => ['feed-me'], + ], + ], + ], + ], +]; +``` + ## Resources - **[Feed Me Plugin Page](https://plugins.craftcms.com/feed-me)** – The official plugin page for Feed Me From fdf2abdc5488c45b3b73cca264c61d9d5c3887e3 Mon Sep 17 00:00:00 2001 From: Tim Kelty Date: Wed, 12 Jun 2024 16:04:13 -0400 Subject: [PATCH 5/5] Changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e26640f7..92810113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - Fixed an error that would occur when running a feed with the backup database setting enabled, when Craft's `backupCommand` was set to false. ([#1461](https://github.com/craftcms/feed-me/pull/1461)) +- Logs now use the default log component, and are stored in the database. [#1344](https://github.com/craftcms/feed-me/issues/1344) ## 5.5.0 - 2024-05-26 @@ -159,4 +160,4 @@ - The `data`, `elements`, `feeds`, `fields`, `logs`, `process`, and `service` components can now be configured via `craft\services\Plugins::$pluginConfigs`. ### Removed -- Removed built-in support for the Verbb Comments plugin, which provides its own Feed Me driver. \ No newline at end of file +- Removed built-in support for the Verbb Comments plugin, which provides its own Feed Me driver.