美文网首页
日志模块

日志模块

作者: imjcw | 来源:发表于2018-09-18 08:47 被阅读0次

    从零开始编写一个PHP框架 系列的《日志模块》

    项目地址:terse

    前言

    在一个系统中,日志模块会记录系统的运行情况,可能是异常,也可能是我们的一些调试信息。

    需求分析

    • 可以实现记录功能
    • 可以指定记录级别
    • 可能有多种日志记录方式
    • 可以指定记录方式
    • 可以指定记录格式

    目录结构

    这个模块需要实现的东西比较多,涉及多个文件,索性先将目录结构抛出来。

    .
    ├── Adapter
    │   └── File.php      文件日志类
    ├── Adapter.php       日志构抽象类
    ├── Formatter
    │   └── Line.php      行格式化类
    └── Formatter.php     格式化抽象类
    

    级别定义及释义

    名称 类型 释义
    DEBUG 调试 调试信息
    INFO 信息 程序输出信息
    NOTICE 通知 程序可以运行,但是还不够完美的错误
    WARNING 警告 需要发出警告的错误
    ERROR 一般错误 一般性错误
    ALERT 警戒性错误 必须被立即修改的错误
    CRITICAL 临界值错误 超过临界值的错误
    EMERG 严重错误 导致系统崩溃无法使用

    日志抽象类

    考虑会有多种记录方式,所以我们需要一个抽象类,来定义一些基本操作。

    <?php
    abstract class Adapter
    {
    }
    

    抽象方法

    由于日志的多样化,我们需要将数据拼凑的操作放在抽象类里,关于构造函数、写日志和关闭连接的动作要在子类里去操作。

    <?php
    abstract class Logger
    {
        /**
         * 构造函数
         * 
         * @param string $name
         * @param mixed  $options
         */
        abstract function __construct(string $name, array $options = []);
    
        /**
         * 写入日志
         * 
         * @param  array  $data
         * @return void
         */
        abstract public function save(array $data);
    
        /**
         * 关闭连接
         * 
         * @return void
         */
        abstract public function close();
    }
    

    属性初始化

    上面的表格已经定义了各个级别,并加了相应的释义,在类初始化的时候,会默认初始化各个级别。

    <?php
    abstract class Adapter
    {
        /**
         * 调试
         * 调试信息
         */
        const DEBUG     = 'DEBUG';
    
        /**
         * 信息
         * 程序输出信息
         */
        const INFO      = 'INFO';
    
        /**
         * 通知
         * 程序可以运行,但是还不够完美的错误
         */
        const NOTICE    = 'NOTICE';
    
        /**
         * 警告
         * 需要发出警告的错误
         */
        const WARNING   = 'WARNING';
    
        /**
         * 一般错误
         * 一般性错误
         */
        const ERROR     = 'ERROR';
    
        /**
         * 警戒性错误
         * 必须被立即修改的错误
         */
        const ALERT     = 'ALERT';
    
        /**
         * 临界值错误
         * 超过临界值的错误
         */
        const CRITICAL  = 'CRITICAL';
    
        /**
         * 严重错误
         * 导致系统崩溃无法使用
         */
        const EMERG     = 'EMERG';
    }
    

    方法初始化

    如果对外只提供一个方法,那我们每次都需要注明需要什么级别的日志,如下:

    $logger->log(Logger::DEBUG, $message);
    

    为了语义化和统一,每个类型都会提供一个方法供外界调用,而 log 方法会变为受保护的方法。

    <?php
    class Adapter
    {
        ...
    
        /**
         * 调试
         * 
         * @param  string $message
         * @return void
         */
        public function debug($message) {
            $this->log(self::DEBUG, $message);
        }
    
        /**
         * 信息
         * 
         * @param  string $message
         * @return void
         */
        public function info($message) {
            $this->log(self::INFO, $message);
        }
    
        /**
         * 通知
         * 
         * @param  string $message
         * @return void
         */
        public function notice($message) {
            $this->log(self::NOTICE, $message);
        }
    
        /**
         * 警告
         * 
         * @param  string $message
         * @return void
         */
        public function warning($message) {
            $this->log(self::WARNING, $message);
        }
    
        /**
         * 一般错误
         * 
         * @param  string $message
         * @return void
         */
        public function error($message) {
            $this->log(self::ERROR, $message);
        }
    
        /**
         * 警戒性错误
         * 
         * @param  string $message
         * @return void
         */
        public function alert($message) {
            $this->log(self::ALERT, $message);
        }
    
        /**
         * 临界值错误
         * 
         * @param  string $message
         * @return void
         */
        public function critical($message) {
            $this->log(self::CRITICAL, $message);
        }
    
        /**
         * 严重错误
         * 
         * @param  string $message
         * @return void
         */
        public function emerge($message) {
            $this->log(self::EMERG, $message);
        }
    
        ...
    }
    

    事务

    有时候,我们需要在一次操作中,写入多次日志。同时,如果出现异常,则回滚之前需要写入的内容。

    针对这种情况,我们需要实现一种类似于数据库事务的功能。

    然而,这里的事务和数据库的事务有所区别,主要是为了我们解决写入多行日志准备,不过在语义和使用上和数据库事务一致。

    <?php
    abstract class Logger
    {
        ...
    
        /**
         * 日志信息
         * 
         * @var array
         */
        protected $_log = [];
    
        /**
         * 事务
         * 
         * @var boolean
         */
        protected $_transcation = false;
    
        /**
         * 开启事务
         * 
         * @return void
         */
        public function begin()
        {
            $transcation = $this->_transcation;
    
            if ($transcation) {
                throw new \Exception('不可重复开启事务', -__LINE__);
            }
    
            $this->_transcation = true;
        }
    
        /**
         * 提交事务
         * 
         * @return mixed
         */
        public function commit()
        {
            $transcation = $this->_transcation;
            $log = $this->_log;
    
            if (!$transcation || !$log) {
                return false;
            }
    
            $this->_transcation = false;
    
            foreach ($log as $row) {
                $this->save($row);
            }
    
            $this->_log = [];
        }
    
        /**
         * 回滚事务
         * 
         * @return void
         */
        public function rollback()
        {
            $this->_transcation = false;
            $this->_log = [];
        }
    
        ...
    }
    

    log 的实现

    到这里,我们需要实现 log 方法了。

    log 需要辨别两种情况:一种是,不存在事务的时候,直接写入文件。一种是,存在事务时,等 commit 再提交。

    <?php
    abstract class Logger
    {
        ...
    
        /**
         * 记录日志
         * 
         * @param  string $level
         * @param  mixed  $message
         * @return void
         */
        protected function log($level, $message)
        {
            $data = [
                'level' => $level,
                'message' => $message,
                'time' => time()
            ];
    
            $transcation = $this->_transcation;
    
            if ($transcation) {
                $this->_log[] = $data;
            } else {
                $this->save($data);
            }
        }
    }
    

    设置格式化类

    在写入日志的时候,我们总是需要一些漂亮统一的格式,这时,我们需要一个单独的类来进行处理。

    <?php
    abstract class Logger
    {
        ...
    
        /**
         * 格式化类
         * 
         * @var Formatter
         */
        protected $_formatHandler;
    
        /**
         * 格式化类
         * 
         * @param Formatter $formatHandler
         */
        public function setFormatHandler(Formatter $formatHandler)
        {
            $this->_formatHandler = $formatHandler;
        }
    
        ...
    }
    

    至此,抽象类结束。

    文件日志类

    在一般系统日志中,文件日志是最为常见的,这里以文件日志为例,实现上述抽象类。

    初始化类

    按照日常习惯,我们这个类有打开、写入、关闭的功能,同时,我们需要指定日志地址,或者其它配置。

    <?php
    class File extends Adapter
    {
        /**
         * 文件句柄
         * 
         * @var resource
         */
        protected $_fileHanlder;
    
        /**
         * 日志文件地址
         * 
         * @var string
         */
        protected $_filePath;
    
        /**
         * 额外配置
         * 
         * @var array
         */
        protected $_options = [];
    
        /**
         * 构造函数
         * 
         * @param string $name
         * @param array  $options
         */
        function __construct(string $name, array $options = [])
        {
            $this->_filePath = $name;
            $this->_options = $options;
            $this->open();
        }
    
        /**
         * 打开文件
         * 
         * @return void
         */
        protected function open()
        {
        }
    
        /**
         * 存储文件
         * 
         * @param  array  $data
         * @return mixed
         */
        protected function save(array $data)
        {
        }
    
        /**
         * 关闭文件
         * 
         * @return void
         */
        public function close()
        {
        }
    
        /**
         * 析构函数
         */
        function __destruct()
        {
            $this->close();
        }
    }
    

    完善打开文件

    我们需要写入日志,当然,指定的文件地址可能不存在,目录也有可能不存在。

    logs/test.log             # test.log 不存在
    logs/2018/09/18/test.log  # 2018/09/18/test.log 不存在
    

    所以我们需要考虑所有情况,不存在则创建。

    <?php
    /**
     * 打开文件
     * 
     * @return void
     */
    protected function open()
    {
        $filePath = $this->_filePath;
    
        // 获取文件目录
        $dir = dirname($filePath);
    
        // 检测创建目录
        if (!is_dir($dir)) {
            if (!mkdir($dir, 0666, true)) {
                throw new \Exception('文件目录创建失败,目录:' . $dir, -__LINE__);
            }
        }
    
        if (!is_writable($dir)) {
            throw new \Exception('不存在创建目录的权限,请分配后重试,目录:' . $dir, -__LINE__);
        }
    
        // 检测创建文件
        if (!is_file($filePath)) {
            if (!touch($filePath)) {
                throw new \Exception('文件创建失败,文件:' . $filePath, -__LINE__);
            }
        }
    
        // 开启文件,后续可以把 mode 交给配置参数
        $fileHanlder = @fopen($filePath, 'a');
        $this->_fileHanlder = $fileHanlder;
    }
    

    完善保存文件

    到这里是真正写入了,上面我们为了能够让日志输出的更加漂亮,添加了一个 Formatter,它将在这里起作用。

    <?php
    /**
     * 存储文件
     * 
     * @param  array  $data
     * @return mixed
     */
    protected function save(array $data)
    {
        // 文件路径
        $filePath = $this->_filePath;
    
        // 格式化句柄
        $formatHandler = $this->_formatHandler;
    
        if (!$formatHandler) {
            throw new \Exception('未指定格式化类型', -__LINE__);
        }
    
        // 文件句柄
        $fileHanlder = $this->_fileHanlder;
    
        // 如果不是资源类型,可能在创建目录或者创建文件时失败,也有可能没有写权限
        if (!is_resource($fileHanlder)) {
            throw new \Exception('文件资源错误', -__LINE__);
        }
    
        // 写入文件
        fwrite($fileHanlder, $formatHandler->format($data['level'], $data['message'], $data['time']));
    }
    

    完善关闭文件

    关闭什么面特别的,直接上代码。

    <?php
    /**
     * 关闭文件
     * 
     * @return void
     */
    public function close()
    {
        $fileHanlder = $this->_fileHanlder;
        fclose($fileHanlder);
    }
    

    格式化抽象类

    相对于日志抽象类,格式化抽象类就简单多了。

    我们需要让其按照我们想要的格式写入日志,那么我们就需要一个配置格式的参数。由于日期比较特殊,这里也给个配置的参数。

    当然,既然是格式化,那当然少不了格式化的方法了。

    <?php
    abstract class Formatter
    {
        /**
         * 消息体格式化
         * 
         * @var string
         */
        protected $_format = '[:date:] [:level:] :message:';
    
        /**
         * 日期格式化
         * 
         * @var string
         */
        protected $_dateFormat = 'Y-m-d H:i:s';
    
        /**
         * 构造函数
         * 
         * @param string $format
         * @param string $dateFormat
         */
        function __construct(string $format = '', string $dateFormat = '')
        {
            if ($format) {
                $this->_format = $format;
            }
            if ($dateFormat) {
                $this->_dateFormat = $dateFormat;
            }
        }
    
        /**
         * 格式化
         * 
         * @param  string $level
         * @param  string  $message
         * @param  string $time
         * @return mixed
         */
        abstract public function format(string $level, string $message, string $time);
    }
    

    行格式化类

    这里以行格式化作为例子。

    我们需要按照指定的格式来渲染每一行记录,我在设定配置的时候,个体每个类型,如:datelevelmessage,两边都添加了 :,主要用作区分。

    这里使用 str_replace 可以批量操作。

    <?php
    class Line extends Formatter
    {
        /**
         * 格式化
         * 
         * @param  string $level
         * @param  string $message
         * @param  string $time
         * @return string
         */
        public function format(string $level, string $message, string $time)
        {
            $format = $this->_format;
            $dateFormat = $this->_dateFormat;
    
            $date = date($dateFormat, $time);
            return str_replace([':date:', ':level:', ':message:'], [$date, $level, $message], $format) . PHP_EOL;
        }
    }
    

    到此,整个日志模块就告一段落了。

    完整代码

    本来准备放的,但是涉及四个文件,而且代码总数也蛮长的,这里就不放了,有需要的小伙伴可以去我的代码库里看。

    总结

    本来是想用一个类来解决掉的,也没有想写那么多,但是写写就发现需要更好的设计。

    下一篇《事件管理模块》

    相关文章

      网友评论

          本文标题:日志模块

          本文链接:https://www.haomeiwen.com/subject/rtcliftx.html