Log rotation handlers to rotate based on log file size and date

Collapse
X
 
  • Time
  • Show
Clear All
new posts
  • bandtank
    Active Community Member
    • Mar 2017
    • 379

    Log rotation handlers to rotate based on log file size and date

    The default log handlers allow date-based rotation, which works fine, but I need to rotate log files based on file size. Unfortunately, the default handlers do not allow size-based rotation.

    Background: Add custom handlers to config-internal.php in logger.handlerList according to this page. The configurations shown below are entries into the logger.handlerList array.

    Solution: To solve my problem, I created two new handlers:
    • RotateBySize
    • RotateBySizeAndDate
    The configurations are as follows:
    PHP Code:
        'handlerList' => array(
          [
            'className' => 'Espo\\Custom\\Log\\RotateBySizeHandler',
            'loaderClassName' => 'Espo\\Custom\\Log\\RotateBySizeLoader',
            'params' => [
              'filename' => 'data/logs/espo-debug.log',
              'maxFileNumber' => 30,
              'maxFileSize' => 10 * 1024 * 1024,
            ],
            'level' => 'DEBUG',
            'formatter' => [
              'className' => 'Espo\\Core\\Log\\DefaultFormatter',
              'params' => [
                'lineFormat' => "[%datetime%] %level_name%: %code% %message% %request% %context%\n"
              ]
            ]
          ],
          [
            'className' => 'Espo\\Custom\\Log\\RotateBySizeAndDateHandler',
            'loaderClassName' => 'Espo\\Custom\\Log\\RotateBySizeAndDateLoader',
            'params' => [
              'filename' => 'data/logs/espo-info.log',
              'maxDays' => 30,
              'maxFilesPerDay' => 20,
              'maxFileSize' => 10 * 1024 * 1024,
            ],
            'level' => 'INFO',
            'formatter' => [
              'className' => 'Espo\\Core\\Log\\DefaultFormatter',
              'params' => [
                'lineFormat' => "[%datetime%] %level_name%: %code% %message% %request% %context%\n"
              ]
            ]
          ]
        )
      ],

    The RotateBySize handler forces the following behaviors:
    • File sizes must be at least 10 kB. Negative values or values that are less than 10 kB will cause the default value of 10 kB to be used.
    • The maximum file count must be 0 (unlimited files) or positive (limited files). Negative numbers will cause the default value of 30 to be used.
    • Log files will be named using the more common format of <filename>.log[.n]. For example, custom-log-file.log.4.
    The RotateBySizeAndDate handler forces the following behaviors:
    • File sizes must be at least 10 kB. Negative values or values that are less than 10 kB will cause the default value of 10 kB to be used.
    • The maximum number of days must be 0 (unlimited days) or positive (limited days). Negative numbers will cause the default value of 30 to be used.
    • The maximum number of files per day must be 0 (unlimited files per day) or positive (limited files per day). Negative numbers will cause the default value of 20 to be used.
    • Log files will be named using the more common format of <filename>-<date>.log[.n]. For example, custom-log-file-2024-07-31.log.4

    Here are the associated files:

    custom/Espo/Custom/Log/RotateBySizeLoader.php
    PHP Code:
    <?php
    
    namespace Espo\Custom\Log;
    
    use Espo\Custom\Log\RotateBySizeHandler;
    use Espo\Core\Utils\Config;
    
    use Monolog\Handler\HandlerInterface;
    use Monolog\Level;
    use Monolog\Logger;
    
    class RotateBySizeLoader implements \Espo\Core\Log\HandlerLoader
    {
        public function __construct(
            private readonly Config $config
        ) { }
    
        public function load(array $params): HandlerInterface
        {
            $filename = $params['filename'] ?? 'data/logs/espo.log';
            $levelCode = $params['level'] ?? Level::Notice->value;
            $maxFileNumber = $params['maxFileNumber'] ?? 30;
            $maxFileSize = $params['maxFileSize'] ?? 10 * 1024 * 1024; # 1 MB
            $level = Logger::toMonologLevel($levelCode);
    
            return new RotateBySizeHandler($this->config, $filename, $maxFileNumber, $maxFileSize, $level);
        }
    }
    


    custom/Espo/Custom/Log/RotateBySizeHandler.php
    PHP Code:
    <?php
    
    namespace Espo\Custom\Log;
    
    use Espo\Core\Utils\Config;
    
    use Monolog\Level;
    
    class RotateBySizeHandler extends \Espo\Core\Log\Handler\EspoFileHandler
    {
      protected string $filename;
      protected int $maxFileNumber;
      protected int $maxFileSize;
    
      public function __construct(
        Config $config,
        string $filename,
        int $maxFileNumber,
        int $maxFileSize,
        Level $level = Level::Debug,
        bool $bubble = true
      ) {
        $this->filename = $filename;
        $this->maxFileNumber = $maxFileNumber > -1 ? $maxFileNumber : 30;
        $this->maxFileSize = $maxFileSize > 10 * 1024 ? $maxFileSize : 10 * 1024;
    
        parent::__construct($config, $this->filename, $level, $bubble);
    
        $this->rotate();
      }
    
      protected function rotate(): void {
        $filePattern = $this->getFilePattern();
        $dirPath = $this->fileManager->getDirName($this->filename);
        $logFiles = $this->fileManager->getFileList($dirPath, false, $filePattern, true);
    
        if( empty($logFiles) ||
            !file_exists($this->filename) ||
            (filesize($this->filename) < $this->maxFileSize))
          return;
    
        usort($logFiles, 'strnatcmp');
    
        $pattern = "{$this->filename}.%d";
        for($i = count($logFiles) - 1; $i > 0; $i--) {
          $x = array($logFiles[$i], $i, sprintf($pattern, $i + 1));
          rename("{$dirPath}/{$logFiles[$i]}", sprintf($pattern, $i + 1));
        }
        rename("{$dirPath}/{$logFiles[0]}", sprintf($pattern, 1));
    
        if($this->maxFileNumber == 0) return;
    
        $logFiles = $this->fileManager->getFileList($dirPath, false, $filePattern, true);
        usort($logFiles, 'strnatcmp');
    
        $logFilesToBeRemoved = array_slice($logFiles, $this->maxFileNumber - 1);
        $this->fileManager->removeFile($logFilesToBeRemoved, $dirPath);
      }
    
      protected function getFilePattern(): string {
        $fileInfo = pathinfo($this->filename);
        return "^{$fileInfo['basename']}(\.\d+)?$";
      }
    }

    custom/Espo/Custom/Log/RotateBySizeAndDateLoader.php
    PHP Code:
    <?php
    
    namespace Espo\Custom\Log;
    
    use Espo\Custom\Log\RotateBySizeAndDateHandler;
    use Espo\Core\Utils\Config;
    
    use Monolog\Handler\HandlerInterface;
    use Monolog\Level;
    use Monolog\Logger;
    
    class RotateBySizeAndDateLoader implements \Espo\Core\Log\HandlerLoader
    {
        public function __construct(
            private readonly Config $config
        ) { }
    
        public function load(array $params): HandlerInterface
        {
            $filename = $params['filename'] ?? 'data/logs/espo.log';
            $levelCode = $params['level'] ?? Level::Notice->value;
            $maxDays = $params['maxDays'] ?? 30;
            $maxFilesPerDay = $params['maxFilesPerDay'] ?? 20;
            $maxFileSize = $params['maxFileSize'] ?? 10 * 1024 * 1024; # 10 MB
            $level = Logger::toMonologLevel($levelCode);
    
            return new RotateBySizeAndDateHandler($this->config, $filename, $maxDays, $maxFilesPerDay, $maxFileSize, $level);
        }
    }

    custom/Espo/Custom/Log/RotateBySizeAndDateHandler.php
    PHP Code:
    <?php
    
    namespace Espo\Custom\Log;
    
    use Espo\Core\Utils\Config;
    
    use Monolog\Level;
    use DateTime;
    
    class RotateBySizeAndDateHandler extends \Espo\Core\Log\Handler\EspoFileHandler
    {
      protected string $dateFormat = 'Y-m-d';
      protected string $filenameFormat = '{filename}-{date}';
      protected string $filename;
      protected int $maxDays;
      protected int $maxFilesPerDay;
      protected int $maxFileSize;
    
      public function __construct(
        Config $config,
        string $filename,
        int $maxDays,
        int $maxFilesPerDay,
        int $maxFileSize,
        Level $level = Level::Debug,
        bool $bubble = true
      ) {
        $this->filename = $filename;
        $this->maxDays = $maxDays;
        $this->maxFilesPerDay = $maxFilesPerDay;
        $this->maxFileSize = $maxFileSize;
    
        $this->maxDays = $maxDays > -1 ? $maxDays : 30;
        $this->maxFilesPerDay = $maxFilesPerDay > -1 ? $maxFilesPerDay : 20;
        $this->maxFileSize = $maxFileSize; # > 10 * 1024 ? $maxFileSize : 10 * 1024;
    
        parent::__construct($config, $this->getTimedFilename(date($this->dateFormat)), $level, $bubble);
    
        $this->rotate();
    
        $this->removeOldLogs();
      }
    
      protected function rotate(): void {
        $filePattern = $this->getFilePattern(date($this->dateFormat));
        $filename = $this->getTimedFilename(date($this->dateFormat));
        $dirPath = $this->fileManager->getDirName($filename);
        $logFiles = $this->fileManager->getFileList($dirPath, false, $filePattern, true);
    
        if( empty($logFiles) ||
            !file_exists($filename) ||
            (filesize($filename) < $this->maxFileSize))
          return;
    
        usort($logFiles, 'strnatcmp');
    
        $pattern = "{$filename}.%d";
        for($i = count($logFiles) - 1; $i > 0; $i--) {
          $x = array($logFiles[$i], $i, sprintf($pattern, $i + 1));
          rename("{$dirPath}/{$logFiles[$i]}", sprintf($pattern, $i + 1));
        }
        rename("{$dirPath}/{$logFiles[0]}", sprintf($pattern, 1));
    
        if($this->maxFilesPerDay == 0) return;
    
        $logFiles = $this->fileManager->getFileList($dirPath, false, $filePattern, true);
        usort($logFiles, 'strnatcmp');
    
        $logFilesToBeRemoved = array_slice($logFiles, $this->maxFilesPerDay - 1);
        $this->fileManager->removeFile($logFilesToBeRemoved, $dirPath);
      }
    
      protected function removeOldLogs(): void {
        if($this->maxDays == 0) return;
    
        $now = new DateTime();
    
        $filePattern = $this->getFilePattern();
        $dirPath = $this->fileManager->getDirName($this->filename);
        $logFiles = $this->fileManager->getFileList($dirPath, false, $filePattern, true);
        usort($logFiles, 'strnatcmp');
    
        $filenameRoot = pathinfo($this->filename)["filename"] . "-";
        $logFilesToBeRemoved = array();
        foreach($logFiles as $logFile) {
          $fileInfo = pathinfo($logFile);
          $parts = explode($filenameRoot, $logFile);
          $date = new DateTime(substr($parts[1], 0, 10));
    
          if($date->diff($now)->days >= $this->maxDays) {
            $logFilesToBeRemoved[] = $logFile;
          }
        }
    
        $this->fileManager->removeFile($logFilesToBeRemoved, $dirPath);
      }
    
      protected function getTimedFilename($date): string {
        $fileInfo = pathinfo($this->filename);
    
        $timedFilename = str_replace(
          ['{filename}', '{date}'],
          [$fileInfo['filename'], $date],
          ($fileInfo['dirname'] ?? '') . '/' . $this->filenameFormat
        );
    
        if (!empty($fileInfo['extension'])) {
          $timedFilename .= '.' . $fileInfo['extension'];
        }
    
        return $timedFilename;
      }
    
      protected function getFilePattern($date = ".*"): string {
        $fileInfo = pathinfo($this->filename);
    
        $glob = str_replace(
            ['{filename}', '{date}'],
            [$fileInfo['filename'], $date],
            $this->filenameFormat
        );
    
        if (!empty($fileInfo['extension'])) {
            $glob .= '\.'.$fileInfo['extension'];
        }
    
        $glob .= "(\.\d+)?";
    
        return '^' . $glob . '$';
      }
    }


    After several days, the log files will look something like this:

    Click image for larger version  Name:	image.png Views:	0 Size:	13.4 KB ID:	108975
    Last edited by bandtank; 08-04-2024, 05:37 PM.
Working...