Announcement

Collapse
No announcement yet.

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

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

  • 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 > -$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($dirPathfalse$filePatterntrue);

        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], $isprintf($pattern$i 1));
          
    rename("{$dirPath}/{$logFiles[$i]}"sprintf($pattern$i 1));
        }
        
    rename("{$dirPath}/{$logFiles[0]}"sprintf($pattern1));

        if(
    $this->maxFileNumber == 0) return;

        
    $logFiles $this->fileManager->getFileList($dirPathfalse$filePatterntrue);
        
    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 > -$maxDays 30;
        
    $this->maxFilesPerDay $maxFilesPerDay > -$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($dirPathfalse$filePatterntrue);

        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], $isprintf($pattern$i 1));
          
    rename("{$dirPath}/{$logFiles[$i]}"sprintf($pattern$i 1));
        }
        
    rename("{$dirPath}/{$logFiles[0]}"sprintf($pattern1));

        if(
    $this->maxFilesPerDay == 0) return;

        
    $logFiles $this->fileManager->getFileList($dirPathfalse$filePatterntrue);
        
    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($dirPathfalse$filePatterntrue);
        
    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], 010));

          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...
X