diff --git a/framework/YiiBase.php b/framework/YiiBase.php index fa097d4..bc5b2cc 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -30,11 +30,10 @@ defined('YII_ENABLE_EXCEPTION_HANDLER') or define('YII_ENABLE_EXCEPTION_HANDLER' */ defined('YII_ENABLE_ERROR_HANDLER') or define('YII_ENABLE_ERROR_HANDLER', true); /** - * Defines the Yii framework installation path. + * This constant defines the framework installation directory. */ defined('YII_PATH') or define('YII_PATH', __DIR__); - /** * YiiBase is the core helper class for the Yii framework. * @@ -66,7 +65,7 @@ class YiiBase * @var array registered path aliases */ public static $aliases = array( - '@yii' => YII_PATH, + '@yii' => __DIR__, ); private static $_imported = array(); // alias => class name or directory @@ -81,15 +80,6 @@ class YiiBase } /** - * Returns the installation directory of the Yii framework. - * @return string the path of the framework - */ - public static function getFrameworkPath() - { - return YII_PATH; - } - - /** * Imports a class or a directory. * * Importing a class is like including the corresponding class file. @@ -370,86 +360,83 @@ class YiiBase } /** - * Writes a trace message. - * This method will only log a message when the application is in debug mode. - * @param string $msg message to be logged - * @param string $category category of the message - * @see log + * Logs a trace message. + * Trace messages are logged mainly for development purpose to see + * the execution work flow of some code. + * @param string $message the message to be logged. + * @param string $category the category of the message. */ - public static function trace($msg, $category = 'application') + public static function trace($message, $category = 'application') { if (YII_DEBUG) { - static::log($msg, CLogger::LEVEL_TRACE, $category); + self::getLogger()->trace($message, $category); } } /** - * Logs a message. - * Messages logged by this method may be retrieved via {@link CLogger::getLogs} - * and may be recorded in different media, such as file, email, database, using - * {@link CLogRouter}. - * @param string $msg 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. + * Logs an error message. + * An error message is typically logged when an unrecoverable error occurs + * during the execution of an application. + * @param string $message the message to be logged. + * @param string $category the category of the message. */ - public static function log($msg, $level = CLogger::LEVEL_INFO, $category = 'application') + public function error($message, $category = 'application') { - if (self::$_logger === null) { - self::$_logger = new CLogger; - } - if (YII_DEBUG && YII_TRACE_LEVEL > 0 && $level !== CLogger::LEVEL_PROFILE) - { - $traces = debug_backtrace(); - $count = 0; - foreach ($traces as $trace) - { - if (isset($trace['file'], $trace['line']) && strpos($trace['file'], YII_PATH) !== 0) - { - $msg .= "\nin " . $trace['file'] . ' (' . $trace['line'] . ')'; - if (++$count >= YII_TRACE_LEVEL) - break; - } - } - } - self::$_logger->log($msg, $level, $category); + self::getLogger()->error($message, $category); } /** - * Marks the begin of a code block for profiling. - * This has to be matched with a call to {@link endProfile()} with the same token. - * The begin- and end- calls must also be properly nested, e.g., - *
-	 * Yii::beginProfile('block1');
-	 * Yii::beginProfile('block2');
-	 * Yii::endProfile('block2');
-	 * Yii::endProfile('block1');
-	 * 
- * The following sequence is not valid: - *
-	 * Yii::beginProfile('block1');
-	 * Yii::beginProfile('block2');
-	 * Yii::endProfile('block1');
-	 * Yii::endProfile('block2');
-	 * 
- * @param string $token token for the code block - * @param string $category the category of this log message + * Logs a warning message. + * A warning message is typically logged when an error occurs while the execution + * can still continue. + * @param string $message the message to be logged. + * @param string $category the category of the message. + */ + public function warn($message, $category = 'application') + { + self::getLogger()->warn($message, $category); + } + + /** + * Logs an informative message. + * An informative message is typically logged by an application to keep record of + * something important (e.g. an administrator logs in). + * @param string $message the message to be logged. + * @param string $category the category of the message. + */ + public function info($message, $category = 'application') + { + self::getLogger()->info($message, $category); + } + + /** + * Marks the beginning of a code block for profiling. + * This has to be matched with a call to [[endProfile]] with the same category name. + * The begin- and end- calls must also be properly nested. For example, + * + * ~~~ + * \Yii::beginProfile('block1'); + * \Yii::beginProfile('block2'); + * \Yii::endProfile('block2'); + * \Yii::endProfile('block1'); + * ~~~ + * @param string $category the category of this profile block * @see endProfile */ - public static function beginProfile($token, $category = 'application') + public static function beginProfile($category) { - static::log('begin:' . $token, CLogger::LEVEL_PROFILE, $category); + self::getLogger()->beginProfile($category); } /** * Marks the end of a code block for profiling. - * This has to be matched with a previous call to {@link beginProfile()} with the same token. - * @param string $token token for the code block - * @param string $category the category of this log message + * This has to be matched with a previous call to [[beginProfile]] with the same category name. + * @param string $category the category of this profile block * @see beginProfile */ - public static function endProfile($token, $category = 'application') + public static function endProfile($category) { - static::log('end:' . $token, CLogger::LEVEL_PROFILE, $category); + self::getLogger()->endProfile($category); } /** diff --git a/framework/logging/DbTarget.php b/framework/logging/DbTarget.php new file mode 100644 index 0000000..c1d4573 --- /dev/null +++ b/framework/logging/DbTarget.php @@ -0,0 +1,156 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + + +/** + * CDbLogRoute stores log messages in a database table. + * + * To specify the database table for storing log messages, set {@link logTableName} as + * the name of the table and specify {@link connectionID} to be the ID of a {@link CDbConnection} + * application component. If they are not set, a SQLite3 database named 'log-YiiVersion.db' will be created + * and used under the application runtime directory. + * + * @author Qiang Xue + * @version $Id: CDbLogRoute.php 3069 2011-03-14 00:28:38Z qiang.xue $ + * @package system.logging + * @since 1.0 + */ +class CDbLogRoute extends CLogRoute +{ + /** + * @var string the ID of CDbConnection application component. If not set, a SQLite database + * will be automatically created and used. The SQLite database file is + * protected/runtime/log-YiiVersion.db. + */ + public $connectionID; + /** + * @var string the name of the DB table that stores log content. Defaults to 'YiiLog'. + * If {@link autoCreateLogTable} is false and you want to create the DB table manually by yourself, + * you need to make sure the DB table is of the following structure: + *
+	 *  (
+	 *		id       INTEGER NOT NULL PRIMARY KEY,
+	 *		level    VARCHAR(128),
+	 *		category VARCHAR(128),
+	 *		logtime  INTEGER,
+	 *		message  TEXT
+	 *   )
+	 * 
+ * Note, the 'id' column must be created as an auto-incremental column. + * In MySQL, this means it should be id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY; + * In PostgreSQL, it is id SERIAL PRIMARY KEY. + * @see autoCreateLogTable + */ + public $logTableName = 'YiiLog'; + /** + * @var boolean whether the log DB table should be automatically created if not exists. Defaults to true. + * @see logTableName + */ + public $autoCreateLogTable = true; + /** + * @var CDbConnection the DB connection instance + */ + private $_db; + + /** + * Initializes the route. + * This method is invoked after the route is created by the route manager. + */ + public function init() + { + parent::init(); + + if ($this->autoCreateLogTable) + { + $db = $this->getDbConnection(); + $sql = "DELETE FROM {$this->logTableName} WHERE 0=1"; + try + { + $db->createCommand($sql)->execute(); + } + catch(Exception $e) + { + $this->createLogTable($db, $this->logTableName); + } + } + } + + /** + * Creates the DB table for storing log messages. + * @param CDbConnection $db the database connection + * @param string $tableName the name of the table to be created + */ + protected function createLogTable($db, $tableName) + { + $driver = $db->getDriverName(); + if ($driver === 'mysql') + $logID = 'id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY'; + elseif ($driver === 'pgsql') + $logID = 'id SERIAL PRIMARY KEY'; + else + $logID = 'id INTEGER NOT NULL PRIMARY KEY'; + + $sql = " +CREATE TABLE $tableName +( + $logID, + level VARCHAR(128), + category VARCHAR(128), + logtime INTEGER, + message TEXT +)"; + $db->createCommand($sql)->execute(); + } + + /** + * @return CDbConnection the DB connection instance + * @throws CException if {@link connectionID} does not point to a valid application component. + */ + protected function getDbConnection() + { + if ($this->_db !== null) + return $this->_db; + elseif (($id = $this->connectionID) !== null) + { + if (($this->_db = Yii::app()->getComponent($id)) instanceof CDbConnection) + return $this->_db; + else + throw new CException(Yii::t('yii', 'CDbLogRoute.connectionID "{id}" does not point to a valid CDbConnection application component.', + array('{id}' => $id))); + } + else + { + $dbFile = Yii::app()->getRuntimePath() . DIRECTORY_SEPARATOR . 'log-' . Yii::getVersion() . '.db'; + return $this->_db = new CDbConnection('sqlite:' . $dbFile); + } + } + + /** + * Stores log messages into database. + * @param array $logs list of log messages + */ + protected function processLogs($logs) + { + $sql = " +INSERT INTO {$this->logTableName} +(level, category, logtime, message) VALUES +(:level, :category, :logtime, :message) +"; + $command = $this->getDbConnection()->createCommand($sql); + foreach ($logs as $log) + { + $command->bindValue(':level', $log[1]); + $command->bindValue(':category', $log[2]); + $command->bindValue(':logtime', (int)$log[3]); + $command->bindValue(':message', $log[0]); + $command->execute(); + } + } +} diff --git a/framework/logging/EmailTarget.php b/framework/logging/EmailTarget.php new file mode 100644 index 0000000..c6a381a --- /dev/null +++ b/framework/logging/EmailTarget.php @@ -0,0 +1,146 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CEmailLogRoute sends selected log messages to email addresses. + * + * The target email addresses may be specified via {@link setEmails emails} property. + * Optionally, you may set the email {@link setSubject subject}, the + * {@link setSentFrom sentFrom} address and any additional {@link setHeaders headers}. + * + * @author Qiang Xue + * @version $Id: CEmailLogRoute.php 3001 2011-02-24 16:42:44Z alexander.makarow $ + * @package system.logging + * @since 1.0 + */ +class CEmailLogRoute extends CLogRoute +{ + /** + * @var array list of destination email addresses. + */ + private $_email = array(); + /** + * @var string email subject + */ + private $_subject; + /** + * @var string email sent from address + */ + private $_from; + /** + * @var array list of additional headers to use when sending an email. + */ + private $_headers = array(); + + /** + * Sends log messages to specified email addresses. + * @param array $logs list of log messages + */ + protected function processLogs($logs) + { + $message = ''; + foreach ($logs as $log) + $message .= $this->formatLogMessage($log[0], $log[1], $log[2], $log[3]); + $message = wordwrap($message, 70); + $subject = $this->getSubject(); + if ($subject === null) + $subject = Yii::t('yii', 'Application Log'); + foreach ($this->getEmails() as $email) + $this->sendEmail($email, $subject, $message); + } + + /** + * Sends an email. + * @param string $email single email address + * @param string $subject email subject + * @param string $message email content + */ + protected function sendEmail($email, $subject, $message) + { + $headers = $this->getHeaders(); + if (($from = $this->getSentFrom()) !== null) + $headers[] = "From: {$from}"; + mail($email, $subject, $message, implode("\r\n", $headers)); + } + + /** + * @return array list of destination email addresses + */ + public function getEmails() + { + return $this->_email; + } + + /** + * @param mixed $value list of destination email addresses. If the value is + * a string, it is assumed to be comma-separated email addresses. + */ + public function setEmails($value) + { + if (is_array($value)) + $this->_email = $value; + else + $this->_email = preg_split('/[\s,]+/', $value, -1, PREG_SPLIT_NO_EMPTY); + } + + /** + * @return string email subject. Defaults to CEmailLogRoute::DEFAULT_SUBJECT + */ + public function getSubject() + { + return $this->_subject; + } + + /** + * @param string $value email subject. + */ + public function setSubject($value) + { + $this->_subject = $value; + } + + /** + * @return string send from address of the email + */ + public function getSentFrom() + { + return $this->_from; + } + + /** + * @param string $value send from address of the email + */ + public function setSentFrom($value) + { + $this->_from = $value; + } + + /** + * @return array additional headers to use when sending an email. + * @since 1.1.4 + */ + public function getHeaders() + { + return $this->_headers; + } + + /** + * @param mixed $value list of additional headers to use when sending an email. + * If the value is a string, it is assumed to be line break separated headers. + * @since 1.1.4 + */ + public function setHeaders($value) + { + if (is_array($value)) + $this->_headers = $value; + else + $this->_headers = preg_split('/\r\n|\n/', $value, -1, PREG_SPLIT_NO_EMPTY); + } +} \ No newline at end of file diff --git a/framework/logging/FileTarget.php b/framework/logging/FileTarget.php new file mode 100644 index 0000000..993cb42 --- /dev/null +++ b/framework/logging/FileTarget.php @@ -0,0 +1,167 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CFileLogRoute records log messages in files. + * + * The log files are stored under {@link setLogPath logPath} and the file name + * is specified by {@link setLogFile logFile}. If the size of the log file is + * greater than {@link setMaxFileSize maxFileSize} (in kilo-bytes), a rotation + * is performed, which renames the current log file by suffixing the file name + * with '.1'. All existing log files are moved backwards one place, i.e., '.2' + * to '.3', '.1' to '.2'. The property {@link setMaxLogFiles maxLogFiles} + * specifies how many files to be kept. + * + * @author Qiang Xue + * @version $Id: CFileLogRoute.php 3001 2011-02-24 16:42:44Z alexander.makarow $ + * @package system.logging + * @since 1.0 + */ +class CFileLogRoute extends CLogRoute +{ + /** + * @var integer maximum log file size + */ + private $_maxFileSize = 1024; // in KB + /** + * @var integer number of log files used for rotation + */ + private $_maxLogFiles = 5; + /** + * @var string directory storing log files + */ + private $_logPath; + /** + * @var string log file name + */ + private $_logFile = 'application.log'; + + + /** + * Initializes the route. + * This method is invoked after the route is created by the route manager. + */ + public function init() + { + parent::init(); + if ($this->getLogPath() === null) + $this->setLogPath(Yii::app()->getRuntimePath()); + } + + /** + * @return string directory storing log files. Defaults to application runtime path. + */ + public function getLogPath() + { + return $this->_logPath; + } + + /** + * @param string $value directory for storing log files. + * @throws CException if the path is invalid + */ + public function setLogPath($value) + { + $this->_logPath = realpath($value); + if ($this->_logPath === false || !is_dir($this->_logPath) || !is_writable($this->_logPath)) + throw new CException(Yii::t('yii', 'CFileLogRoute.logPath "{path}" does not point to a valid directory. Make sure the directory exists and is writable by the Web server process.', + array('{path}' => $value))); + } + + /** + * @return string log file name. Defaults to 'application.log'. + */ + public function getLogFile() + { + return $this->_logFile; + } + + /** + * @param string $value log file name + */ + public function setLogFile($value) + { + $this->_logFile = $value; + } + + /** + * @return integer maximum log file size in kilo-bytes (KB). Defaults to 1024 (1MB). + */ + public function getMaxFileSize() + { + return $this->_maxFileSize; + } + + /** + * @param integer $value maximum log file size in kilo-bytes (KB). + */ + public function setMaxFileSize($value) + { + if (($this->_maxFileSize = (int)$value) < 1) + $this->_maxFileSize = 1; + } + + /** + * @return integer number of files used for rotation. Defaults to 5. + */ + public function getMaxLogFiles() + { + return $this->_maxLogFiles; + } + + /** + * @param integer $value number of files used for rotation. + */ + public function setMaxLogFiles($value) + { + if (($this->_maxLogFiles = (int)$value) < 1) + $this->_maxLogFiles = 1; + } + + /** + * Saves log messages in files. + * @param array $logs list of log messages + */ + protected function processLogs($logs) + { + $logFile = $this->getLogPath() . DIRECTORY_SEPARATOR . $this->getLogFile(); + if (@filesize($logFile) > $this->getMaxFileSize() * 1024) + $this->rotateFiles(); + $fp = @fopen($logFile, 'a'); + @flock($fp, LOCK_EX); + foreach ($logs as $log) + @fwrite($fp, $this->formatLogMessage($log[0], $log[1], $log[2], $log[3])); + @flock($fp, LOCK_UN); + @fclose($fp); + } + + /** + * Rotates log files. + */ + protected function rotateFiles() + { + $file = $this->getLogPath() . DIRECTORY_SEPARATOR . $this->getLogFile(); + $max = $this->getMaxLogFiles(); + for ($i = $max;$i > 0;--$i) + { + $rotateFile = $file . '.' . $i; + if (is_file($rotateFile)) + { + // suppress errors because it's possible multiple processes enter into this section + if ($i === $max) + @unlink($rotateFile); + else + @rename($rotateFile, $file . '.' . ($i + 1)); + } + } + if (is_file($file)) + @rename($file, $file . '.1'); // suppress errors because it's possible multiple processes enter into this section + } +} diff --git a/framework/logging/Filter.php b/framework/logging/Filter.php new file mode 100644 index 0000000..ab436e4 --- /dev/null +++ b/framework/logging/Filter.php @@ -0,0 +1,107 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CLogFilter preprocesses the logged messages before they are handled by a log route. + * + * CLogFilter is meant to be used by a log route to preprocess the logged messages + * before they are handled by the route. The default implementation of CLogFilter + * prepends additional context information to the logged messages. In particular, + * by setting {@link logVars}, predefined PHP variables such as + * $_SERVER, $_POST, etc. can be saved as a log message, which may help identify/debug + * issues encountered. + * + * @author Qiang Xue + * @version $Id: CLogFilter.php 3204 2011-05-05 21:36:32Z alexander.makarow $ + * @package system.logging + * @since 1.0.6 + */ +class CLogFilter extends CComponent +{ + /** + * @var boolean whether to prefix each log message with the current user session ID. + * Defaults to false. + */ + public $prefixSession = false; + /** + * @var boolean whether to prefix each log message with the current user + * {@link CWebUser::name name} and {@link CWebUser::id ID}. Defaults to false. + */ + public $prefixUser = false; + /** + * @var boolean whether to log the current user name and ID. Defaults to true. + */ + public $logUser = true; + /** + * @var array list of the PHP predefined variables that should be logged. + * Note that a variable must be accessible via $GLOBALS. Otherwise it won't be logged. + */ + public $logVars = array('_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER'); + + + /** + * Filters the given log messages. + * This is the main method of CLogFilter. It processes the log messages + * by adding context information, etc. + * @param array $logs the log messages + * @return array + */ + public function filter(&$logs) + { + if (!empty($logs)) + { + if (($message = $this->getContext()) !== '') + array_unshift($logs, array($message, CLogger::LEVEL_INFO, 'application', YII_BEGIN_TIME)); + $this->format($logs); + } + return $logs; + } + + /** + * Formats the log messages. + * The default implementation will prefix each message with session ID + * if {@link prefixSession} is set true. It may also prefix each message + * with the current user's name and ID if {@link prefixUser} is true. + * @param array $logs the log messages + */ + protected function format(&$logs) + { + $prefix = ''; + if ($this->prefixSession && ($id = session_id()) !== '') + $prefix .= "[$id]"; + if ($this->prefixUser && ($user = Yii::app()->getComponent('user', false)) !== null) + $prefix .= '[' . $user->getName() . '][' . $user->getId() . ']'; + if ($prefix !== '') + { + foreach ($logs as &$log) + $log[0] = $prefix . ' ' . $log[0]; + } + } + + /** + * Generates the context information to be logged. + * The default implementation will dump user information, system variables, etc. + * @return string the context information. If an empty string, it means no context information. + */ + protected function getContext() + { + $context = array(); + if ($this->logUser && ($user = Yii::app()->getComponent('user', false)) !== null) + $context[] = 'User: ' . $user->getName() . ' (ID: ' . $user->getId() . ')'; + + foreach ($this->logVars as $name) + { + if (!empty($GLOBALS[$name])) + $context[] = "\$ {$name}=" . var_export($GLOBALS[$name], true); + } + + return implode("\n\n", $context); + } +} \ No newline at end of file diff --git a/framework/logging/Logger.php b/framework/logging/Logger.php new file mode 100644 index 0000000..e09092b --- /dev/null +++ b/framework/logging/Logger.php @@ -0,0 +1,320 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2012 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\logging; + +/** + * Logger records logged messages in memory. + * + * When [[flushInterval]] is reached or when application terminates, it will + * call [[flush]] to send logged messages to different log targets, such as + * file, email, Web. + * + * @author Qiang Xue + * @since 2.0 + */ +class Logger extends \yii\base\Component +{ + const LEVEL_TRACE = 'trace'; + const LEVEL_WARN = 'warn'; + const LEVEL_ERROR = 'error'; + const LEVEL_INFO = 'info'; + const LEVEL_PROFILE = 'profile'; + + /** + * @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; + /** + * @var boolean this property will be passed as the parameter to [[flush]] when it is + * called due to the [[flushInterval]] is reached. Defaults to false, meaning the flushed + * messages are still kept in the memory by each log target. If this is true, they will + * be exported to the actual storage medium (e.g. DB, email) defined by each log target. + * @see flushInterval + */ + public $autoExport = false; + /** + * @var array logged messages. This property is mainly managed by [[log]] and [[flush]]. + */ + public $messages = array(); + /** + * @var array the profiling results (category, token => time in seconds) + */ + private $_timings; + + /** + * Logs an error message. + * An error message is typically logged when an unrecoverable error occurs + * during the execution of an application. + * @param string $message the message to be logged. + * @param string $category the category of the message. + */ + public function error($message, $category = 'application') + { + $this->log($message, self::LEVEL_ERROR, $category); + } + + /** + * Logs a trace message. + * Trace messages are logged mainly for development purpose to see + * the execution work flow of some code. + * @param string $message the message to be logged. + * @param string $category the category of the message. + */ + public function trace($message, $category = 'application') + { + $this->log($message, self::LEVEL_TRACE, $category); + } + + /** + * Logs a warning message. + * A warning message is typically logged when an error occurs while the execution + * can still continue. + * @param string $message the message to be logged. + * @param string $category the category of the message. + */ + public function warn($message, $category = 'application') + { + $this->log($message, self::LEVEL_TRACE, $category); + } + + /** + * Logs an informative message. + * An informative message is typically logged by an application to keep record of + * something important (e.g. an administrator logs in). + * @param string $message the message to be logged. + * @param string $category the category of the message. + */ + public function info($message, $category = 'application') + { + $this->log($message, self::LEVEL_TRACE, $category); + } + + /** + * Marks the beginning of a code block for profiling. + * This has to be matched with a call to [[endProfile]] with the same category name. + * The begin- and end- calls must also be properly nested. For example, + * @param string $category the category of this profile block + * @see endProfile + */ + public function beginProfile($category) + { + $this->log('begin', self::LEVEL_PROFILE, $category); + } + + /** + * Marks the end of a code block for profiling. + * This has to be matched with a previous call to [[beginProfile]] with the same category name. + * @param string $category the category of this profile block + * @see beginProfile + */ + public function endProfile($category) + { + $this->log('end', self::LEVEL_PROFILE, $category); + } + + /** + * Logs a message with the given type and category. + * If `YII_DEBUG` is true and `YII_TRACE_LEVEL` is greater than 0, then additional + * call stack information about application code will be appended to the message. + * @param string $message the message to be logged. + * @param string $level the level of the message. This must be one of the following: + * 'trace', 'info', 'warn', 'error', 'profile'. + * @param string $category the category of the message. + */ + public function log($message, $level, $category) + { + if (YII_DEBUG && YII_TRACE_LEVEL > 0 && $level !== self::LEVEL_PROFILE) { + $traces = debug_backtrace(); + $count = 0; + foreach ($traces as $trace) { + if (isset($trace['file'], $trace['line']) && strpos($trace['file'], YII_DIR) !== 0) { + $message .= "\nin " . $trace['file'] . ' (' . $trace['line'] . ')'; + if (++$count >= YII_TRACE_LEVEL) { + break; + } + } + } + } + + $this->messages[] = array($message, $level, $category, microtime(true)); + if (count($this->messages) >= $this->flushInterval && $this->flushInterval > 0) { + $this->flush($this->autoExport); + } + } + + /** + * 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 elements 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; + elseif (empty($levels)) + return array_values(array_filter(array_filter($this->_logs, array($this, 'filterByCategory')))); + elseif (empty($categories)) + return array_values(array_filter(array_filter($this->_logs, array($this, 'filterByLevel')))); + else + { + $ret = array_values(array_filter(array_filter($this->_logs, array($this, 'filterByLevel')))); + return array_values(array_filter(array_filter($ret, array($this, 'filterByCategory')))); + } + } + + /** + * Filter function used by {@link getLogs} + * @param array $value element to be filtered + * @return array 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 $value; + } + return false; + } + + /** + * Filter function used by {@link getLogs} + * @param array $value element to be filtered + * @return array valid log, false if not. + */ + private function filterByLevel($value) + { + return in_array(strtolower($value[1]), $this->_levels) ? $value : false; + } + + /** + * 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. + */ + public function getExecutionTime() + { + return microtime(true) - YII_BEGIN_TIME; + } + + /** + * 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. + * @param string $token token filter. Defaults to null, meaning not filtered by token. + * @param string $category 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, $category = null, $refresh = false) + { + if ($this->_timings === null || $refresh) { + $this->calculateTimings(); + } + if ($token === null && $category === null) { + return $this->_timings; + } + $results = array(); + foreach ($this->_timings as $timing) { + if (($category === null || $timing[1] === $category) && ($token === null || $timing[0] === $token)) { + $results[] = $timing[2]; + } + } + return $results; + } + + private function calculateTimings() + { + $this->_timings = array(); + + $stack = array(); + foreach ($this->messages as $log) { + if ($log[1] !== self::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 \yii\base\Exception('Found a mismatching profiling block: ' . $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 $export whether to notify log targets to export the filtered messages they have received. + */ + public function flush($export = false) + { + $this->onFlush(new \yii\base\Event($this, array('export' => $export))); + $this->messages = array(); + } + + /** + * Raises an `onFlush` event. + * @param \yii\base\Event $event the event parameter + */ + public function onFlush($event) + { + $this->raiseEvent('onFlush', $event); + } +} diff --git a/framework/logging/ProfileTarget.php b/framework/logging/ProfileTarget.php new file mode 100644 index 0000000..28100f9 --- /dev/null +++ b/framework/logging/ProfileTarget.php @@ -0,0 +1,201 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CProfileLogRoute displays the profiling results in Web page. + * + * The profiling is done by calling {@link YiiBase::beginProfile()} and {@link YiiBase::endProfile()}, + * which marks the begin and end of a code block. + * + * CProfileLogRoute supports two types of report by setting the {@link setReport report} property: + *
    + *
  • summary: list the execution time of every marked code block
  • + *
  • callstack: list the mark code blocks in a hierarchical view reflecting their calling sequence.
  • + *
+ * + * @author Qiang Xue + * @version $Id: CProfileLogRoute.php 3204 2011-05-05 21:36:32Z alexander.makarow $ + * @package system.logging + * @since 1.0 + */ +class CProfileLogRoute extends CWebLogRoute +{ + /** + * @var boolean whether to aggregate results according to profiling tokens. + * If false, the results will be aggregated by categories. + * Defaults to true. Note that this property only affects the summary report + * that is enabled when {@link report} is 'summary'. + * @since 1.0.6 + */ + public $groupByToken = true; + /** + * @var string type of profiling report to display + */ + private $_report = 'summary'; + + /** + * Initializes the route. + * This method is invoked after the route is created by the route manager. + */ + public function init() + { + $this->levels = CLogger::LEVEL_PROFILE; + } + + /** + * @return string the type of the profiling report to display. Defaults to 'summary'. + */ + public function getReport() + { + return $this->_report; + } + + /** + * @param string $value the type of the profiling report to display. Valid values include 'summary' and 'callstack'. + */ + public function setReport($value) + { + if ($value === 'summary' || $value === 'callstack') + $this->_report = $value; + else + throw new CException(Yii::t('yii', 'CProfileLogRoute.report "{report}" is invalid. Valid values include "summary" and "callstack".', + array('{report}' => $value))); + } + + /** + * Displays the log messages. + * @param array $logs list of log messages + */ + public function processLogs($logs) + { + $app = Yii::app(); + if (!($app instanceof CWebApplication) || $app->getRequest()->getIsAjaxRequest()) + return; + + if ($this->getReport() === 'summary') + $this->displaySummary($logs); + else + $this->displayCallstack($logs); + } + + /** + * Displays the callstack of the profiling procedures for display. + * @param array $logs list of logs + */ + protected function displayCallstack($logs) + { + $stack = array(); + $results = array(); + $n = 0; + foreach ($logs as $log) + { + if ($log[1] !== CLogger::LEVEL_PROFILE) + continue; + $message = $log[0]; + if (!strncasecmp($message, 'begin:', 6)) + { + $log[0] = substr($message, 6); + $log[4] = $n; + $stack[] = $log; + $n++; + } + elseif (!strncasecmp($message, 'end:', 4)) + { + $token = substr($message, 4); + if (($last = array_pop($stack)) !== null && $last[0] === $token) + { + $delta = $log[3] - $last[3]; + $results[$last[4]] = array($token, $delta, count($stack)); + } + 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))); + } + } + // remaining entries should be closed here + $now = microtime(true); + while (($last = array_pop($stack)) !== null) + $results[$last[4]] = array($last[0], $now - $last[3], count($stack)); + ksort($results); + $this->render('profile-callstack', $results); + } + + /** + * Displays the summary report of the profiling result. + * @param array $logs list of logs + */ + protected function displaySummary($logs) + { + $stack = array(); + foreach ($logs as $log) + { + if ($log[1] !== CLogger::LEVEL_PROFILE) + continue; + $message = $log[0]; + 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]; + if (!$this->groupByToken) + $token = $log[2]; + if (isset($results[$token])) + $results[$token] = $this->aggregateResult($results[$token], $delta); + else + $results[$token] = array($token, 1, $delta, $delta, $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]; + $token = $this->groupByToken ? $last[0] : $last[2]; + if (isset($results[$token])) + $results[$token] = $this->aggregateResult($results[$token], $delta); + else + $results[$token] = array($token, 1, $delta, $delta, $delta); + } + + $entries = array_values($results); + $func = create_function('$a,$b', 'return $a[4]<$b[4]?1:0;'); + usort($entries, $func); + + $this->render('profile-summary', $entries); + } + + /** + * Aggregates the report result. + * @param array $result log result for this code block + * @param float $delta time spent for this code block + * @return array + */ + protected function aggregateResult($result, $delta) + { + list($token, $calls, $min, $max, $total) = $result; + if ($delta < $min) + $min = $delta; + elseif ($delta > $max) + $max = $delta; + $calls++; + $total += $delta; + return array($token, $calls, $min, $max, $total); + } +} \ No newline at end of file diff --git a/framework/logging/Router.php b/framework/logging/Router.php new file mode 100644 index 0000000..b0ad8c2 --- /dev/null +++ b/framework/logging/Router.php @@ -0,0 +1,136 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2012 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\logging; + +/** + * Router manages [[Target|log targets]] that record log messages in different media. + * + * For example, a [[FileTarget|file log target]] records log messages + * in files; an [[EmailTarget|email log target]] sends log messages + * to specific email addresses. Each log target may specify filters on + * message levels and categories to record specific messages only. + * + * Router and the targets it manages may be configured in application configuration, + * like the following: + * + * ~~~ + * array( + * // preload log component when application starts + * 'preload' => array('log'), + * 'components' => array( + * 'log' => array( + * 'class' => '\yii\logging\Router', + * 'targets' => array( + * 'file' => array( + * 'class' => '\yii\logging\FileTarget', + * 'levels' => 'trace, info', + * 'categories' => 'yii\*', + * ), + * 'email' => array( + * 'class' => '\yii\logging\EmailTarget', + * 'levels' => '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; + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class Router extends \yii\base\ApplicationComponent +{ + private $_targets; + + /** + * Constructor. + */ + public function __construct() + { + $this->_targets = new \yii\base\Dictionary; + } + + /** + * Initializes this application component. + * This method is invoked when the Router component is created by the application. + * The method attaches the [[processLogs]] method to both the [[Logger::onFlush]] event + * and the [[\yii\base\Application::onEndRequest]] event. + */ + public function init() + { + parent::init(); + \Yii::getLogger()->attachEventHandler('onFlush', array($this, 'processMessages')); + if (($app = \Yii::app()) !== null) { + $app->attachEventHandler('onEndRequest', array($this, 'processMessages')); + } + } + + /** + * Returns the log targets managed by this log router. + * The keys of the dictionary are the names of the log targets. + * You can use the name to access a specific log target. For example, + * + * ~~~ + * $target = $router->targets['file']; + * ~~~ + * @return \yii\base\Dictionary the targets managed by this log router. + */ + public function getTargets() + { + return $this->_targets; + } + + /** + * Sets the log targets. + * @param array $config list of log target configurations. Each array element + * represents the configuration for creating a single log target. It will be + * passed to [[\Yii::createComponent]] to create the target instance. + */ + public function setTargets($config) + { + foreach ($config as $name => $target) { + if ($target instanceof Target) { + $this->_targets[$name] = $target; + } + else { + $this->_targets[$name] = \Yii::createComponent($target); + } + } + } + + /** + * Retrieves and processes log messages from the system logger. + * This method mainly serves the event handler to [[Logger::onFlush]] + * and [[\yii\base\Application::onEndRequest]] events. + * It will retrieve the available log messages from the [[\Yii::getLogger|system logger]] + * and invoke the registered [[targets|log targets]] to do the actual processing. + * @param \yii\base\Event $event event parameter + */ + public function processMessages($event) + { + $logger = Yii::getLogger(); + $export = !isset($event->params['export']) || $event->params['export']; + foreach ($this->_targets as $target) { + if ($target->enabled) { + $target->processMessages($logger, $export); + } + } + } +} diff --git a/framework/logging/Target.php b/framework/logging/Target.php new file mode 100644 index 0000000..b14cfbe --- /dev/null +++ b/framework/logging/Target.php @@ -0,0 +1,171 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2012 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\logging; + +/** + * Target is the base class for all log target classes. + * + * A log target object retrieves log messages from a logger and sends it + * somewhere, such as files, emails. + * The messages being retrieved may be filtered first before being sent + * to the destination. The filters include log level filter and log category filter. + * + * To specify level filter, set {@link levels} property, + * which takes a string of comma-separated desired level names (e.g. 'Error, Debug'). + * To specify category filter, set {@link categories} property, + * which takes a string of comma-separated desired category names (e.g. 'System.Web, System.IO'). + * + * Level filter and category filter are combinational, i.e., only messages + * satisfying both filter conditions will they be returned. + * + * @author Qiang Xue + * @since 2.0 + */ +abstract class Target extends \yii\base\Component implements \yii\base\Initable +{ + /** + * @var boolean whether to enable this log target. Defaults to true. + */ + public $enabled = true; + /** + * @var string list of levels separated by comma or space. Defaults to empty, meaning all levels. + */ + public $levels; + /** + * @var string list of categories separated by comma or space. Defaults to empty, meaning all categories. + */ + public $categories; + /** + * @var string list of categories that should be excluded. + */ + public $excludeCategories; + /** + * @var mixed the additional filter (eg {@link CLogFilter}) that can be applied to the log messages. + * The value of this property will be passed to {@link Yii::createComponent} to create + * a log filter object. As a result, this can be either a string representing the + * filter class name or an array representing the filter configuration. + * In general, the log filter class should be {@link CLogFilter} or a child class of it. + * Defaults to null, meaning no filter will be used. + */ + public $filter; + /** + * @var array the messages that are collected so far by this log target. + */ + public $messages; + + /** + * Pre-initializes this component. + * This method is required by the [[Initable]] interface. It is invoked by + * [[\Yii::createComponent]] after its creates the new component instance but + * BEFORE the component properties are initialized. + * + * You may override this method to do work such as setting property default values. + */ + public function preinit() + { + } + + /** + * Initializes this component. + * This method is invoked after the component is created and its property values are + * initialized. + */ + public function init() + { + } + + /** + * Formats a log message given different fields. + * @param string $message message content + * @param integer $level message level + * @param string $category message category + * @param integer $time timestamp + * @return string formatted message + */ + protected function formatMessage($message, $level, $category, $time) + { + return @date('Y/m/d H:i:s', $time) . " [$level] [$category] $message\n"; + } + + /** + * Retrieves filtered log messages from logger for further processing. + * @param CLogger $logger logger instance + * @param boolean $processLogs whether to process the messages after they are collected from the logger + */ + public function processMessages($logger, $export) + { + $messages = $logger->getLogs($this->levels, $this->categories); + $this->messages = empty($this->messages) ? $messages : array_merge($this->messages, $messages); + if ($processLogs && !empty($this->messages)) + { + if ($this->filter !== null) + Yii::createComponent($this->filter)->filter($this->messages); + $this->processLogs($this->messages); + $this->messages = array(); + } + } + + protected function filterMessages($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; + elseif (empty($levels)) + return array_values(array_filter(array_filter($this->_logs, array($this, 'filterByCategory')))); + elseif (empty($categories)) + return array_values(array_filter(array_filter($this->_logs, array($this, 'filterByLevel')))); + else + { + $ret = array_values(array_filter(array_filter($this->_logs, array($this, 'filterByLevel')))); + return array_values(array_filter(array_filter($ret, array($this, 'filterByCategory')))); + } + } + + /** + * Filter function used by {@link getLogs} + * @param array $value element to be filtered + * @return array valid log, false if not. + */ + protected function filterByCategory($value) + { + foreach ($this->_categories as $category) + { + $cat = strtolower($value[2]); + if ($cat === $category || (($c = rtrim($category, '.*')) !== $category && strpos($cat, $c) === 0)) + return $value; + } + return false; + } + + /** + * Filter function used by {@link getLogs} + * @param array $value element to be filtered + * @return array valid log, false if not. + */ + protected function filterByLevel($value) + { + return in_array(strtolower($value[1]), $this->_levels) ? $value : false; + } + + /** + * Processes log messages and sends them to specific destination. + * Derived child classes must implement this method. + * @param array $messages list of messages. Each array elements represents one message + * with the following structure: + * array( + * [0] => message (string) + * [1] => level (string) + * [2] => category (string) + * [3] => timestamp (float, obtained by microtime(true)); + */ + abstract protected function processLogs($messages); +} diff --git a/framework/logging/WebTarget.php b/framework/logging/WebTarget.php new file mode 100644 index 0000000..cdc622e --- /dev/null +++ b/framework/logging/WebTarget.php @@ -0,0 +1,67 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CWebLogRoute shows the log content in Web page. + * + * The log content can appear either at the end of the current Web page + * or in FireBug console window (if {@link showInFireBug} is set true). + * + * @author Qiang Xue + * @version $Id: CWebLogRoute.php 3001 2011-02-24 16:42:44Z alexander.makarow $ + * @package system.logging + * @since 1.0 + */ +class CWebLogRoute extends CLogRoute +{ + /** + * @var boolean whether the log should be displayed in FireBug instead of browser window. Defaults to false. + */ + public $showInFireBug = false; + + /** + * @var boolean whether the log should be ignored in FireBug for ajax calls. Defaults to true. + * This option should be used carefully, because an ajax call returns all output as a result data. + * For example if the ajax call expects a json type result any output from the logger will cause ajax call to fail. + */ + public $ignoreAjaxInFireBug = true; + + /** + * Displays the log messages. + * @param array $logs list of log messages + */ + public function processLogs($logs) + { + $this->render('log', $logs); + } + + /** + * Renders the view. + * @param string $view the view name (file name without extension). The file is assumed to be located under framework/data/views. + * @param array $data data to be passed to the view + */ + protected function render($view, $data) + { + $app = Yii::app(); + $isAjax = $app->getRequest()->getIsAjaxRequest(); + + if ($this->showInFireBug) + { + if ($isAjax && $this->ignoreAjaxInFireBug) + return; + $view .= '-firebug'; + } + elseif (!($app instanceof CWebApplication) || $isAjax) + return; + + $viewFile = YII_PATH . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . $view . '.php'; + include($app->findLocalizedFile($viewFile, 'en')); + } +} \ No newline at end of file