最近写laravel的时候觉得框架日志实在是一言难尽,由于习惯了前东家的日志系统,感觉主要是由以下几点不合理:
没有功能分区,理论上来讲,请求信息,数据库信息和报错信息,简单的原生日志没办法区分。
日志信息不够详细,比如说报错的时候的请求信息,请求参数,执行位置,数据库执行信息,上下文,加上很大的调用栈信息一起,很难分辨。
没有request-id,理想的情况下,从接收到请求之后,添加request-id,然后每次执行或者进入其他模块交互都添加相关ID,这样如果一次执行有问题查找日志也很方便。
看了很多网上的教程,大多数都是类似于重新搞一个Logger然后添加一些自己需要的信息,但是有个问题就是类似于这种教程都是使用的自己重新开发的日志类,如果是修改已有的项目感觉有点麻烦,诉求是开发一个通用的小插件或者composer,保证适用并且不需要改动原有代码;
日志格式如下,按照每天+分类创建日志。
接下来看代码,目录结构如下
Exceptions下面是可能遇见的异常
extend下面是常见的日志格式处理
facades下面是需要的门面,后面会把这个门面注册到laravel中
handers下面是根据monolog handler基础上自定义的,添加了自定义的格式
processor下面是一个抽象的processor类和自定义的处理类,类似于RequestProcessor和SqlProcessor用于实现具体的写日志部分或者说用于日志的具体格式等参数的定义
Logger.php是之后注册到service-provider中的文件,主要定义了你需要之行哪些processor
首先,声明一个抽象类Processors.php
abstract class Processor
{
protected $path;
protected $type;
protected $level;
// 日志执行主体
abstract public function boot($logger);
// 日志存储路径
abstract public function getPath();
// 日志类型,按天/年/月/小时等
abstract public function getType();
// 日志的级别
abstract public function getLevel();
// 此处还可以定义其他的参数的获取方法,比如request-id的获取规则,客户用户信息获取规则等通用的信息都可以在此处获取
}
其他的processor就按照以上的格式去实现四个方法即可。
举个例子
class SQLLogProcessor extends Processor
{
private function __construct()
{
// 这里是我定义的一些参数,requestId,用户ID等
$this->requestId = $this->getRequestId();
$this->operatorId = $this->getOperatorId();
// 这个是定义的标准日志格式,可以写到配置里面
$this->defaultFormat = $this->getDefaultFormat();
// 这个是根据每个processor特定的日志格式,也可以加到配置里面
$this->format = $this->getFormat();
// 写日志在这里
$this->initListen();
}
// 实现boot
public function boot($logger)
{
// 这里声明了hander和logger
$this->handler = Handler::getInstance('sql');
$this->logger = $logger;
// 这是一些配置,就是之前Processor里面的type,path,level等,因为每个都需要配置,所以当作公共方法写到了抽象类Processor里面
$this->configure($this->handler);
}
// 看一下方法举例
public function getPath()
{
$this->path = '配置里面取'; // 取值举例 storage_path('logs/sql/sql.log')
return $this->path;
}
public function getType()
{
$this->type = '配置里面取'; // 取值举例 daily
return $this->type;
}
public function getLevel()
{
$this->level = '配置里面取'; // 取值举例 debug
return $this->level;
}
public function getFormat()
{
if (is_null($this->format)) {
$this->format = '配置里面取'; // 取值举例 'time:{time} occurTime:{occurTime} sql:[{sql}]'
}
return $this->format;
}
// 简单来说数据库的原理就是监听Illuminate\Database\Events\QueryExecuted::class这个类,query是连接,biddings是sql里面的值
protected function initListen()
{
$events = app('events');
$queryCollector = $this;
$listener = '\Illuminate\Database\Events\QueryExecuted::class';
$events->listen($listener, function ($query, $bindings = null, $time = null, $connectionName = null, $occurTime = null) use
($queryCollector) {
$queryCollector->writeLog($query, $bindings, $time, $connectionName, $occurTime);
});
}
// 开始写了
public function writeLog($query, $bindings = null, $time = null, $connectionName = null, $occurTime = null)
{
// 把值替换进sql语句中
if (is_array($bindings)) {
$query = str_replace('%', '%%', $query);
$query = str_replace('?', "'%s'", $query);
$query = vsprintf($query, $bindings);
}
// 获取格式,举例time:{time} occurTime:{occurTime} sql:[{sql}],最后处理效果就是要把{}里面的东西替换成根据你的方法获得的值这个方法就结束了
$message = $this->getDefaultFormat() . $this->getFormat();
// 这是处理,保证这些func在你的processor或者抽象类中有就行
$replace = [
'{requestId}' => $this->getRequestId(),
'{operatorId}' => $this->getOperatorId(),
'{clientIp}' => $this->getClientIp(),
'{occurTime}' => $occurTime,
'{sql}' => $query,
'{time}' => $time
];
// 替换
$result = strtr($message, $replace);
// 这一步是在写了
$this->handler->info($result);
}
下面看Logger.class
class Logger
{
protected $handler;
public function __construct()
{
// 想之行的process都放在这
$this->registerSQLLog();
}
public function registerSQLLog()
{
$processor = SQLLogProcessor::getInstance();
$processor->boot($this);
}
// 在这个class里面重写通用记录日志的方法
protected function writeLog($level, $message, $context)
{
$this->handler->{$level}($message, $context);
}
public function emergency($message, array $context = [])
{
$this->writeLog(__FUNCTION__, $message, $context);
}
public function info($message, array $context = [])
{
$this->writeLog(__FUNCTION__, $message, $context);
}
还缺少一个门面。这个用于把log注册到框架中,这里跟原生日志格式是一样的
***Log.php
<?php
namespace 自定义;
use Illuminate\Support\Facades\Facade;
/**
* @see \Illuminate\Log\Writer
*/
class MyLogger extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'mylogger';
}
}
大体上的架子就可以了,然后看看怎么在laravel中用,
需要一个ServicesProvider
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Logger.php的namespace;
class LogServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton('mylogger', function ($app) {
return new Logger;
});
}
public function boot()
{
$this->app->make('mylogger');
$this->app->singleton('Psr\Log\LoggerInterface', function ($app) {
return $this->app->make('mylogger');
});
}
}
别忘记在config/app.php中加上这个serviceProvider
'providers' => [
...
// 日志能用了
App\Providers\LogServiceProvider::class,
],
'aliases' => [
// 可以覆盖原生日志,我这里定义了一种base类型的log,用于基础日志,也就是写Log::info直接记录到base下了。
'Log' => MyLogger那个Facade,
]
以后需要添加其他类型的日志,就写一个XXXProcessor类,实现Processor中的抽象方法,然后在Logger.class里面初始化的时候加上你那个XXXProcessor的boot的方法就行了。