本章主要介绍下SugarCRM里的日志处理类。
// 获取日志实例,以后调用日志处理时,只需类似$GLOBAL['log']->fatal('msg');即可
$GLOBALS['log'] = LoggerManager::getLogger('SugarCRM');
// 返回一个logger实例,单例类型
// ./include/SugarLogger/LoggerManager.php
public static function getLogger() {
if (!LoggerManager::$_instance) {
LoggerManager::$_instance = new LoggerManager();
}
return LoggerManager::$_instance;
}
// 仅仅通过getLogger方法来实例化对象
private function __construct() {
// 获取日志级别,如果没有,则是返回本类的级别
$level = SugarConfig::getInstance()->get('logger.level', $this->_level);
// 设置日志级别
if (!empty($level))
$this->setLevel($level);
if (empty(self::$_loggers))
$this->_findAvailableLoggers();
}
protected function _findAvailableLoggers() {
// 获取include/SugarLogger下的文件,并以此载入
$locations = SugarAutoLoader::getFilesCustom('include/SugarLogger');
foreach ($locations as $location) {
$loggerClass = basename($location, ".php");
if ($loggerClass == "LoggerTemplate" || $loggerClass == "LoggerManager") {
continue;
}
require_once $location;
// ./include/SugarLogger/SugarLogger.php
// class就是文件名,这里是SugarLogger
if (class_exists($loggerClass) && class_implements($loggerClass, 'LoggerTemplate')) {
self::$_loggers[$loggerClass] = new $loggerClass();
}
}
}
// ./include/SugarLogger/SugarLogger.php
// 获取相关日志配置,若没有配置则去本类的默认设置
public function __construct()
{
$config = SugarConfig::getInstance();
$this->ext = $config->get('logger.file.ext', $this->ext);
$this->logfile = $config->get('logger.file.name', $this->logfile);
$this->dateFormat = $config->get('logger.file.dateFormat', $this->dateFormat);
$this->logSize = $config->get('logger.file.maxSize', $this->logSize);
$this->maxLogs = $config->get('logger.file.maxLogs', $this->maxLogs);
$this->filesuffix = $config->get('logger.file.suffix', $this->filesuffix);
$log_dir = $config->get('log_dir' , $this->log_dir);
$this->log_dir = $log_dir . (empty($log_dir)?'':'/');
unset($config);
$this->_doInitialization();
/*
public static function setLogger($level, $logger) {
self::$_logMapping[$level] = $logger;
}
*/
// 设置默认的日志处理类
LoggerManager::setLogger('default','SugarLogger');
}
// 设置前缀、生成log文件【没有时生成】
protected function _doInitialization()
{
// 如果有前缀,就设置前缀
if( $this->filesuffix && array_key_exists($this->filesuffix, self::$filename_suffix) )
{ //if the global config contains date-format suffix, it will create suffix by parsing datetime
$this->date_suffix = "_" . date(str_replace("%", "", $this->filesuffix));
}
// 没有就生成此文件,有则看看此文件是否达到单个文件最大值,是的话,就再生成个日志文件
$this->full_log_file = $this->log_dir . $this->logfile . $this->date_suffix . $this->ext;
$this->initialized = $this->_fileCanBeCreatedAndWrittenTo();
$this->rollLog();
}
protected function _fileCanBeCreatedAndWrittenTo()
{
$this->_attemptToCreateIfNecessary();
return file_exists($this->full_log_file) && is_writable($this->full_log_file);
}
protected function _attemptToCreateIfNecessary()
{
if (file_exists($this->full_log_file)) {
return;
}
@touch($this->full_log_file);
}
// 此方法判断日志文件大小在达到最大值时便按规则再生成一个新的日志文件
// 原有的则为-1,历史的文件则一次加1
// 如果$force为true,那么久强制生成一个
protected function rollLog($force = false)
{
if (!$this->initialized || empty($this->logSize)) {
return;
}
// bug#50265: Parse the its unit string and get the size properly
$units = array(
'b' => 1, //Bytes
'k' => 1024, //KBytes
'm' => 1024 * 1024, //MBytes
'g' => 1024 * 1024 * 1024, //GBytes
);
if( preg_match('/^\s*([0-9]+\.[0-9]+|\.?[0-9]+)\s*(k|m|g|b)(b?ytes)?/i', $this->logSize, $match) ) {
$rollAt = ( int ) $match[1] * $units[strtolower($match[2])];
}
//check if our log file is greater than that or if we are forcing the log to roll if and only if roll size assigned the value correctly
if ($force || (!empty($rollAt) && filesize($this->full_log_file) >= $rollAt)) {
//now lets move the logs starting at the oldest and going to the newest
// 如果一开始只有sugarcrm.log,那么下面的for循环就无用,但此条件不成立,因为进到这里文件已经生成了
// 当条件成立时
// 第一次循环,因为没有-$i文件,sugarcrm.log变成sugarcrm-1.log
// 第二次循环sugarcrm-1.log变成sugarcrm-2.log,sugarcrm.log变成sugarcrm-1.log
// 第三次循环sugarcrm-2.log变成sugarcrm-3.log,sugarcrm.log变成sugarcrm-1.log
for($i = $this->maxLogs - 2; $i > 0; $i --) {
if (file_exists ( $this->log_dir . $this->logfile . $this->date_suffix . '_'. $i . $this->ext )) {
$to = $i + 1;
$old_name = $this->log_dir . $this->logfile . $this->date_suffix . '_'. $i . $this->ext;
$new_name = $this->log_dir . $this->logfile . $this->date_suffix . '_'. $to . $this->ext;
//nsingh- Bug 22548 Win systems fail if new file name already exists. The fix below checks for that.
//if/else branch is necessary as suggested by someone on php-doc ( see rename function ).
sugar_rename($old_name, $new_name);
//rename ( $this->logfile . $i . $this->ext, $this->logfile . $to . $this->ext );
}
}
//now lets move the current .log file
sugar_rename ($this->full_log_file, $this->log_dir . $this->logfile . $this->date_suffix . '_1' . $this->ext);
}
}
// 如果$new_filename存在变删除
// 否则就进行重命名
// 之后再把命名之后的文件加载file_map.php里的去
function sugar_rename( $old_filename, $new_filename){
if (empty($old_filename) || empty($new_filename)) return false;
$success = false;
if(SugarAutoLoader::fileExists($new_filename)) {
SugarAutoLoader::unlink($new_filename);
$success = rename($old_filename, $new_filename);
}
else {
$success = rename($old_filename, $new_filename);
}
if ($success) {
SugarAutoLoader::addToMap($new_filename);
}
return $success;
}
// ./include/utils/autoloader.php
// 把文件加入到file_map.php及映射缓存数组$memmap中
public static function addToMap($filename, $save = true, $dir = false) {
// 规范化文件路径
$filename = self::normalizeFilePath($filename);
// 如果文件已经存在则直接返回
if (self::fileExists($filename))
return;
// 看看文件是否位于系统的临时文件目录类中
foreach (self::$exclude as $exclude_pattern) {
if (substr($filename, 0, strlen($exclude_pattern)) == $exclude_pattern) {
return;
}
}
// 放入映射数组中
self::$memmap[$filename] = 1;
// 放入映射数组中,正常情况下
// self::$filemap里存放的是系统的所有可用的文件
// 这步处理是把新的文件添加进来,如果save为真,则写入file_map.php中
$parts = explode('/', $filename);
$filename = array_pop($parts);
$data = & self::$filemap;
foreach ($parts as $part) {
if (empty($part))
continue; // allow sequences of /s
if (!isset($data[$part])) {
$data[$part] = array();
}
$data = & $data[$part];
}
if (!is_array($data)) {
$data = array();
}
$data[$filename] = $dir ? array() : 1;
if ($save) {
write_array_to_file("existing_files", self::$filemap, sugar_cached(self::CACHE_FILE));
}
}
看完上述代码后,着重讲下项目目录中的sugarcrm.log及sugarcrm-n.log文件,其实明白了上面的rollLog方法,此处可以忽略。日志的相关配置如日志文件名、前缀、后缀、容量最大值都在config.php中,如果没有配置则取日志处理类中自带的默认配置。当项目运行时,如果一开始没有sugarcrm.log【可配置】,那么会在实例化日志类时生成个空的;如果有,那么会比较sugarcrm.log的大小是否超过了配置文件的日志最大容量,是的话,那么就会把当前的sugarcrm.log改为guarcrm-1.log,若下次新的guarcrm.log又超过了,那么重复此步骤,也就是日志都是写在sugarcrm.log中,一旦容量超过了配置,那么便依次把历史的日志文件加一,如,sugarcrm-1.log变为sugarcrm-2.log,最后再把sugarcrm.log重命名为sugarcrm-1.log,下次进来便又生成了新的sugarcrm.log,或是在调用fatal方法时fwrite会生成个。
日志的实例存储在全局变量log里的,若是项目中需要把信息写入日志,只需调用$GLOBALS['log']->debug("test log");即可。接下来就来详细说说SugarCRM中日志的级别了,也就是调用$GLOBALS['log']->debug("test log")处理过程。
$GLOBALS['log']->debug("test log");
// 会自动调用LoggerManager类中的__call魔术方法
/*
* 这里主要用到了两个私有数组变量
* $method 方法名,如本次调用的debug
* $message 日志信息
// 记录项目处理日志的级别,默认为fatal
private static $_levelMapping = array(
'debug' => 100,
'info' => 70,
'warn' => 50,
'deprecated' => 40,
'error' => 25,
'fatal' => 10,
'security' => 5,
'off' => 0,
);
// 项目中处理日志的类,见上个区域的代码分析
private static $_logMapping = array(
'default' => 'SugarLogger',
);
*/
public function __call($method, $message) {
// 如果调用的日志方法不存在,则置为默认的级别
// $this->level可以在配置文件中在log书中的level设置
if (!isset(self::$_levelMapping[$method]))
$method = $this->_level;
// 如果日志级别为默认的/配种文件中的日志级别
// 或是日志级别存在并且小于系统默认的日志级别
// 打个比方配置文件中的level为debug,那么调用的是error【25】,那么100 > 25,即可调用log方法写日志反之不行
// 也就是说如果系统中的日志级别为fatal,那么调用debug不会有任何效果的,因为 20 > 100 不成立
// if the method is a direct match to our level let's let it through this allows for custom levels
if ($method == $this->_level
//otherwise if we have a level mapping for the method and that level is less than or equal to the current level let's let it log
|| (!empty(self::$_levelMapping[$method]) && self::$_levelMapping[$this->_level] >= self::$_levelMapping[$method])) {
//now we get the logger type this allows for having a file logger an email logger, a firebug logger or any other logger you wish you can set different levels to log differently
$logger = (!empty(self::$_logMapping[$method])) ?
self::$_logMapping[$method] : self::$_logMapping['default'];
//if we haven't instantiated that logger let's instantiate
if (!isset(self::$_loggers[$logger])) {
self::$_loggers[$logger] = new $logger();
}
//tell the logger to log the message
self::$_loggers[$logger]->log($method, $message);
}
}
// SugarLogger类的方法,此方法是接口LoggerTemplate中的方法,必须实现
public function log($level, $message)
{
// 没有日志文件,就会返回false,因为无法写入
// 这里和下面的fopens设计的很巧妙,如果只读了上半段代码分析,会发现
// 如果sugarcrm.log容量达到系统之后从而重命名为sugarcrm-1.log,那么接下来如何写入呢
// 下面的fopen中的a参数,就可以生成个新的sugarcrm.log文件了
if (!$this->initialized) {
return;
}
// lets get the current user id or default to -none- if it is not set yet
// $GLOBALS['current_user']存储当前用户的信息
$userID = (!empty($GLOBALS['current_user']->id))?$GLOBALS['current_user']->id:'-none-';
// if we haven't opened a file pointer yet let's do that
if (! $this->fp)$this->fp = fopen ($this->full_log_file , 'a' );
// change to a string if there is just one entry
if ( is_array($message) && count($message) == 1 )
$message = array_shift($message);
// change to a human-readable array output if it's any other array
if ( is_array($message) )
$message = print_r($message,true);
// write out to the file including the time in the dateFormat the process id , the user id , and the log level as well as the message
// 日志写入的格式
$this->write(strftime($this->dateFormat) . ' [' . getmypid () . '][' . $userID . '][' . strtoupper($level) . '] ' . $message . "\n");
}