Yii的日志的处理机制以及扩展案例
Yii使用层次的日志处理机制,即日志的收集与日志最终的处理(如显示、保存到文件、保存到数据数)是分离的。
?
日志信息的收集由CLogger(日志记录器)完成,而日志信息的分发处理,则在CLogRouter的调度(称为日志路由管理器)下,分发给处理对象(如CFileLogRoute以及logging目录下继承自CLogRoute的类,?称为日志处理器),经过反复阅读其源代码,我更是为Yii的设计思想所折服,如此的分层处理,使得其易于灵活扩展。
?
而日志信息有级别之分,如普通的info,?profile,?trace,?warning,?error级别,可以在日志路由中设置过虑条件,如设置CFileRoute的levels属性,即可只处理指定级别的日志信息。
?
如在程序中调用:
Yii::log($message,CLogger::LEVEL_ERROR,$category);
对应的流程可能如下:
1.?生成CLogger实例
2.?如果YII_DEBUG?,?YII_TRACE_LEVEL都已经定义为有效值,并且日志级别不是profile,?则产生调用回溯信息,?并追加到日志信息上。
3.?调用CLogger::log($msg,$level,$category)收集日志,实际上这时日志并没有写入文件,仅仅是暂存于内存之中。
问题:日志是在何时被写入文件的?
?
经过反复跟踪,我发现在CLogRouter类的init方法中为Application对象的OnEndRequest事件绑定处理器CLogRouter::processLogs()。
同时也给Yii::$_logger的onFlush事件绑定事件处理器CLogRouter::collectLogs方法,用于在Yii::log()中当日志消息量过多时,及时将日志刷新写入文件。
?
而在CApplication::run()方法中定义了:
if($this->hasEventHandler('onEndRequest'))?{
$this->onEndRequest(new?CEvent($this));
?}
到这里我们可以理解CLogger,?(Yii::$_logger)仅仅是将日志进行收集(记录到内容结构之中),然后在程序结束时,由$app对象调用CLogRouter的processLogs进行日志的处理。Yii支持日志多道路由,比如:同一份日志即可写入至文件,又可显示到页面上,甚至同时以电子邮件发送,更甚至同时记录到数据库中,这是由配置文件中的log:routes配置实现的,为log:routes配置多个元素,实现多个路由分发。日志信息的过滤,记录均是由最终的日志处理器处理。
?
日志处理器要完成的任务主要包含以下几点:
从CLogger中取得所有日志,并进行过滤(主要是levels,?categories两项定义log:routes:levels/categories)
1.?先进行过滤
参考CFileLogRoute::collectLogs()中的逻辑:
$logs=$logger->getLogs($this->levels,$this->categories);?//执行过滤,只得到期望信息
日志过滤已经完成接下来就要对日志进行最终处理(如写入到文件,记录至数据库等)
CFileLogRoute::processLogs($logs);
但这个函数之中,有个小bug,?只判断日志目录是否可写,没有判断日志文件本身是否可写.
CFileLogRoute实现了类似Linux的日志轮换功能(LogRoate),?并规定了日志文件的大小,考虑得很周到,很完善!?我也要向其学习并吸收其思想!
?
protected/config/main.php中的配置:
'preload'=>array('log'),
components?=>?array(
'log'=>array(
'class'=>'CLogRouter',
'routes'=>array(
array(
'class'=>'CFileLogRoute',
'levels'=>'error,?warning,trace',
),
)
?
定义log组件需要预先加载(实例化)。配置使用CLogRouter作为日志路由管理器,并设置了其日志路由处理器(routes属性)及其配置属性。
而preload,?log属性的定义,均要应用到CWebApplication对象上(请参阅CApplication::__construct中的configure调用,?configure从CModule继承而来)。而在CWebApplication的构造函数中执行preloadComponents(),就创建了log对象(即CLogRouter的实例)。
?
创建并初始化一个组件时,实际上调用的是CModule::getComponent,?这个调用中使用YiiBase::createComponent创建组件对象,并再调用组件的init初始化之。
?
再阅读CLogRouter::init()过程,在这里有两个关键之处,一是创建日志路由处理器(即决定日志的最终处理方式:写入文件,邮件发送等等),二是给应用程序对象绑定onEndRequest事件处理CLogRouter::processLogs()。而在CApplication::run()确实有相关代码用于运行onEndRequest事件处理句柄:
if($this->hasEventHandler('onEndRequest'))?{
$this->onEndRequest(new?CEvent($this));
}
?
也就是说,日志的最终处理(比如写入文件,系统日志,发送邮件)是发生在应用程序运行完毕之后的。Yii使用事件机制,巧妙地实现了事件与处理句柄的关联。
?
也就是说,当应用程序运行完毕,将执行CLogRouter::processLogs,对日志进行处理,。CLogRouter被称之为日志路由管理器。每个日志路由处理器从CLooger对象中取得相应的日志(使用过滤机制),作最终处理。
?
具体而言Yii的日志系统,分为以下几个层次:
日志发送者,即程序中调用Yii::log($msg,?$level,?$category),将日志发送给CLogger对象
CLogger对象负责将日志记录暂存于内存之中
程序运行结束后,log组件(日志路由管理器CLogRoute)的processLogs方法被激活执行,由其逐个调用日志路由器,作日志的最后处理。
?
大致过程如下:
1.?CApplication::__construct()中调用preloadComponents,?这导致log组件(CLogRoute)被实例化,并被调用init方法初始化。
2.?log组件(CLogRoute)的init方法中,其是初始化日志路由,并给CApplication对象onEndRequest事件绑定处理流程processLogs。给CLooger组件的onFlush事件绑定处理流程collectLogs。
3.?应用程序的其它部分通过调用Yii::log()向CLogger组件发送日志信息,CLogger组件将日志信息暂存到内存中。
4.?CApplication执行完毕(run方法中),会激活onEndRequest事件,绑定的事件处理器processLogs被执行,日志被写入文件之中。
Yii的日志路由机制,给日志系统扩展带来了无限的灵活。并且其多道路由处理机制,可将同一份日志信息进行多种方式处理。
?
这里举出一个案例:发生error级别的数据库错误时,及时给相关维护人员发送电子邮件,并同时将这些日志记录到文件之中。
规划思路,发送邮件和手机短信是两个不同的功能,Yii已经带了日志邮件发送组件(logging/CEmailLogRoute.php),但这个组件中却使用了php自带的mail函数,使用mail函数需要配置php.ini中的smtp主机,并且使用非验证发送方式,这种方式在目前的实际情况下已经完全不可使用。代替地我们需要使用带验证功能的smtp发送方式。
?
1.?在protected/components/目录下定义日志处理器类myEmailLogRoute,并让其继承自CEmailLogRoute,最主要的目的是重写CEmailLogRoute::sendEmail()方法
protected/components/myEmailLogRoute.php的内容
<?php
class?myEmailLogRoute?extends?CEmailLogRoute?{
????public?$host;
????public?$port;
????public?$user;
????public?$password;
????public?$timeout?=?20;
????
????private?$_isConnected?=?false?;
????private?$_link?;
????
????public?function?__destruct()?{
????????if(is_resource($this->_link))?{
????????????fclose($this->_link);
????????}
????}
????
????public?function?setActive($setActive=false)?{
????????if(!$this->_isConnected?||?$setActive)?{
????????????$this->connect();
????????????$this->login();
????????}
????}
????
????public?function?connect($reConnect?=?false)?{
????????#if?connection?is?failed,?
????????$this->_link?=?fsockopen($this->host,$this->port,$errno,?$error,?$this->timeout);????????
????????if(!$this->_link)?{
????????????#do?not?use?Yii::log?to?avoid?recursive?errors?logging.
????????????error_log("$error?($errno)?in?".get_class($this));
????????????syslog(LOG_WARNING,?"$error?($errno)?in".get_class($this));
????????????if(YII_DEBUG)?{
????????????????throw?new?Exception($error,?$errno);
????????????}
????????}
????????Yii::trace("conneted?to?SMTP?Server?$host:$port");
????????return?true?;
????}
????
????#use?$this->user,?$this->password?to?login?SMTP?Server.
????private?function?login()?{
????????//?login?process.
????????$this->_isConnected?=?true?;
????}
????
????public?function?sendEmail($email,$subject,$message)?{
???????$this->active?=?true?;
???????return?$this->_sendEmail($email,?$subject,?$message);
????}
????
????public?function?_sendEmail($receiver,?$subject,?$message)?{
????????#define?you?own?smtp?handler?process.
????}
}
?
其中,SMTP的处理细节请自行完善(本文的重点是放在如何处理日志上,而不是发送邮件上)。
?
接下来,我们就可以定义日志路由处理,编辑protected/config/main.php,?在log组件的routes组件添加新的路由配置:
'log'=>array(
'class'=>'CLogRouter',
'routes'=>array(
array(
'class'=>'CFileLogRoute',
'levels'=>'error,?warning,trace',
),
array(
'class'?=>?'myEmailLogRoute',
'levels'?=>?'error',?#所有异常的错误级别均为error,?
'categories'?=>?'exception.CDbException',?#数据库产生错误时,均会产生CDbException异常。
'host'?=>?'mail.163.com',
'port'?=>?25,
'user'?=>?'zhangxugg@163.com',
'password'?=>?'you?password',
'timeout'?=>?30,
'emails'?=>?'zhangsan@126.com,lisi@qq.com',?#日志接收人。
'sentFrom'?=>?'zhangxugg@163.com',
),
?
经过以上处理,即可使之实现我们的目的,当然你可以根据自己的需要进一步扩展之。