Permalink
Browse files

Implemented log file rotation.

  • Loading branch information...
1 parent ef59236 commit 4ab27ca840002738df0d2a6cc509124ffc9bf071 @ADmad ADmad committed Feb 12, 2013
Showing with 219 additions and 37 deletions.
  1. +124 −21 lib/Cake/Log/Engine/FileLog.php
  2. +95 −16 lib/Cake/Test/Case/Log/Engine/FileLogTest.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
@@ -30,38 +31,91 @@
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.
*
* @var string
*/
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.
*
* Config
*
* - `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());
}
}
@@ -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);
+ }
}
}

0 comments on commit 4ab27ca

Please sign in to comment.