diff options
author | Fabian Becker <fabian.becker@uni-tuebingen.de> | 2013-09-29 21:11:12 +0400 |
---|---|---|
committer | Fabian Becker <fabian.becker@uni-tuebingen.de> | 2013-09-29 21:11:12 +0400 |
commit | 9f215ec612d32d999b6fda42d4596d0b42ffd0b0 (patch) | |
tree | e77c81e78bf71d66d4b215d9bb53764efc5906ac /core/Log.php | |
parent | 7d91192397f3bd1f889dfdd3b3b4554f8452372e (diff) | |
parent | c36718671a30b52355d53038383476c126e12651 (diff) |
Merge branch 'master' into registry
Conflicts:
core/ErrorHandler.php
core/ExceptionHandler.php
core/FrontController.php
core/Log.php
core/Piwik.php
Diffstat (limited to 'core/Log.php')
-rw-r--r-- | core/Log.php | 543 |
1 files changed, 403 insertions, 140 deletions
diff --git a/core/Log.php b/core/Log.php index fe1cc9e8f7..a3127bec4d 100644 --- a/core/Log.php +++ b/core/Log.php @@ -10,197 +10,460 @@ */ namespace Piwik; -use Piwik\Log\APICall; -use Piwik\Log\Error; -use Piwik\Log\Exception; -use Piwik\Log\Message; -use Piwik\Registry; +use Piwik\Common; +use Piwik\Db; /** - * - * @package Piwik - * @subpackage Log - * @see Zend_Log, libs/Zend/Log.php - * @link http://framework.zend.com/manual/en/zend.log.html + * Logging utility. + * + * You can log messages using one of the public static functions (eg, 'error', 'warning', + * 'info', etc.). + * + * Currently, Piwik supports the following logging backends: + * - logging to the screen + * - logging to a file + * - logging to a database + * + * The logging utility can be configured by manipulating the INI config options in the + * [log] section. */ -abstract class Log extends \Zend_Log +class Log { - protected $logToDatabaseTableName = null; - protected $logToDatabaseColumnMapping = null; - protected $logToFileFilename = null; - protected $fileFormatter = null; - protected $screenFormatter = null; - protected $currentRequestKey; + // log levels + const NONE = 0; + const ERROR = 1; + const WARN = 2; + const INFO = 3; + const DEBUG = 4; + const VERBOSE = 5; + + // config option names + const LOG_LEVEL_CONFIG_OPTION = 'log_level'; + const LOG_WRITERS_CONFIG_OPTION = 'log_writers'; + const LOGGER_FILE_PATH_CONFIG_OPTION = 'logger_file_path'; + const STRING_MESSAGE_FORMAT_OPTION = 'string_message_format'; /** - * @param string $logToFileFilename filename of logfile - * @param \Zend_Log_Formatter_Interface $fileFormatter - * @param \Zend_Log_Formatter_Interface $screenFormatter - * @param string $logToDatabaseTableName - * @param array $logToDatabaseColumnMapping + * This event is called when trying to log an object to a file. Plugins can use + * this event to convert objects to strings before they are logged. + * + * Callback signature: function (&$message, $level, $tag, $datetime, $logger) + * + * The $message parameter is the object that is being logged. Event handlers should + * check if the object is of a certain type and if it is, set $message to the + * string that should be logged. */ - function __construct($logToFileFilename, - $fileFormatter, - $screenFormatter, - $logToDatabaseTableName, - $logToDatabaseColumnMapping) - { - parent::__construct(); + const FORMAT_FILE_MESSAGE_EVENT = 'Log.formatFileMessage'; + + /** + * This event is called when trying to log an object to the screen. Plugins can use + * this event to convert objects to strings before they are logged. + * + * Callback signature: function (&$message, $level, $tag, $datetiem, $logger) + * + * The $message parameter is the object that is being logged. Event handlers should + * check if the object is of a certain type and if it is, set $message to the + * string that should be logged. + * + * The result of this callback can be HTML so no sanitization is done on the result. + * This means YOU MUST SANITIZE THE MESSAGE YOURSELF if you use this event. + */ + const FORMAT_SCREEN_MESSAGE_EVENT = 'Log.formatScreenMessage'; + + /** + * This event is called when trying to log an object to a database table. Plugins can use + * this event to convert objects to strings before they are logged. + * + * Callback signature: function (&$message, $level, $tag, $datetiem, $logger) + * + * The $message parameter is the object that is being logged. Event handlers should + * check if the object is of a certain type and if it is, set $message to the + * string that should be logged. + */ + const FORMAT_DATABASE_MESSAGE_EVENT = 'Log.formatDatabaseMessage'; - $this->currentRequestKey = substr(Common::generateUniqId(), 0, 8); + /** + * The singleton Log instance. + * + * @var Log + */ + private static $instance = null; - $log_dir = Config::getInstance()->log['logger_file_path']; - if ($log_dir[0] != '/' && $log_dir[0] != DIRECTORY_SEPARATOR) { - $log_dir = PIWIK_USER_PATH . '/' . $log_dir; + /** + * Returns the singleton Log instance or creates it if it doesn't exist. + * + * @return Log + */ + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new Log(); } - $this->logToFileFilename = $log_dir . '/' . $logToFileFilename; + return self::$instance; + } - $this->fileFormatter = $fileFormatter; - $this->screenFormatter = $screenFormatter; - $this->logToDatabaseTableName = Common::prefixTable($logToDatabaseTableName); - $this->logToDatabaseColumnMapping = $logToDatabaseColumnMapping; + /** + * Unsets the singleton instance so it will be re-created the next time getInstance() is + * called. For testing purposes only. + */ + public static function clearInstance() + { + self::$instance = null; } - function addWriteToFile() + /** + * The current logging level. Everything of equal or greater priority will be logged. + * Everything else will be ignored. + * + * @var int + */ + private $currentLogLevel = self::WARN; + + /** + * The array of callbacks executed when logging to a file. Each callback writes a log + * message to a logging backend. + * + * @var array + */ + private $writers = array(); + + /** + * The log message format string that turns a tag name, date-time and message into + * one string to log. + * + * @var string + */ + private $logMessageFormat = "[%tag%:%datetime%] %message%"; + + /** + * If we're logging to a file, this is the path to the file to log to. + * + * @var string + */ + private $logToFilePath; + + /** + * True if we're currently setup to log to a screen, false if otherwise. + * + * @var bool + */ + private $loggingToScreen; + + /** + * Constructor. + */ + private function __construct() { - Filesystem::mkdir(dirname($this->logToFileFilename)); - $writerFile = new \Zend_Log_Writer_Stream($this->logToFileFilename); - $writerFile->setFormatter($this->fileFormatter); - $this->addWriter($writerFile); + $logConfig = Config::getInstance()->log; + $this->setCurrentLogLevelFromConfig($logConfig); + $this->setLogWritersFromConfig($logConfig); + $this->setLogFilePathFromConfig($logConfig); + $this->setStringLogMessageFormat($logConfig); + $this->disableLoggingBasedOnConfig($logConfig); } - function addWriteToNull() + /** + * Logs a message using the ERROR log level. + * + * Note: Messages logged with the ERROR level are always logged to the screen in addition + * to configured writers. + * + * @param string $message The log message. This can be a sprintf format string. + * @param ... mixed Optional sprintf params. + */ + public static function error($message /* ... */) { - $this->addWriter(new \Zend_Log_Writer_Null); + self::log(self::ERROR, $message, array_slice(func_get_args(), 1)); } - function addWriteToDatabase() + /** + * Logs a message using the WARNING log level. + * + * @param string $message The log message. This can be a sprintf format string. + * @param ... mixed Optional sprintf params. + */ + public static function warning($message /* ... */) { - $writerDb = new \Zend_Log_Writer_Db( - Db::get(), - $this->logToDatabaseTableName, - $this->logToDatabaseColumnMapping); + self::log(self::WARN, $message, array_slice(func_get_args(), 1)); + } - $this->addWriter($writerDb); + /** + * Logs a message using the INFO log level. + * + * @param string $message The log message. This can be a sprintf format string. + * @param ... mixed Optional sprintf params. + */ + public static function info($message /* ... */) + { + self::log(self::INFO, $message, array_slice(func_get_args(), 1)); } - function addWriteToScreen() + /** + * Logs a message using the DEBUG log level. + * + * @param string $message The log message. This can be a sprintf format string. + * @param ... mixed Optional sprintf params. + */ + public static function debug($message /* ... */) { - $writerScreen = new \Zend_Log_Writer_Stream('php://output'); - $writerScreen->setFormatter($this->screenFormatter); - $this->addWriter($writerScreen); + self::log(self::DEBUG, $message, array_slice(func_get_args(), 1)); } - public function getWritersCount() + /** + * Logs a message using the VERBOSE log level. + * + * @param string $message The log message. This can be a sprintf format string. + * @param ... mixed Optional sprintf params. + */ + public static function verbose($message /* ... */) { - return count($this->_writers); + self::log(self::VERBOSE, $message, array_slice(func_get_args(), 1)); } /** - * Log an event - * @param string $event - * @param int $priority - * @param null $extras - * @throws \Zend_Log_Exception - * @return void + * Creates log message combining logging info including a log level, tag name, + * date time, and caller provided log message. The log message can be set through + * the string_message_format ini option in the [log] section. By default it will + * create log messages like: + * + * [tag:datetime] log message + * + * @param int $level + * @param string $tag + * @param string $datetime + * @param string $message + * @return string */ - public function log($event, $priority, $extras = null) + public function formatMessage($level, $tag, $datetime, $message) { - // sanity checks - if (empty($this->_writers)) { - throw new \Zend_Log_Exception('No writers were added'); - } + return str_replace( + array("%tag%", "%message%", "%datetime%", "%level%"), + array($tag, $message, $datetime, $this->getStringLevel($level)), + $this->logMessageFormat + ); + } - $event['timestamp'] = date('Y-m-d H:i:s'); - $event['requestKey'] = $this->currentRequestKey; - // pack into event required by filters and writers - $event = array_merge($event, $this->_extras); + private function setLogWritersFromConfig($logConfig) + { + // set the log writers + $logWriters = $logConfig[self::LOG_WRITERS_CONFIG_OPTION]; - // one message must stay on one line - if (isset($event['message'])) { - $event['message'] = str_replace(array(PHP_EOL, "\n"), " ", $event['message']); - } + $logWriters = array_map('trim', $logWriters); + foreach ($logWriters as $writerName) { + $writer = $this->createWriterByName($writerName); + if (!empty($writer)) { + $this->writers[] = $writer; + } - // Truncate the backtrace which can be too long to display in the browser - if (!empty($event['backtrace'])) { - $maxSizeOutputBytes = 1024 * 1024; // no more than 1M output please - $truncateBacktraceLineAfter = 1000; - $maxLines = ceil($maxSizeOutputBytes / $truncateBacktraceLineAfter); - $bt = explode("\n", $event['backtrace']); - foreach ($bt as $count => &$line) { - if (strlen($line) > $truncateBacktraceLineAfter) { - $line = substr($line, 0, $truncateBacktraceLineAfter) . '...'; - } - if ($count > $maxLines) { - $line .= "\nTruncated error message."; - break; - } + if ($writerName == 'screen') { + $this->loggingToScreen = true; } - $event['backtrace'] = implode("\n", $bt); } - // abort if rejected by the global filters - foreach ($this->_filters as $filter) { - if (!$filter->accept($event)) { - return; + } + + private function setCurrentLogLevelFromConfig($logConfig) + { + if (!empty($logConfig[self::LOG_LEVEL_CONFIG_OPTION])) { + $logLevel = $this->getLogLevelFromStringName(self::LOG_LEVEL_CONFIG_OPTION); + + if ($logLevel >= self::NONE // sanity check + && $logLevel <= self::VERBOSE + ) { + $this->currentLogLevel = $logLevel; } } + } - // send to each writer - foreach ($this->_writers as $writer) { - $writer->write($event); + private function setStringLogMessageFormat($logConfig) + { + if (isset($logConfig['string_message_format'])) { + $this->logMessageFormat = $logConfig['string_message_format']; } } + private function setLogFilePathFromConfig($logConfig) + { + $logPath = $logConfig[self::LOGGER_FILE_PATH_CONFIG_OPTION]; + if ($logPath[0] != '/' && $logPath[0] != DIRECTORY_SEPARATOR) { + $logPath = PIWIK_USER_PATH . '/' . $logPath; + } + if (is_dir($logPath)) { + $logPath .= '/piwik.log'; + } + $this->logToFilePath = $logPath; + } - /** - * Create log object - * @throws Exception - */ - static public function make() + private function createWriterByName($writerName) { - $configAPI = Config::getInstance()->log; + $writer = false; + if ($writerName == 'file') { + $writer = array($this, 'logToFile'); + } else if ($writerName == 'screen') { + $writer = array($this, 'logToScreen'); + } else if ($writerName == 'database') { + $writer = array($this, 'logToDatabase'); + } + return $writer; + } - /** @var Log[] $aLoggers */ - $aLoggers = array( - 'logger_api_call' => new APICall, - 'logger_exception' => new Exception, - 'logger_error' => new Error, - 'logger_message' => new Message, - ); + private function logToFile($level, $tag, $datetime, $message) + { + if (is_string($message)) { + $message = $this->formatMessage($level, $tag, $datetime, $message); + } else { + Piwik_PostEvent(self::FORMAT_FILE_MESSAGE_EVENT, array(&$message, $level, $tag, $datetime, $this)); + } - foreach ($configAPI as $loggerType => $aRecordTo) { - if (isset($aLoggers[$loggerType])) { - $logger = $aLoggers[$loggerType]; - - foreach ($aRecordTo as $recordTo) { - switch ($recordTo) { - case 'screen': - $logger->addWriteToScreen(); - break; - - case 'database': - $logger->addWriteToDatabase(); - break; - - case 'file': - $logger->addWriteToFile(); - break; - - default: - throw new \Exception("'$recordTo' is not a valid Log type. Valid logger types are: screen, database, file."); - break; - } - } - } + if (empty($message)) { + return; + } + + file_put_contents($this->logToFilePath, $message . "\n", FILE_APPEND); + } + + private function logToScreen($level, $tag, $datetime, $message) + { + if (is_string($message)) { + $message = Common::sanitizeInputValue($this->formatMessage($level, $tag, $datetime, $message)); + $message = '<pre>' . $message . '</pre>'; + } else { + Piwik_PostEvent(self::FORMAT_SCREEN_MESSAGE_EVENT, array(&$message, $level, $tag, $datetime, $this)); + } + + if (empty($message)) { + return; + } + + echo $message . "\n"; + } + + private function logToDatabase($level, $tag, $datetime, $message) + { + if (is_string($message)) { + $message = $this->formatMessage($level, $tag, $datetime, $message); + } else { + Piwik_PostEvent(self::FORMAT_DATABASE_MESSAGE_EVENT, array(&$message, $level, $tag, $datetime, $this)); } - foreach ($aLoggers as $loggerType => $logger) { - if ($logger->getWritersCount() == 0) { - $logger->addWriteToNull(); + if (empty($message)) { + return; + } + + $sql = "INSERT INTO " . Common::prefixTable('logger_message') + . " (tag, timestamp, level, message)" + . " VALUES (?, ?, ?, ?)"; + Db::query($sql, array($tag, $datetime, $level, (string)$message)); + } + + private function doLog($level, $message, $sprintfParams = array()) + { + if ($this->shouldLoggerLog($level)) { + $datetime = date("Y-m-d H:i:s"); + if (is_string($message) + && !empty($sprintfParams) + ) { + $message = vsprintf($message, $sprintfParams); + } + + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + $tag = Plugin::getPluginNameFromBacktrace($backtrace); + + // if we can't determine the plugin, use the name of the calling class + if ($tag == false) { + $tag = $this->getClassNameThatIsLogging($backtrace); } - Registry::set($loggerType, $logger); + + $this->writeMessage($level, $tag, $datetime, $message); } } -} + private function writeMessage($level, $tag, $datetime, $message) + { + foreach ($this->writers as $writer) { + call_user_func($writer, $level, $tag, $datetime, $message); + } + // errors are always printed to screen + if ($level == self::ERROR + && !$this->loggingToScreen + ) { + $this->logToScreen($level, $tag, $datetime, $message); + } + } + + private static function log($level, $message, $sprintfParams) + { + self::getInstance()->doLog($level, $message, $sprintfParams); + } + + private function shouldLoggerLog($level) + { + return $level <= $this->currentLogLevel; + } + + private function disableLoggingBasedOnConfig($logConfig) + { + $disableLogging = false; + + if (!empty($logConfig['log_only_when_cli']) + && !Common::isPhpCliMode() + ) { + $disableLogging = true; + } + + if (!empty($logConfig['log_only_when_debug_parameter']) + && !isset($_REQUEST['debug']) + ) { + $disableLogging = true; + } + + if ($disableLogging) { + $this->currentLogLevel = self::NONE; + } + } + + private function getLogLevelFromStringName($name) + { + switch (strtoupper($name)) { + case 'NONE': + return self::NONE; + case 'ERROR': + return self::ERROR; + case 'WARN': + return self::WARN; + case 'INFO': + return self::INFO; + case 'DEBUG': + return self::DEBUG; + case 'VERBOSE': + return self::VERBOSE; + default: + return -1; + } + } + + private function getStringLevel($level) + { + static $levelToName = array( + self::NONE => 'NONE', + self::ERROR => 'ERROR', + self::WARN => 'WARN', + self::INFO => 'INFO', + self::DEBUG => 'DEBUG', + self::VERBOSE => 'VERBOSE' + ); + return $levelToName[$level]; + } + + private function getClassNameThatIsLogging($backtrace) + { + foreach ($backtrace as $tracepoint) { + if (isset($tracepoint['class']) + && $tracepoint['class'] != "Piwik\\Log" + ) { + return $tracepoint['class']; + } + } + return false; + } +} |