Logger.php 10.7 KB
Newer Older
w  
Qiang Xue committed
1 2 3
<?php
/**
 * @link http://www.yiiframework.com/
Qiang Xue committed
4
 * @copyright Copyright (c) 2008 Yii Software LLC
w  
Qiang Xue committed
5 6 7
 * @license http://www.yiiframework.com/license/
 */

Qiang Xue committed
8
namespace yii\log;
Qiang Xue committed
9

Qiang Xue committed
10 11 12
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
13

w  
Qiang Xue committed
14
/**
Qiang Xue committed
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
 * Logger records logged messages in memory and sends them to different targets as needed.
 *
 * Logger is registered as a core application component and can be accessed using `Yii::$app->log`.
 * You can call the method [[log()]] to record a single log message. For convenience, a set of shortcut
 * methods are provided for logging messages of various severity levels via the [[Yii]] class:
 *
 * - [[Yii::trace()]]
 * - [[Yii::error()]]
 * - [[Yii::warning()]]
 * - [[Yii::info()]]
 * - [[Yii::beginProfile()]]
 * - [[Yii::endProfile()]]
 *
 * When enough messages are accumulated in the logger, or when the current request finishes,
 * the logged messages will be sent to different [[targets]], such as log files, emails.
 *
 * You may configure the targets in application configuration, like the following:
 *
 * ~~~
 * array(
 *     'components' => array(
 *         'log' => array(
 *             'targets' => array(
 *                 'file' => array(
 *                     'class' => 'yii\log\FileTarget',
 *                     'levels' => array('trace', 'info'),
 *                     'categories' => array('yii\*'),
 *                 ),
 *                 'email' => array(
 *                     'class' => 'yii\log\EmailTarget',
 *                     'levels' => array('error', 'warning'),
 *                     'emails' => array('admin@example.com'),
 *                 ),
 *             ),
 *         ),
 *     ),
 * )
 * ~~~
 *
 * Each log target can have a name and can be referenced via the [[targets]] property
 * as follows:
 *
 * ~~~
 * Yii::$app->log->targets['file']->enabled = false;
 * ~~~
w  
Qiang Xue committed
60
 *
Qiang Xue committed
61 62
 * When the application ends or [[flushInterval]] is reached, Logger will call [[flush()]]
 * to send logged messages to different log targets, such as file, email, Web.
w  
Qiang Xue committed
63 64 65 66
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
Qiang Xue committed
67
class Logger extends Component
w  
Qiang Xue committed
68
{
Qiang Xue committed
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
	/**
	 * Error message level. An error message is one that indicates the abnormal termination of the
	 * application and may require developer's handling.
	 */
	const LEVEL_ERROR = 0x01;
	/**
	 * Warning message level. A warning message is one that indicates some abnormal happens but
	 * the application is able to continue to run. Developers should pay attention to this message.
	 */
	const LEVEL_WARNING = 0x02;
	/**
	 * Informational message level. An informational message is one that includes certain information
	 * for developers to review.
	 */
	const LEVEL_INFO = 0x04;
	/**
	 * Tracing message level. An tracing message is one that reveals the code execution flow.
	 */
	const LEVEL_TRACE = 0x08;
	/**
	 * Profiling message level. This indicates the message is for profiling purpose.
	 */
	const LEVEL_PROFILE = 0x40;
	/**
	 * Profiling message level. This indicates the message is for profiling purpose. It marks the
	 * beginning of a profiling block.
	 */
	const LEVEL_PROFILE_BEGIN = 0x50;
	/**
	 * Profiling message level. This indicates the message is for profiling purpose. It marks the
	 * end of a profiling block.
	 */
	const LEVEL_PROFILE_END = 0x60;

w  
Qiang Xue committed
103 104

	/**
Qiang Xue committed
105
	 * @var array logged messages. This property is managed by [[log()]] and [[flush()]].
w  
Qiang Xue committed
106 107 108 109
	 * Each log message is of the following structure:
	 *
	 * ~~~
	 * array(
Qiang Xue committed
110
	 *   [0] => message (mixed, can be a string or some complex data, such as an exception object)
Qiang Xue committed
111
	 *   [1] => level (integer)
w  
Qiang Xue committed
112 113
	 *   [2] => category (string)
	 *   [3] => timestamp (float, obtained by microtime(true))
114
	 *   [4] => traces (array, debug backtrace, contains the application code call stacks)
w  
Qiang Xue committed
115 116
	 * )
	 * ~~~
w  
Qiang Xue committed
117 118
	 */
	public $messages = array();
Qiang Xue committed
119 120 121 122 123
	/**
	 * @var array debug data. This property stores various types of debug data reported at
	 * different instrument places.
	 */
	public $data = array();
Qiang Xue committed
124
	/**
Qiang Xue committed
125
	 * @var array|Target[] the log targets. Each array element represents a single [[Target|log target]] instance
Qiang Xue committed
126
	 * or the configuration for creating the log target instance.
Qiang Xue committed
127
	 */
Qiang Xue committed
128
	public $targets = array();
Qiang Xue committed
129 130 131 132 133 134 135 136
	/**
	 * @var integer how many messages should be logged before they are flushed from memory and sent to targets.
	 * Defaults to 1000, meaning the [[flush]] method will be invoked once every 1000 messages logged.
	 * Set this property to be 0 if you don't want to flush messages until the application terminates.
	 * This property mainly affects how much memory will be taken by the logged messages.
	 * A smaller value means less memory, but will increase the execution time due to the overhead of [[flush()]].
	 */
	public $flushInterval = 1000;
137 138 139 140 141 142
	/**
	 * @var integer how much call stack information (file name and line number) should be logged for each message.
	 * Defaults to 0, meaning no backtrace information. If it is greater than 0,
	 * at most that number of call stacks will be logged. Only application call stacks are considered.
	 */
	public $traceLevel = 0;
Qiang Xue committed
143

Qiang Xue committed
144 145 146 147 148 149
	/**
	 * Initializes the logger by registering [[flush()]] as a shutdown function.
	 */
	public function init()
	{
		parent::init();
Qiang Xue committed
150 151 152 153 154
		foreach ($this->targets as $name => $target) {
			if (!$target instanceof Target) {
				$this->targets[$name] = Yii::createObject($target);
			}
		}
Qiang Xue committed
155 156 157
		register_shutdown_function(array($this, 'flush'), true);
	}

w  
Qiang Xue committed
158 159
	/**
	 * Logs a message with the given type and category.
160 161
	 * If [[traceLevel]] is greater than 0, additional call stack information about
	 * the application code will be logged as well.
w  
Qiang Xue committed
162
	 * @param string $message the message to be logged.
Qiang Xue committed
163 164 165
	 * @param integer $level the level of the message. This must be one of the following:
	 * `Logger::LEVEL_ERROR`, `Logger::LEVEL_WARNING`, `Logger::LEVEL_INFO`, `Logger::LEVEL_TRACE`,
	 * `Logger::LEVEL_PROFILE_BEGIN`, `Logger::LEVEL_PROFILE_END`.
w  
Qiang Xue committed
166 167
	 * @param string $category the category of the message.
	 */
Qiang Xue committed
168
	public function log($message, $level, $category = 'application')
w  
Qiang Xue committed
169
	{
Qiang Xue committed
170
		$time = microtime(true);
171 172
		$traces = array();
		if ($this->traceLevel > 0) {
w  
Qiang Xue committed
173
			$count = 0;
174 175 176
			$ts = debug_backtrace();
			array_pop($ts); // remove the last trace since it would be the entry script, not very useful
			foreach ($ts as $trace) {
Qiang Xue committed
177
				if (isset($trace['file'], $trace['line']) && strpos($trace['file'], YII_PATH) !== 0) {
178 179 180
					unset($trace['object'], $trace['args']);
					$traces[] = $trace;
					if (++$count >= $this->traceLevel) {
w  
Qiang Xue committed
181 182 183 184 185
						break;
					}
				}
			}
		}
186
		$this->messages[] = array($message, $level, $category, $time, $traces);
187
		if ($this->flushInterval > 0 && count($this->messages) >= $this->flushInterval) {
Qiang Xue committed
188
			$this->flush();
w  
Qiang Xue committed
189 190 191 192
		}
	}

	/**
193
	 * Flushes log messages from memory to targets.
Qiang Xue committed
194
	 * @param boolean $final whether this is a final call during a request.
w  
Qiang Xue committed
195
	 */
Qiang Xue committed
196
	public function flush($final = false)
w  
Qiang Xue committed
197
	{
Qiang Xue committed
198 199 200 201 202
		/** @var Target $target */
		foreach ($this->targets as $target) {
			if ($target->enabled) {
				$target->collect($this->messages, $final);
			}
Qiang Xue committed
203
		}
w  
Qiang Xue committed
204
		$this->messages = array();
w  
Qiang Xue committed
205 206 207 208 209 210 211 212 213
	}

	/**
	 * Returns the total elapsed time since the start of the current request.
	 * This method calculates the difference between now and the timestamp
	 * defined by constant `YII_BEGIN_TIME` which is evaluated at the beginning
	 * of [[YiiBase]] class file.
	 * @return float the total elapsed time in seconds for current request.
	 */
Qiang Xue committed
214
	public function getElapsedTime()
w  
Qiang Xue committed
215 216 217 218 219 220
	{
		return microtime(true) - YII_BEGIN_TIME;
	}

	/**
	 * Returns the profiling results.
w  
Qiang Xue committed
221 222 223 224 225 226 227 228
	 *
	 * By default, all profiling results will be returned. You may provide
	 * `$categories` and `$excludeCategories` as parameters to retrieve the
	 * results that you are interested in.
	 *
	 * @param array $categories list of categories that you are interested in.
	 * You can use an asterisk at the end of a category to do a prefix match.
	 * For example, 'yii\db\*' will match categories starting with 'yii\db\',
Qiang Xue committed
229
	 * such as 'yii\db\Connection'.
230
	 * @param array $excludeCategories list of categories that you want to exclude
w  
Qiang Xue committed
231
	 * @return array the profiling results. Each array element has the following structure:
Qiang Xue committed
232
	 *  `array($token, $category, $time)`.
w  
Qiang Xue committed
233
	 */
w  
Qiang Xue committed
234
	public function getProfiling($categories = array(), $excludeCategories = array())
w  
Qiang Xue committed
235
	{
w  
Qiang Xue committed
236 237 238
		$timings = $this->calculateTimings();
		if (empty($categories) && empty($excludeCategories)) {
			return $timings;
w  
Qiang Xue committed
239
		}
w  
Qiang Xue committed
240 241 242 243 244

		foreach ($timings as $i => $timing) {
			$matched = empty($categories);
			foreach ($categories as $category) {
				$prefix = rtrim($category, '*');
Qiang Xue committed
245
				if (strpos($timing[1], $prefix) === 0 && ($timing[1] === $category || $prefix !== $category)) {
w  
Qiang Xue committed
246 247 248 249 250 251 252 253 254
					$matched = true;
					break;
				}
			}

			if ($matched) {
				foreach ($excludeCategories as $category) {
					$prefix = rtrim($category, '*');
					foreach ($timings as $i => $timing) {
Qiang Xue committed
255
						if (strpos($timing[1], $prefix) === 0 && ($timing[1] === $category || $prefix !== $category)) {
w  
Qiang Xue committed
256 257 258 259 260 261 262 263 264
							$matched = false;
							break;
						}
					}
				}
			}

			if (!$matched) {
				unset($timings[$i]);
w  
Qiang Xue committed
265 266
			}
		}
w  
Qiang Xue committed
267
		return array_values($timings);
w  
Qiang Xue committed
268 269
	}

270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
	/**
	 * Returns the statistical results of DB queries.
	 * The results returned include the number of SQL statements executed and
	 * the total time spent.
	 * @return array the first element indicates the number of SQL statements executed,
	 * and the second element the total time spent in SQL execution.
	 */
	public function getDbProfiling()
	{
		$timings = $this->getProfiling(array('yii\db\Command::query', 'yii\db\Command::execute'));
		$count = count($timings);
		$time = 0;
		foreach ($timings as $timing) {
			$time += $timing[1];
		}
		return array($count, $time);
	}

w  
Qiang Xue committed
288 289
	private function calculateTimings()
	{
w  
Qiang Xue committed
290
		$timings = array();
w  
Qiang Xue committed
291 292 293

		$stack = array();
		foreach ($this->messages as $log) {
Qiang Xue committed
294 295
			list($token, $level, $category, $timestamp) = $log;
			if ($level == self::LEVEL_PROFILE_BEGIN) {
w  
Qiang Xue committed
296
				$stack[] = $log;
Qiang Xue committed
297
			} elseif ($level == self::LEVEL_PROFILE_END) {
Qiang Xue committed
298 299 300
				if (($last = array_pop($stack)) !== null && $last[0] === $token) {
					$timings[] = array($token, $category, $timestamp - $last[3]);
				} else {
Qiang Xue committed
301
					throw new InvalidConfigException("Unmatched profiling block: $token");
w  
Qiang Xue committed
302 303 304 305 306 307 308
				}
			}
		}

		$now = microtime(true);
		while (($last = array_pop($stack)) !== null) {
			$delta = $now - $last[3];
Qiang Xue committed
309
			$timings[] = array($last[0], $last[2], $delta);
w  
Qiang Xue committed
310 311
		}

w  
Qiang Xue committed
312
		return $timings;
w  
Qiang Xue committed
313
	}
Qiang Xue committed
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331

	/**
	 * Returns the text display of the specified level.
	 * @param integer $level the message level, e.g. [[LEVEL_ERROR]], [[LEVEL_WARNING]].
	 * @return string the text display of the level
	 */
	public static function getLevelName($level)
	{
		static $levels = array(
			self::LEVEL_ERROR => 'error',
			self::LEVEL_WARNING => 'warning',
			self::LEVEL_INFO => 'info',
			self::LEVEL_TRACE => 'trace',
			self::LEVEL_PROFILE_BEGIN => 'profile begin',
			self::LEVEL_PROFILE_END => 'profile end',
		);
		return isset($levels[$level]) ? $levels[$level] : 'unknown';
	}
w  
Qiang Xue committed
332
}