/ PHP

[CakePHP note.] 滾動你的紀錄檔

其實就是 Log Rotation 這件事情。當我們佈署好一個簡單的 CakePHP 環境時,在 /tmp/logs 中可以發現一些放 log 檔案用的地方。然而,因為這些 log 檔案資訊是由 CakePHP 的 Logging 部份來控制,預設是使用檔案存取的方式來存放 log 紀錄。

Log 的使用方法很簡單:

<?php 
	// 直接使用 CakeLog 類別做靜態呼叫。
	CakeLog::write("error", "something wrong.");
	// 或者是使用 log 函式呼叫。
	$this->log("something wrong", "error");
	// 上述兩者呼叫檔案會存放在 /tmp/logs/error.log 這個檔案中。

而,Log 檔案會一天一天的長大,雖然,我們在正式站上,可能不會在那麼多地方下 Log 紀錄。但是為了避免萬一,許多關鍵點還是會紀錄起來,以免發生死無對證之類的事情。這種時候,Log Rotation 就有其必要性。所以說,我們可以做一個 Components 塞在 CakePHP 裡面,並且在 app_controller.php 加入,讓他可以預先載入。

重點是,我們打開 CakePHP 核心中的 CakeLog(libs/cake_log.php)中,可以看到幾個跟 LOG 有關的東西。主要是這個地方,他把這些東西都換成數字了,所以得留心這個部份。

<?php 
	if (!defined('LOG_WARNING')) {
	        define('LOG_WARNING', 3);
	}
	if (!defined('LOG_NOTICE')) {
	        define('LOG_NOTICE', 4);
	}
	if (!defined('LOG_DEBUG')) {
	        define('LOG_DEBUG', 5);
	}
	if (!defined('LOG_INFO')) {
	        define('LOG_INFO', 6);
	}
	if (!defined('LOG_ERROR')) {
	        define('LOG_ERROR', 2);
	}
	if (!defined('LOG_ERR')) {
	        define('LOG_ERR', LOG_ERROR);
	}

首先,我們有個設定的部份,寫在 core.php 裡面。三種紀錄時間區隔,每日、每星期、每月。

<?php 
	/*
	* Log Rotation
	*/
	Configure::write('Log.rotate', array( 
	'daily' => array(
	    LOG_DEBUG =>    5,
	    LOG_ERROR =>    10,
	),
	'weekly' => array(
	    '*'    =>       3
	),
	'monthly' => array()
	));

最後是 Components 加入 logrotation.php 檔案。

<?php
	/**
	 * Copy from CakeLog::write()
	 */
	if (!defined('LOG_WARNING')) {
	    define('LOG_WARNING', 3);
	}
	if (!defined('LOG_NOTICE')) {
	    define('LOG_NOTICE', 4);
	}
	if (!defined('LOG_DEBUG')) {
	    define('LOG_DEBUG', 5);
	}
	if (!defined('LOG_INFO')) {
	    define('LOG_INFO', 6);
	}

開頭的基礎設定爾等。

<?php 
	class LogrotationComponent extends Object {
	    /**
	     * Name.
	     */
	    var $name = 'Logrotation';
	    /**
	     * Default rotating files.
	     *
	     * @public
	     */
	    var $defaultRotate = 5;
	    /**
	     * Max rotating times, crash?
	     *
	     * @public
	     */
	    var $maxRotate = 100;
	    /**
	     * Default confugure.
	     *
	     * @private.
	     */
	    var $__conf = array();
	    /**
	     * Server time.
	     *
	     * @private;
	     */
	    var $__time = null;
	    /**
	     * Get timestamp data from Cache.
	     *
	     * @private
	     */
	    var $__cache = array();
	    /**
	     * For LOG.
	     *
	     * @private
	     */
	    var $__logfloder = null;

從 startup 開始,也從 startup 結束,我並沒有另外呼叫其他函式。

<?php 
	    function initialize(&$controller, $settings = array()) {
	        if (!class_exists('Folder')) {
	            uses('folder');
	        }
	    }
	    function startup(&$controller) {
	        if (!($this->__conf = Configure::read('Log.rotate'))) {
	            return;
	        }
	        $this->__time = $_SERVER['REQUEST_TIME'];
	        $this->__cache = array(
	            'daily' => Cache::read('Log.daily'),
	            'weekly' => Cache::read('Log.weekly'),
	            'monthly' => Cache::read('Log.monthly')
	        );
	        if ($this->__cache['daily'] === false 
	            || $this->__cache['weekly'] === false
	            || $this->__cache['monthly'] === false
	        ) {
	            Cache::write('Log.daily', $this->__time);
	            Cache::write('Log.weekly', $this->__time);
	            Cache::write('Log.monthly', $this->__time);
	            CakeLog::write('debug', 'Cache log timestamp maybe lost. Reset it now.');
	            return;
	        }
	        $this->__logfolder = new Folder(LOGS);
	        $this->__loadingConfigure();
	        $timeToStr = date("Y/m/d H:i:s", $this->__time);
	        // 24 hrs rotation.
	        if ( ceil($this->__time - $this->__cache['daily']) >= 60*60*24 ) {
	            $this->__rotation($this->__conf['daily']);
	            Cache::write('Log.daily', $this->__time);
	            CakeLog::write('debug', 'Daily Log Rotation.');
	        }
	        // 7 days rotation.
	        if ( ceil($this->__time - $this->__cache['weekly']) >= 60*60*24*7 ) {
	            $this->__rotation($this->__conf['weekly']);
	            Cache::write('Log.weekly', $this->__time);
	            CakeLog::write('debug', 'Weekly Log Rotation.');
	        }
	        // 30 days rotation.
	        if ( ceil($this->__time - $this->__cache['monthly']) >= 60*60*24*30 ) {
	            $this->__rotation($this->__conf['monthly']);
	            Cache::write('Log.monthly', $this->__time);
	            CakeLog::write('debug', 'Monthly Log Rotation.');
	        }
	    }

底下兩個公開函式,可以設定最大的執行次數,跟預設 log 檔案要累加的次數。

<?php 
	    function setMaxRotate($times = 100) {
	        $this->maxRotate = $times;
	    }
	    function setDefaultRotate($logs = 5) {
	        $this->defaultRotate = $logs;
	    }

底下全部都是私有函式,只是因為要相容到 PHP4,所以就這樣啦。

<?php 
	    function __rotation($logFiles) {
	        foreach ($logFiles as $logType => $numberOfLogs) {
	            $filename = $this->__getLogFilename($logType);
	            $files = $this->__logfolder->find(basename($filename).'.*',true);
	            $files = array_reverse($files);
	            $this->__dorotate($files, $numberOfLogs);
	        }
	    }
	    function __dorotate($files, $numberOfLogs, $rotateCount = 1) {
	        if (count($files) <= 0 || $rotateCounter >= $this->maxRotate) {
	            return;
	        }
	        $file = $files[0];
	        $fileInfo = pathinfo(LOGS.$file);
	        if ( is_numeric($fileInfo['extension']) ) {
	            if ( $numberOfLogs > 0 
	                && ($fileInfo['extension']+1) > $numberOfLogs
	                && is_file(LOGS.$file)
	            ) {
	                unlink(LOGS.$file);
	            }
	            $newFile = basename($file, $fileInfo['extension']) . ($fileInfo['extension'] + 1);
	            $move = array('from' => LOGS.$file, 'to' => LOGS.$newFile);
	        } elseif ( $fileInfo['extension'] === 'log' ) {
	            $move = array('from' => LOGS.$file, 'to' => LOGS.$file.'.1');
	        } else {
	            $move = null;
	            array_shift($files);
	        }
	        if (!IS_NULL($move)) {
	            exec('mv '.$move['from'].' '.$move['to'], $output, $return_val);
	            if ($return_val === 0) {
	                array_shift($files);
	            }
	        }
	        unset($move);
	        $this->__dorotate($files, $numberOfLogs, $rotateCount+1);
	    }
	    function __loadingConfigure() {
	        $configured = array();
	        foreach ($this->__conf as $interval => $logFiles) {
	            foreach ($logFiles as $logType => $numberOfLogs) {
	                if (!is_numeric($numberOfLogs)) {
	                    $numberOfLogs = $this->defaultRotate;
	                }
	                if (!isset($default) && $logType === '*') {
	                    $default = array($interval, $numberOfLogs);
	                } else {
	                    $configured[] = basename($this->__getLogFilename($logType));
	                }
	            }
	        }
	        if (isset($default)) {
	            list($interval, $numberOfLogs) = $default;
	            unset($this->__conf[$interval]['*']);
	            $logs = $this->__logfolder->find('.*\.log', true);
	            foreach ($logs as $filename) {
	                if (!in_array($filename, $configured)) {
	                    $this->__conf[$interval][basename($filename, '.log')] = $numberOfLogs;
	                }
	            }
	        }
	    }
	    function __getLogFilename($loggerType) {
	        /**
	         * Clone directly from CakeLog::write()
	         *
	         * cakephp/libs/cake_log.php, line 211.
	         */
	        if (!defined('LOG_ERROR')) {
	            define('LOG_ERROR', 2);
	        }
	        if (!defined('LOG_ERR')) {
	            define('LOG_ERR', LOG_ERROR);
	        }
	        $levels = array(
	            LOG_WARNING => 'warning',
	            LOG_NOTICE => 'notice',
	            LOG_INFO => 'info',
	            LOG_DEBUG => 'debug',
	            LOG_ERR => 'error',
	            LOG_ERROR => 'error'
	        );
	        if (isset($loggerType) && is_int($loggerType) && isset($levels[$loggerType])) {
	            $loggerType = $levels[$loggerType];
	        }
	        switch($loggerType) {
	            case 'error':
	            case 'warning':
	                $filename = LOGS . 'error.log';
	            break;
	            case 'notice':
	            case 'info':
	            case 'debug':
	                $filename = LOGS . 'debug.log';
	            break;
	            default:
	                $filename = LOGS . $loggerType . '.log';
	        }
	        return $filename;
	    }
	}
?>

最後在 app_controller.php 中加入。

public $components = array('RequestHandler', 'Logrotation');

然後就這樣了。