From 4ab27ca840002738df0d2a6cc509124ffc9bf071 Mon Sep 17 00:00:00 2001 From: ADmad Date: Wed, 13 Feb 2013 03:13:39 +0530 Subject: [PATCH] Implemented log file rotation. --- lib/Cake/Log/Engine/FileLog.php | 145 +++++++++++++++--- lib/Cake/Test/Case/Log/Engine/FileLogTest.php | 111 ++++++++++++-- 2 files changed, 219 insertions(+), 37 deletions(-) diff --git a/lib/Cake/Log/Engine/FileLog.php b/lib/Cake/Log/Engine/FileLog.php index 14ff20c3043..6ad5e0f8f64 100644 --- a/lib/Cake/Log/Engine/FileLog.php +++ b/lib/Cake/Log/Engine/FileLog.php @@ -20,6 +20,7 @@ App::uses('BaseLog', 'Log/Engine'); App::uses('Hash', 'Utility'); +App::uses('CakeNumber', 'Utility'); /** * File Storage stream for Logging. Writes logs to different files @@ -29,6 +30,21 @@ */ class FileLog extends BaseLog { +/** + * Default configuration values + * + * @var array + * @see FileLog::__construct() + */ + protected $_defaults = array( + 'path' => LOGS, + 'file' => null, + 'types' => null, + 'scopes' => array(), + 'rotate' => 10, + 'size' => 10485760 // 10MB + ); + /** * Path to save log files on. * @@ -36,6 +52,20 @@ class FileLog extends BaseLog { */ protected $_path = null; +/** + * Log file name + * + * @var string + */ + protected $_file = null; + +/** + * Max file size, used for log file rotation. + * + * @var integer + */ + protected $_size = null; + /** * Constructs a new File Logger. * @@ -43,25 +73,49 @@ class FileLog extends BaseLog { * * - `types` string or array, levels the engine is interested in * - `scopes` string or array, scopes the engine is interested in - * - `file` log file name - * - `path` the path to save logs on. + * - `file` Log file name + * - `path` The path to save logs on. + * - `size` Used to implement basic log file rotation. If log file size + * reaches specified size the existing file is renamed by appending timestamp + * to filename and new log file is created. Can be integer bytes value or + * human reabable string values like '10MB', '100KB' etc. + * - `rotate` Log files are rotated specified times before being removed. + * If value is 0, old versions are removed rather then rotated. * * @param array $options Options for the FileLog, see above. */ public function __construct($config = array()) { + $config = Hash::merge($this->_defaults, $config); parent::__construct($config); - $config = Hash::merge(array( - 'path' => LOGS, - 'file' => null, - 'types' => null, - 'scopes' => array(), - ), $this->_config); - $config = $this->config($config); - $this->_path = $config['path']; - $this->_file = $config['file']; - if (!empty($this->_file) && !preg_match('/\.log$/', $this->_file)) { - $this->_file .= '.log'; + } + +/** + * Sets protected properties based on config provided + * + * @param array $config Engine configuration + * @return array + */ + public function config($config = array()) { + parent::config($config); + + if (!empty($config['path'])) { + $this->_path = $config['path']; + } + if (!empty($config['file'])) { + $this->_file = $config['file']; + if (substr($this->_file, -4) !== '.log') { + $this->_file .= '.log'; + } + } + if (!empty($config['size'])) { + if (is_numeric($config['size'])) { + $this->_size = (int)$config['size']; + } else { + $this->_size = CakeNumber::fromReadableSize($config['size']); + } } + + return $this->_config; } /** @@ -72,21 +126,70 @@ public function __construct($config = array()) { * @return boolean success of write. */ public function write($type, $message) { + $output = date('Y-m-d H:i:s') . ' ' . ucfirst($type) . ': ' . $message . "\n"; + $filename = $this->_getFilename($type); + if (!empty($this->_size)) { + $this->_rotateFile($filename); + } + + return file_put_contents($this->_path . $filename, $output, FILE_APPEND); + } + +/** + * Get filename + * @param string $type The type of log. + * @return string File name + */ + protected function _getFilename($type) { $debugTypes = array('notice', 'info', 'debug'); if (!empty($this->_file)) { - $filename = $this->_path . $this->_file; + $filename = $this->_file; } elseif ($type == 'error' || $type == 'warning') { - $filename = $this->_path . 'error.log'; + $filename = 'error.log'; } elseif (in_array($type, $debugTypes)) { - $filename = $this->_path . 'debug.log'; - } elseif (in_array($type, $this->_config['scopes'])) { - $filename = $this->_path . $this->_file; + $filename = 'debug.log'; } else { - $filename = $this->_path . $type . '.log'; + $filename = $type . '.log'; } - $output = date('Y-m-d H:i:s') . ' ' . ucfirst($type) . ': ' . $message . "\n"; - return file_put_contents($filename, $output, FILE_APPEND); + + return $filename; + } + +/** + * Rotate log file if size specified in config is reached. + * Also if `rotate` count is reached oldest file is removed. + * + * @param string $filename Log file name + * @return mixed True if rotated successfully or false in case of error. + * Void if file doesn't need to be rotated. + */ + protected function _rotateFile($filename) { + $filepath = $this->_path . $filename; + if (version_compare(PHP_VERSION, '5.3.0') >= 0) { + clearstatcache(true, $filepath); + } else { + clearstatcache(); + } + + if (!file_exists($filepath) || + filesize($filepath) < $this->_size + ) { + return; + } + + if ($this->_config['rotate'] === 0) { + return unlink($filepath); + } + + if ($this->_config['rotate']) { + $files = glob($filepath . '.*'); + if (count($files) === $this->_config['rotate']) { + unlink(array_shift($files)); + } + } + + return rename($filepath, $filepath . '.' . time()); } } diff --git a/lib/Cake/Test/Case/Log/Engine/FileLogTest.php b/lib/Cake/Test/Case/Log/Engine/FileLogTest.php index 800023bb754..6ea2c3c6868 100644 --- a/lib/Cake/Test/Case/Log/Engine/FileLogTest.php +++ b/lib/Cake/Test/Case/Log/Engine/FileLogTest.php @@ -32,36 +32,26 @@ class FileLogTest extends CakeTestCase { * @return void */ public function testLogFileWriting() { - if (file_exists(LOGS . 'error.log')) { - unlink(LOGS . 'error.log'); - } + $this->_deleteLogs(LOGS); + $log = new FileLog(); $log->write('warning', 'Test warning'); $this->assertTrue(file_exists(LOGS . 'error.log')); $result = file_get_contents(LOGS . 'error.log'); $this->assertRegExp('/^2[0-9]{3}-[0-9]+-[0-9]+ [0-9]+:[0-9]+:[0-9]+ Warning: Test warning/', $result); - unlink(LOGS . 'error.log'); - if (file_exists(LOGS . 'debug.log')) { - unlink(LOGS . 'debug.log'); - } $log->write('debug', 'Test warning'); $this->assertTrue(file_exists(LOGS . 'debug.log')); $result = file_get_contents(LOGS . 'debug.log'); $this->assertRegExp('/^2[0-9]{3}-[0-9]+-[0-9]+ [0-9]+:[0-9]+:[0-9]+ Debug: Test warning/', $result); - unlink(LOGS . 'debug.log'); - if (file_exists(LOGS . 'random.log')) { - unlink(LOGS . 'random.log'); - } $log->write('random', 'Test warning'); $this->assertTrue(file_exists(LOGS . 'random.log')); $result = file_get_contents(LOGS . 'random.log'); $this->assertRegExp('/^2[0-9]{3}-[0-9]+-[0-9]+ [0-9]+:[0-9]+:[0-9]+ Random: Test warning/', $result); - unlink(LOGS . 'random.log'); } /** @@ -71,14 +61,103 @@ public function testLogFileWriting() { */ public function testPathSetting() { $path = TMP . 'tests' . DS; - if (file_exists(LOGS . 'error.log')) { - unlink(LOGS . 'error.log'); - } + $this->_deleteLogs($path); $log = new FileLog(compact('path')); $log->write('warning', 'Test warning'); $this->assertTrue(file_exists($path . 'error.log')); - unlink($path . 'error.log'); + } + +/** + * test log rotation + * + * @return void + */ + public function testRotation() { + $path = TMP . 'tests' . DS; + $this->_deleteLogs($path); + + file_put_contents($path . 'error.log', "this text is under 35 bytes\n"); + $log = new FileLog(array( + 'path' => $path, + 'size' => 35, + 'rotate' => 2 + )); + $log->write('warning', 'Test warning one'); + $this->assertTrue(file_exists($path . 'error.log')); + + $result = file_get_contents($path . 'error.log'); + $this->assertRegExp('/Warning: Test warning one/', $result); + $this->assertEquals(0, count(glob($path . 'error.log.*'))); + + clearstatcache(); + $log->write('warning', 'Test warning second'); + + $files = glob($path . 'error.log.*'); + $this->assertEquals(1, count($files)); + + $result = file_get_contents($files[0]); + $this->assertRegExp('/this text is under 35 bytes/', $result); + $this->assertRegExp('/Warning: Test warning one/', $result); + + sleep(1); + clearstatcache(); + $log->write('warning', 'Test warning third'); + + $result = file_get_contents($path . 'error.log'); + $this->assertRegExp('/Warning: Test warning third/', $result); + + $files = glob($path . 'error.log.*'); + $this->assertEquals(2, count($files)); + + $result = file_get_contents($files[0]); + $this->assertRegExp('/this text is under 35 bytes/', $result); + + $result = file_get_contents($files[1]); + $this->assertRegExp('/Warning: Test warning second/', $result); + + sleep(1); + clearstatcache(); + $log->write('warning', 'Test warning fourth'); + + // rotate count reached so file count should not increase + $files = glob($path . 'error.log.*'); + $this->assertEquals(2, count($files)); + + $result = file_get_contents($path . 'error.log'); + $this->assertRegExp('/Warning: Test warning fourth/', $result); + + $result = file_get_contents(array_pop($files)); + $this->assertRegExp('/Warning: Test warning third/', $result); + + $result = file_get_contents(array_pop($files)); + $this->assertRegExp('/Warning: Test warning second/', $result); + + file_put_contents($path . 'debug.log', "this text is just greater than 35 bytes\n"); + $log = new FileLog(array( + 'path' => $path, + 'size' => 35, + 'rotate' => 0 + )); + $log->write('debug', 'Test debug'); + $this->assertTrue(file_exists($path . 'debug.log')); + + $result = file_get_contents($path . 'debug.log'); + $this->assertRegExp('/^2[0-9]{3}-[0-9]+-[0-9]+ [0-9]+:[0-9]+:[0-9]+ Debug: Test debug/', $result); + $this->assertFalse(strstr($result, 'greater than 5 bytes')); + $this->assertEquals(0, count(glob($path . 'debug.log.*'))); + } + +/** + * helper function to clears all log files in specified directory + * + * @return void + */ + protected function _deleteLogs($dir) { + $files = array_merge(glob($dir . '*.log'), glob($dir . '*.log.*')); + foreach ($files as $file) { + unlink($file); + } } }