CLogger.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. <?php
  2. /**
  3. * CLogger class file
  4. *
  5. * @author Qiang Xue <qiang.xue@gmail.com>
  6. * @link http://www.yiiframework.com/
  7. * @copyright 2008-2013 Yii Software LLC
  8. * @license http://www.yiiframework.com/license/
  9. */
  10. /**
  11. * CLogger records log messages in memory.
  12. *
  13. * CLogger implements the methods to retrieve the messages with
  14. * various filter conditions, including log levels and log categories.
  15. *
  16. * @property array $logs List of messages. Each array element represents one message
  17. * with the following structure:
  18. * array(
  19. * [0] => message (string)
  20. * [1] => level (string)
  21. * [2] => category (string)
  22. * [3] => timestamp (float, obtained by microtime(true));.
  23. * @property float $executionTime The total time for serving the current request.
  24. * @property integer $memoryUsage Memory usage of the application (in bytes).
  25. * @property array $profilingResults The profiling results.
  26. *
  27. * @author Qiang Xue <qiang.xue@gmail.com>
  28. * @package system.logging
  29. * @since 1.0
  30. */
  31. class CLogger extends CComponent
  32. {
  33. const LEVEL_TRACE='trace';
  34. const LEVEL_WARNING='warning';
  35. const LEVEL_ERROR='error';
  36. const LEVEL_INFO='info';
  37. const LEVEL_PROFILE='profile';
  38. /**
  39. * @var integer how many messages should be logged before they are flushed to destinations.
  40. * Defaults to 10,000, meaning for every 10,000 messages, the {@link flush} method will be
  41. * automatically invoked once. If this is 0, it means messages will never be flushed automatically.
  42. * @since 1.1.0
  43. */
  44. public $autoFlush=10000;
  45. /**
  46. * @var boolean this property will be passed as the parameter to {@link flush()} when it is
  47. * called in {@link log()} due to the limit of {@link autoFlush} being reached.
  48. * By default, this property is false, meaning the filtered messages are still kept in the memory
  49. * by each log route after calling {@link flush()}. If this is true, the filtered messages
  50. * will be written to the actual medium each time {@link flush()} is called within {@link log()}.
  51. * @since 1.1.8
  52. */
  53. public $autoDump=false;
  54. /**
  55. * @var array log messages
  56. */
  57. private $_logs=array();
  58. /**
  59. * @var integer number of log messages
  60. */
  61. private $_logCount=0;
  62. /**
  63. * @var array log levels for filtering (used when filtering)
  64. */
  65. private $_levels;
  66. /**
  67. * @var array log categories for filtering (used when filtering)
  68. */
  69. private $_categories;
  70. /**
  71. * @var array log categories for excluding from filtering (used when filtering)
  72. */
  73. private $_except=array();
  74. /**
  75. * @var array the profiling results (category, token => time in seconds)
  76. */
  77. private $_timings;
  78. /**
  79. * @var boolean if we are processing the log or still accepting new log messages
  80. * @since 1.1.9
  81. */
  82. private $_processing=false;
  83. /**
  84. * Logs a message.
  85. * Messages logged by this method may be retrieved back via {@link getLogs}.
  86. * @param string $message message to be logged
  87. * @param string $level level of the message (e.g. 'Trace', 'Warning', 'Error'). It is case-insensitive.
  88. * @param string $category category of the message (e.g. 'system.web'). It is case-insensitive.
  89. * @see getLogs
  90. */
  91. public function log($message,$level='info',$category='application')
  92. {
  93. $this->_logs[]=array($message,$level,$category,microtime(true));
  94. $this->_logCount++;
  95. if($this->autoFlush>0 && $this->_logCount>=$this->autoFlush && !$this->_processing)
  96. {
  97. $this->_processing=true;
  98. $this->flush($this->autoDump);
  99. $this->_processing=false;
  100. }
  101. }
  102. /**
  103. * Retrieves log messages.
  104. *
  105. * Messages may be filtered by log levels and/or categories.
  106. * A level filter is specified by a list of levels separated by comma or space
  107. * (e.g. 'trace, error'). A category filter is similar to level filter
  108. * (e.g. 'system, system.web'). A difference is that in category filter
  109. * you can use pattern like 'system.*' to indicate all categories starting
  110. * with 'system'.
  111. *
  112. * If you do not specify level filter, it will bring back logs at all levels.
  113. * The same applies to category filter.
  114. *
  115. * Level filter and category filter are combinational, i.e., only messages
  116. * satisfying both filter conditions will be returned.
  117. *
  118. * @param string $levels level filter
  119. * @param array|string $categories category filter
  120. * @param array|string $except list of log categories to ignore
  121. * @return array list of messages. Each array element represents one message
  122. * with the following structure:
  123. * array(
  124. * [0] => message (string)
  125. * [1] => level (string)
  126. * [2] => category (string)
  127. * [3] => timestamp (float, obtained by microtime(true));
  128. */
  129. public function getLogs($levels='',$categories=array(), $except=array())
  130. {
  131. $this->_levels=preg_split('/[\s,]+/',strtolower($levels),-1,PREG_SPLIT_NO_EMPTY);
  132. if (is_string($categories))
  133. $this->_categories=preg_split('/[\s,]+/',strtolower($categories),-1,PREG_SPLIT_NO_EMPTY);
  134. else
  135. $this->_categories=array_filter(array_map('strtolower',$categories));
  136. if (is_string($except))
  137. $this->_except=preg_split('/[\s,]+/',strtolower($except),-1,PREG_SPLIT_NO_EMPTY);
  138. else
  139. $this->_except=array_filter(array_map('strtolower',$except));
  140. $ret=$this->_logs;
  141. if(!empty($levels))
  142. $ret=array_values(array_filter($ret,array($this,'filterByLevel')));
  143. if(!empty($this->_categories) || !empty($this->_except))
  144. $ret=array_values(array_filter($ret,array($this,'filterByCategory')));
  145. return $ret;
  146. }
  147. /**
  148. * Filter function used by {@link getLogs}
  149. * @param array $value element to be filtered
  150. * @return boolean true if valid log, false if not.
  151. */
  152. private function filterByCategory($value)
  153. {
  154. return $this->filterAllCategories($value, 2);
  155. }
  156. /**
  157. * Filter function used by {@link getProfilingResults}
  158. * @param array $value element to be filtered
  159. * @return boolean true if valid timing entry, false if not.
  160. */
  161. private function filterTimingByCategory($value)
  162. {
  163. return $this->filterAllCategories($value, 1);
  164. }
  165. /**
  166. * Filter function used to filter included and excluded categories
  167. * @param array $value element to be filtered
  168. * @param integer $index index of the values array to be used for check
  169. * @return boolean true if valid timing entry, false if not.
  170. */
  171. private function filterAllCategories($value, $index)
  172. {
  173. $cat=strtolower($value[$index]);
  174. $ret=empty($this->_categories);
  175. foreach($this->_categories as $category)
  176. {
  177. if($cat===$category || (($c=rtrim($category,'.*'))!==$category && strpos($cat,$c)===0))
  178. $ret=true;
  179. }
  180. if($ret)
  181. {
  182. foreach($this->_except as $category)
  183. {
  184. if($cat===$category || (($c=rtrim($category,'.*'))!==$category && strpos($cat,$c)===0))
  185. $ret=false;
  186. }
  187. }
  188. return $ret;
  189. }
  190. /**
  191. * Filter function used by {@link getLogs}
  192. * @param array $value element to be filtered
  193. * @return boolean true if valid log, false if not.
  194. */
  195. private function filterByLevel($value)
  196. {
  197. return in_array(strtolower($value[1]),$this->_levels);
  198. }
  199. /**
  200. * Returns the total time for serving the current request.
  201. * This method calculates the difference between now and the timestamp
  202. * defined by constant YII_BEGIN_TIME.
  203. * To estimate the execution time more accurately, the constant should
  204. * be defined as early as possible (best at the beginning of the entry script.)
  205. * @return float the total time for serving the current request.
  206. */
  207. public function getExecutionTime()
  208. {
  209. return microtime(true)-YII_BEGIN_TIME;
  210. }
  211. /**
  212. * Returns the memory usage of the current application.
  213. * This method relies on the PHP function memory_get_usage().
  214. * If it is not available, the method will attempt to use OS programs
  215. * to determine the memory usage. A value 0 will be returned if the
  216. * memory usage can still not be determined.
  217. * @return integer memory usage of the application (in bytes).
  218. */
  219. public function getMemoryUsage()
  220. {
  221. if(function_exists('memory_get_usage'))
  222. return memory_get_usage();
  223. else
  224. {
  225. $output=array();
  226. if(strncmp(PHP_OS,'WIN',3)===0)
  227. {
  228. exec('tasklist /FI "PID eq ' . getmypid() . '" /FO LIST',$output);
  229. return isset($output[5])?preg_replace('/[\D]/','',$output[5])*1024 : 0;
  230. }
  231. else
  232. {
  233. $pid=getmypid();
  234. exec("ps -eo%mem,rss,pid | grep $pid", $output);
  235. $output=explode(" ",$output[0]);
  236. return isset($output[1]) ? $output[1]*1024 : 0;
  237. }
  238. }
  239. }
  240. /**
  241. * Returns the profiling results.
  242. * The results may be filtered by token and/or category.
  243. * If no filter is specified, the returned results would be an array with each element
  244. * being array($token,$category,$time).
  245. * If a filter is specified, the results would be an array of timings.
  246. *
  247. * Since 1.1.11, filtering results by category supports the same format used for filtering logs in
  248. * {@link getLogs}, and similarly supports filtering by multiple categories and wildcard.
  249. * @param string $token token filter. Defaults to null, meaning not filtered by token.
  250. * @param string $categories category filter. Defaults to null, meaning not filtered by category.
  251. * @param boolean $refresh whether to refresh the internal timing calculations. If false,
  252. * only the first time calling this method will the timings be calculated internally.
  253. * @return array the profiling results.
  254. */
  255. public function getProfilingResults($token=null,$categories=null,$refresh=false)
  256. {
  257. if($this->_timings===null || $refresh)
  258. $this->calculateTimings();
  259. if($token===null && $categories===null)
  260. return $this->_timings;
  261. $timings = $this->_timings;
  262. if($categories!==null) {
  263. $this->_categories=preg_split('/[\s,]+/',strtolower($categories),-1,PREG_SPLIT_NO_EMPTY);
  264. $timings=array_filter($timings,array($this,'filterTimingByCategory'));
  265. }
  266. $results=array();
  267. foreach($timings as $timing)
  268. {
  269. if($token===null || $timing[0]===$token)
  270. $results[]=$timing[2];
  271. }
  272. return $results;
  273. }
  274. private function calculateTimings()
  275. {
  276. $this->_timings=array();
  277. $stack=array();
  278. foreach($this->_logs as $log)
  279. {
  280. if($log[1]!==CLogger::LEVEL_PROFILE)
  281. continue;
  282. list($message,$level,$category,$timestamp)=$log;
  283. if(!strncasecmp($message,'begin:',6))
  284. {
  285. $log[0]=substr($message,6);
  286. $stack[]=$log;
  287. }
  288. elseif(!strncasecmp($message,'end:',4))
  289. {
  290. $token=substr($message,4);
  291. if(($last=array_pop($stack))!==null && $last[0]===$token)
  292. {
  293. $delta=$log[3]-$last[3];
  294. $this->_timings[]=array($message,$category,$delta);
  295. }
  296. else
  297. 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.',
  298. array('{token}'=>$token)));
  299. }
  300. }
  301. $now=microtime(true);
  302. while(($last=array_pop($stack))!==null)
  303. {
  304. $delta=$now-$last[3];
  305. $this->_timings[]=array($last[0],$last[2],$delta);
  306. }
  307. }
  308. /**
  309. * Removes all recorded messages from the memory.
  310. * This method will raise an {@link onFlush} event.
  311. * The attached event handlers can process the log messages before they are removed.
  312. * @param boolean $dumpLogs whether to process the logs immediately as they are passed to log route
  313. * @since 1.1.0
  314. */
  315. public function flush($dumpLogs=false)
  316. {
  317. $this->onFlush(new CEvent($this, array('dumpLogs'=>$dumpLogs)));
  318. $this->_logs=array();
  319. $this->_logCount=0;
  320. }
  321. /**
  322. * Raises an <code>onFlush</code> event.
  323. * @param CEvent $event the event parameter
  324. * @since 1.1.0
  325. */
  326. public function onFlush($event)
  327. {
  328. $this->raiseEvent('onFlush', $event);
  329. }
  330. }