Skip to content

Commit

Permalink
Implemented log file rotation.
Browse files Browse the repository at this point in the history
  • Loading branch information
ADmad committed Feb 18, 2013
1 parent ef59236 commit 4ab27ca
Show file tree
Hide file tree
Showing 2 changed files with 219 additions and 37 deletions.
145 changes: 124 additions & 21 deletions lib/Cake/Log/Engine/FileLog.php
Expand Up @@ -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
Expand All @@ -29,39 +30,92 @@
*/
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;
}

/**
Expand All @@ -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());
}

}
111 changes: 95 additions & 16 deletions lib/Cake/Test/Case/Log/Engine/FileLogTest.php
Expand Up @@ -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');
}

/**
Expand All @@ -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.