可以说,日志服务是系统中不可或缺的基础模块之一,从小型网站,到大型企业级网站系统,都是如此。
但问题是,很多时候,由于考虑不周,日志服务是缺失的,或者是不全面的。由于激发的矛盾点是,日志服务不能满足日益增长的纪录需求,或者因为日志本身设计的不合理性影响了系统的正常运转,损害了主营业务,甚至还对日后升级切换统一的日志服务埋下了沉重的技术痛点。这些,皆因对日志服务没有清晰的理解与定位,也许是尚未对其引起关注。
需要纪录日志的场景可谓有很多,有用于开发调试排查问题用的,有用于关键业务节点的场景纪录,有对系统异常的监控,也有用于大数据分析的埋点和统计。除了要考虑日志服务的统一接入、统一格式、统一管理外,还要考虑到日志服务在高并发、高流量时不会对系统造成额外的损害。
1 PSR-3中的日志规范
在PHP开源社区里,已经存在很多成熟的日志服务解决方案。作为软件开发工程师,我们没有必要再去重复造轮子。如果确实有需要,也可以自建日志服务,但首先要熟悉PHP关于日志的约定。
摘自PSR-3的日志接口,日志的种类可分为8类,如下面代码所示:
<?php namespace Psr\Log; /** * Describes log levels */ class LogLevel { const EMERGENCY = 'emergency'; const ALERT = 'alert'; const CRITICAL = 'critical'; const ERROR = 'error'; const WARNING = 'warning'; const NOTICE = 'notice'; const INFO = 'info'; const DEBUG = 'debug'; }
对于日志服务的选择,有四种方案。
2 自建简易日志服务
第一种,自建简易日志服务。简单的做法,利用文件写入file_put_contents()就能快速实现一个日志服务了。日志信息的基本组成有:时间,日志级别或类型,日志内容,以及上下文场景信息。这些组成要素通常以竖线分割,每一条日志为一行,最后的上下文场景信息是一个数组,需要进行序列化。例如,如果需要纪录新客注册的情况,日志类似如下:
2018-06-18 02:21:31|INFO|新客注册|{"nickname":"张三"}
一个快速简单实现的参考日志类如下:
<?php class Logger { function log($level, $message, array $context = array()) { $file = '/path/to/application.log'; $logData = array( date('Y-m-d H:i:s', time()), strtoupper($level), $message, json_encode($context, JSON_UNESCAPED_UNICODE), ); $logStr = implode('|', $logData) . PHP_EOL; file_put_contents($file, $logStr, FILE_APPEND); } }
注意到,在实现上面日志服务时,需要指定日志文件的存放路径,在对上下文场景数组信息进行JSON编码时要注意传递JSON_UNESCAPED_UNICODE选项以字面编码多字节 Unicode 字符,在写入日志文件时要采用追加的方式而非覆盖,日志内容最后要添加换行。这些都是实现时的注意点。
实现日志类后,写入日志就非常简单了,只需要一行代码即可。
Logger::log('INFO', '新客注册', array('nickname' => '张三'));
但若想实现一个完备的日志服务,还有很长的路要走,除了上面这些基本的注意点外,还有日志文件的分割、备份,对日志内容特殊字符的处理,PHP代码执行的调用栈信息纪录,权限的判断,允许日志写入级别的配置,和日志纪录的性能提升。
可以说,自建日志服务,只能解决前期的小问题,小需求,以及是作为技术上的一个小小尝试。自己实现过日志服务,才能更好明白日志服务组件的重要性和其原理、约束和规范。下面我们转入开源社区是如何提供日志服务的。
3 使用开源框架提供的日志服务
第二种,使用开源框架提供的日志服务。在我们开发人员熟悉的PHP开源框架中,都会提供日志服务,例如Yii、CakePHP、Laravel、Symfony、ThinkPHP等。我们以CakePHP为例,简单看下如何使用CakePHP提供的日志,以及其内部是如何实现的。
根据CakePHP官方文档的说明,可通过Cake\Core\Log进行日志配置。你可以为不同级别指定日志存储的方式,以文件日志为例,可以指定日志文件的目录保存路径、日志文件名,以及允许的日志级别。
use Cake\Log\Log; // Short classname Log::config('debug', [ 'className' => 'File', 'path' => LOGS, 'levels' => ['notice', 'info', 'debug'], 'file' => 'debug', ]);
同样在CakePHP中,也是遵循了PSR-3的规范,支持POSIX系列的标准,日志级别分为以下八类。
Emergency: system is unusable Alert: action must be taken immediately Critical: critical conditions Error: error conditions Warning: warning conditions Notice: normal but significant condition Info: informational messages Debug: debug-level messages
配置好日志后,就可以通过静态类方法`Cake\Log\Log::write()
或者使用特质LogTrait
来纪录日志。例如CakePHP官网提示的示例:
Log::write('debug', 'Something did not work');
让我们稍微来看下`Cake\Log\Log::write()
的背后做了什么事情,从而窥探开源框架对于日志的一般设计思路。从CakePHP摘取的相关源代码片段如下:
<?php namespace Cake\Log; class Log { public static function write($level, $message, $context = []) { static::_init(); if (is_int($level) && in_array($level, static::$_levelMap)) { $level = array_search($level, static::$_levelMap); } if (!in_array($level, static::$_levels)) { throw new InvalidArgumentException(sprintf('Invalid log level "%s"', $level)); } $logged = false; $context = (array)$context; …… foreach (static::$_registry->loaded() as $streamName) { …… } return $logged; } }
部分代码有省略,但也不难看出其处理的核心路径和思路。首先,使用static::_init()
进行必要的初始化,再继续深入进去可以找到在Cake\Log\Log::_loadConfig()
方法会对前面配置的信息进行委托解析和加载。然后,对传递的日志级别参数进行转换和检测。接下来,提取上下文相关信息。最后,遍历已注册且加载的日志处理器,进行日志写入的授理操作。处理完毕后,返回日志处理的结果。
通常,开源框架都会提供多套不同的日志流处理方式。CakePHP则内置提供了三种不同的日志引擎,它们分别是:控制台标准和错误输出、文件日志和PHP系统日志。
$ tree ./Log/Engine/ ./Log/Engine/ |-- BaseLog.php |-- ConsoleLog.php |-- FileLog.php `-- SyslogLog.php 0 directories, 4 files
就Cake\Log\Engine\FileLog日志引擎而言,它的内部实现和我们前面介绍的简易文件日志基本类似。通过构造函数完成了基本的初始化工作后,在Cake\Log\Engine\FileLog::log()
方法内,就可以看到与前面例子的相似之处。不过,它的实现更为严谨,考虑的因素更多,实现更为细致。
作为完整的解说,这里再稍微粘贴一下Cake\Log\Engine\FileLog::log()
的相关代码片段,让大家有一个更全面的认识。
<?php namespace Cake\Log\Engine; class FileLog extends BaseLog { public function log($level, $message, array $context = []) { $message = $this->_format($message, $context); $output = date('Y-m-d H:i:s') . ' ' . ucfirst($level) . ': ' . $message . "\n"; $filename = $this->_getFilename($level); …… $pathname = $this->_path . $filename; …… $exists = file_exists($pathname); $result = file_put_contents($pathname, $output, FILE_APPEND); static $selfError = false; …… return $result; } }
在一开始,FileLog也会对传递进来的参数进行格式化,然后在前面添加当前时间,在行末追加换行。关键部分也是使用file_put_contents()函数把新的日志追加到日志文件。
小结一下,开源框架提供的日志组件,在使用前一般都可以进行配置,指定使用的日志流引擎、允许的日志级别和个性化的配置。在实现的内部,开源框架会提供统一的日志控件入口,并内置多种日志存储或处理方式。顺着这个思路,再去使用开源框架的日志就会更加得心应手了。
4 使用log4php日志组件
第三种,使用log4php日志组件。与前面开源框架内的日志不同的是,log4php是一个独立的、针对PHP、多功能日志框架。正如其官网介绍的,它的主要特性有:
第1级无序列表”>可通过XML文件、INI属性文件或者PHP文件进行配置
支持多种日志存储方式,包括:控制台的标准输出和错误输出、文件、邮件、数据库、Socket和系统日志
多套内置日志消息格式,包括:HTML、XML、自定义格式
在项目中引入log4php,然后进行使用,其过程是非常简单的,因为它已经把强大的封装好简单易用的接口。下面快速来领略一下它的魅力。下载log4php并且解压后,通过XML文件进行相关配置,参考log4php官网说明,但把最低日志级别设置为DEBUG。与此同时,为了接近前面的日志格式,我们还通过conversionPattern选项配置了日志的内容格式。创建config.xml文件,并放置以下内容:
<configuration xmlns="http://logging.apache.org/log4php/"> <appender name="myAppender" class="LoggerAppenderFile"> <param name="file" value="myLog.log" /> <layout class="LoggerLayoutPattern"> <param name="conversionPattern" value="%date{Y-m-d H:i:s}|%level|%msg%n" /> </layout> </appender> <root> <level value="DEBUG" /> <appender_ref ref="myAppender" /> </root> </configuration>
日志文件为当前目录的myLog.log文件,然后编写客户端使用代码,其实现逻辑是:加载log4php,加载xml配置文件,获取日志实例,最后写日志。
<?php // 根据解压的位置,加载log4php,也可以使用composer方式进行加载 include('./src/main/php/Logger.php'); // 加载xml配置文件 Logger::configure('config.xml'); // 获取日志实例 $log = Logger::getLogger('myLogger'); // 写日志 $log->trace("我的第一条日志"); // 不会纪录,因为:TRACE < WARN $log->debug("我的第二条日志"); $log->info("我的第三条日志"); $log->warn("我的第四条日志"); $log->error("我的第五条日志"); $log->fatal("我的第六条日志");
把上面的PHP代码保存到test.php文件,然后通过控制台命令$php test.php
执行,最后在myLog.log文件中,可以看到类似以下的日志内容。
$ cat ./myLog.log 2018-06-18 07:11:18|DEBUG|我的第二条日志 2018-06-18 07:11:18|INFO|我的第三条日志 2018-06-18 07:11:18|WARN|我的第四条日志 2018-06-18 07:11:18|ERROR|我的第五条日志 2018-06-18 07:11:18|FATAL|我的第六条日志
与PSR-3约定的规范不同的是,log4php只提供了6种日志级别,如上所示,日志级别从最高到最低依次是:FATAL、ERROR、WARN、INFO、DEBUG、TRACE。相比之下少了2种日志级别,但对于大部分的项目开发已经是适用的了。此外,log4php值得称赞的是,它提供了更为丰富的Appender,包括有文件、控制台、数据库、邮件、MongoDB、Socket、系统日志等。
只需要简单集成整合,就能在项目中使用功能强大的log4php。
5 使用日志生态系统
日志服务,从小的角度实现,可以是一个函数,或者一个日记类;而从大的层面来宏观看待,可以把日记的纪录、收集、分析等各个环节作为了一个子系统来看待,进而演变成由异构系统组成的更大的日志生态系统。当诸如log4php这样的日志类库无法满足日益庞大的企业级系统对于日志的需求时,可以考虑搭建更完备的日志系统。当下,比较流行的是ELK架构,由ElasticSearch 、Logstash 、Kibana三个系统组成。关于ELK的介绍,超出了本书的范畴,感兴趣的同学可以继续深入研究。
顺便也可以从日志这一点看出,任何一个基础模块,最开始都是以一个函数或者一个类的形式简单实现,其次到了框架这一层,则封装成通用的组件、包、类库或者插件。再往上,可以单独抽离出来,形成一个工具,一个子系统,一套模板。最后,独立的工具在大型企业级系统中无法满足苛刻的需求时,会催生更系统化、具备一定生态圈的产品、解决方案。
与日志服务相似的另一个案例则是消息队列。我们可以简单地使用数据库实现一个消息队列,也可以使用开源框架比如PhalApi的Task计划任务扩展类库,也可以使用类似RabbitMQ这样独立的消息队列类库,甚至还可以使用异构系统搭建一套企业级消息中心系统。
所以,我们平时在开发过程中,也可以顺着这个思维方向走。在开发功能,使用组件时,结合当前项目、公司和团队的情况,选择配备的对策。小团队用大系统,会成本高昂且收益不明显;大公司用小工具,就会步履维艰,捉衣见肘。
不管是使用何种日志服务,回到我们这一节的课题,在接入日志时我们都应尽量统一客户端使用的方式。不管是使用同时阻塞式写入,还是采用异步定时入库的策略,日志服务背后的实现应该对客户端开发人中是透明的。尽早考虑日志服务的设计,收益颇丰。