CLogger.php
10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
<?php
/**
* CLogger class file
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @link http://www.yiiframework.com/
* @copyright Copyright © 2008-2011 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>
* @version $Id$
* @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 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 string $categories category filter
* @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='')
{
$this->_levels=preg_split('/[\s,]+/',strtolower($levels),-1,PREG_SPLIT_NO_EMPTY);
$this->_categories=preg_split('/[\s,]+/',strtolower($categories),-1,PREG_SPLIT_NO_EMPTY);
if(empty($levels) && empty($categories))
return $this->_logs;
else if(empty($levels))
return array_values(array_filter($this->_logs,array($this,'filterByCategory')));
else if(empty($categories))
return array_values(array_filter($this->_logs,array($this,'filterByLevel')));
else
{
$ret=array_filter($this->_logs,array($this,'filterByLevel'));
return array_values(array_filter($ret,array($this,'filterByCategory')));
}
}
/**
* 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)
{
foreach($this->_categories as $category)
{
$cat=strtolower($value[2]);
if($cat===$category || (($c=rtrim($category,'.*'))!==$category && strpos($cat,$c)===0))
return true;
}
return false;
}
/**
* 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)
{
foreach($this->_categories as $category)
{
$cat=strtolower($value[1]);
if($cat===$category || (($c=rtrim($category,'.*'))!==$category && strpos($cat,$c)===0))
return true;
}
return false;
}
/**
* 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;
}
else if(!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);
}
}