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