355 lines
11 KiB
PHP
355 lines
11 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* CLogger class file
|
||
|
*
|
||
|
* @author Qiang Xue <qiang.xue@gmail.com>
|
||
|
* @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 <qiang.xue@gmail.com>
|
||
|
* @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 <code>onFlush</code> event.
|
||
|
* @param CEvent $event the event parameter
|
||
|
* @since 1.1.0
|
||
|
*/
|
||
|
public function onFlush($event)
|
||
|
{
|
||
|
$this->raiseEvent('onFlush', $event);
|
||
|
}
|
||
|
}
|