* @link http://www.yiiframework.com/ * @copyright 2008-2013 Yii Software LLC * @license http://www.yiiframework.com/license/ */ /** * CLogger records log messages in memory. * * CLogger implements the methods to retrieve the messages with * various filter conditions, including log levels and log categories. * * @property array $logs List of messages. Each array element represents one message * with the following structure: * array( * [0] => message (string) * [1] => level (string) * [2] => category (string) * [3] => timestamp (float, obtained by microtime(true));. * @property float $executionTime The total time for serving the current request. * @property integer $memoryUsage Memory usage of the application (in bytes). * @property array $profilingResults The profiling results. * * @author Qiang Xue * @package system.logging * @since 1.0 */ class CLogger extends CComponent { const LEVEL_TRACE='trace'; const LEVEL_WARNING='warning'; const LEVEL_ERROR='error'; const LEVEL_INFO='info'; const LEVEL_PROFILE='profile'; /** * @var integer how many messages should be logged before they are flushed to destinations. * Defaults to 10,000, meaning for every 10,000 messages, the {@link flush} method will be * automatically invoked once. If this is 0, it means messages will never be flushed automatically. * @since 1.1.0 */ public $autoFlush=10000; /** * @var boolean this property will be passed as the parameter to {@link flush()} when it is * called in {@link log()} due to the limit of {@link autoFlush} being reached. * By default, this property is false, meaning the filtered messages are still kept in the memory * by each log route after calling {@link flush()}. If this is true, the filtered messages * will be written to the actual medium each time {@link flush()} is called within {@link log()}. * @since 1.1.8 */ public $autoDump=false; /** * @var array log messages */ private $_logs=array(); /** * @var integer number of log messages */ private $_logCount=0; /** * @var array log levels for filtering (used when filtering) */ private $_levels; /** * @var array log categories for filtering (used when filtering) */ private $_categories; /** * @var array log categories for excluding from filtering (used when filtering) */ private $_except=array(); /** * @var array the profiling results (category, token => time in seconds) */ private $_timings; /** * @var boolean if we are processing the log or still accepting new log messages * @since 1.1.9 */ private $_processing=false; /** * Logs a message. * Messages logged by this method may be retrieved back via {@link getLogs}. * @param string $message message to be logged * @param string $level level of the message (e.g. 'Trace', 'Warning', 'Error'). It is case-insensitive. * @param string $category category of the message (e.g. 'system.web'). It is case-insensitive. * @see getLogs */ public function log($message,$level='info',$category='application') { $this->_logs[]=array($message,$level,$category,microtime(true)); $this->_logCount++; if($this->autoFlush>0 && $this->_logCount>=$this->autoFlush && !$this->_processing) { $this->_processing=true; $this->flush($this->autoDump); $this->_processing=false; } } /** * Retrieves log messages. * * Messages may be filtered by log levels and/or categories. * A level filter is specified by a list of levels separated by comma or space * (e.g. 'trace, error'). A category filter is similar to level filter * (e.g. 'system, system.web'). A difference is that in category filter * you can use pattern like 'system.*' to indicate all categories starting * with 'system'. * * If you do not specify level filter, it will bring back logs at all levels. * The same applies to category filter. * * Level filter and category filter are combinational, i.e., only messages * satisfying both filter conditions will be returned. * * @param string $levels level filter * @param array|string $categories category filter * @param array|string $except list of log categories to ignore * @return array list of messages. Each array element represents one message * with the following structure: * array( * [0] => message (string) * [1] => level (string) * [2] => category (string) * [3] => timestamp (float, obtained by microtime(true)); */ public function getLogs($levels='',$categories=array(), $except=array()) { $this->_levels=preg_split('/[\s,]+/',strtolower($levels),-1,PREG_SPLIT_NO_EMPTY); if (is_string($categories)) $this->_categories=preg_split('/[\s,]+/',strtolower($categories),-1,PREG_SPLIT_NO_EMPTY); else $this->_categories=array_filter(array_map('strtolower',$categories)); if (is_string($except)) $this->_except=preg_split('/[\s,]+/',strtolower($except),-1,PREG_SPLIT_NO_EMPTY); else $this->_except=array_filter(array_map('strtolower',$except)); $ret=$this->_logs; if(!empty($levels)) $ret=array_values(array_filter($ret,array($this,'filterByLevel'))); if(!empty($this->_categories) || !empty($this->_except)) $ret=array_values(array_filter($ret,array($this,'filterByCategory'))); return $ret; } /** * Filter function used by {@link getLogs} * @param array $value element to be filtered * @return boolean true if valid log, false if not. */ private function filterByCategory($value) { return $this->filterAllCategories($value, 2); } /** * Filter function used by {@link getProfilingResults} * @param array $value element to be filtered * @return boolean true if valid timing entry, false if not. */ private function filterTimingByCategory($value) { return $this->filterAllCategories($value, 1); } /** * Filter function used to filter included and excluded categories * @param array $value element to be filtered * @param integer $index index of the values array to be used for check * @return boolean true if valid timing entry, false if not. */ private function filterAllCategories($value, $index) { $cat=strtolower($value[$index]); $ret=empty($this->_categories); foreach($this->_categories as $category) { if($cat===$category || (($c=rtrim($category,'.*'))!==$category && strpos($cat,$c)===0)) $ret=true; } if($ret) { foreach($this->_except as $category) { if($cat===$category || (($c=rtrim($category,'.*'))!==$category && strpos($cat,$c)===0)) $ret=false; } } return $ret; } /** * Filter function used by {@link getLogs} * @param array $value element to be filtered * @return boolean true if valid log, false if not. */ private function filterByLevel($value) { return in_array(strtolower($value[1]),$this->_levels); } /** * Returns the total time for serving the current request. * This method calculates the difference between now and the timestamp * defined by constant YII_BEGIN_TIME. * To estimate the execution time more accurately, the constant should * be defined as early as possible (best at the beginning of the entry script.) * @return float the total time for serving the current request. */ public function getExecutionTime() { return microtime(true)-YII_BEGIN_TIME; } /** * Returns the memory usage of the current application. * This method relies on the PHP function memory_get_usage(). * If it is not available, the method will attempt to use OS programs * to determine the memory usage. A value 0 will be returned if the * memory usage can still not be determined. * @return integer memory usage of the application (in bytes). */ public function getMemoryUsage() { if(function_exists('memory_get_usage')) return memory_get_usage(); else { $output=array(); if(strncmp(PHP_OS,'WIN',3)===0) { exec('tasklist /FI "PID eq ' . getmypid() . '" /FO LIST',$output); return isset($output[5])?preg_replace('/[\D]/','',$output[5])*1024 : 0; } else { $pid=getmypid(); exec("ps -eo%mem,rss,pid | grep $pid", $output); $output=explode(" ",$output[0]); return isset($output[1]) ? $output[1]*1024 : 0; } } } /** * Returns the profiling results. * The results may be filtered by token and/or category. * If no filter is specified, the returned results would be an array with each element * being array($token,$category,$time). * If a filter is specified, the results would be an array of timings. * * Since 1.1.11, filtering results by category supports the same format used for filtering logs in * {@link getLogs}, and similarly supports filtering by multiple categories and wildcard. * @param string $token token filter. Defaults to null, meaning not filtered by token. * @param string $categories category filter. Defaults to null, meaning not filtered by category. * @param boolean $refresh whether to refresh the internal timing calculations. If false, * only the first time calling this method will the timings be calculated internally. * @return array the profiling results. */ public function getProfilingResults($token=null,$categories=null,$refresh=false) { if($this->_timings===null || $refresh) $this->calculateTimings(); if($token===null && $categories===null) return $this->_timings; $timings = $this->_timings; if($categories!==null) { $this->_categories=preg_split('/[\s,]+/',strtolower($categories),-1,PREG_SPLIT_NO_EMPTY); $timings=array_filter($timings,array($this,'filterTimingByCategory')); } $results=array(); foreach($timings as $timing) { if($token===null || $timing[0]===$token) $results[]=$timing[2]; } return $results; } private function calculateTimings() { $this->_timings=array(); $stack=array(); foreach($this->_logs as $log) { if($log[1]!==CLogger::LEVEL_PROFILE) continue; list($message,$level,$category,$timestamp)=$log; if(!strncasecmp($message,'begin:',6)) { $log[0]=substr($message,6); $stack[]=$log; } elseif(!strncasecmp($message,'end:',4)) { $token=substr($message,4); if(($last=array_pop($stack))!==null && $last[0]===$token) { $delta=$log[3]-$last[3]; $this->_timings[]=array($message,$category,$delta); } else throw new CException(Yii::t('yii','CProfileLogRoute found a mismatching code block "{token}". Make sure the calls to Yii::beginProfile() and Yii::endProfile() be properly nested.', array('{token}'=>$token))); } } $now=microtime(true); while(($last=array_pop($stack))!==null) { $delta=$now-$last[3]; $this->_timings[]=array($last[0],$last[2],$delta); } } /** * Removes all recorded messages from the memory. * This method will raise an {@link onFlush} event. * The attached event handlers can process the log messages before they are removed. * @param boolean $dumpLogs whether to process the logs immediately as they are passed to log route * @since 1.1.0 */ public function flush($dumpLogs=false) { $this->onFlush(new CEvent($this, array('dumpLogs'=>$dumpLogs))); $this->_logs=array(); $this->_logCount=0; } /** * Raises an onFlush event. * @param CEvent $event the event parameter * @since 1.1.0 */ public function onFlush($event) { $this->raiseEvent('onFlush', $event); } }