设计一个精简易用的日志

本文大概是我在几年前开发个人项目时所做,如果有疏漏之处,请诸位补漏.

日志记录对于应用的维护特别是对于已部署到运行环境之后的应用调试都有着重要的意义。

对于一个应用的日志系统而言,首先必须得有一个日志对象,该对象负责记录日志信息。同时该信息可以输出到不同的位置,例如控制台,文件甚至网络中。对于信息的格式,则可以根据不同的需求,可以输出成普通文本,XML 或者 HTML 的格式。同时还需要对日志信息进行不同级别的分类,这样的好处是可以过滤冗余信息,只保留关键的日志。对于一个日志框架而言,日志对象必须是可配置的,它可以按照配置来输出到指定的目标,同时按照配置来决定输出的格式和决定何种级别以上的日志才能输出。

在我成为PHP程序员后,我使用过许多的PHP框架,也使用过太多大同小易的日志类,抑或者自己实现一个日志类也是非常简单的,譬如
这个 , 这个 , 这个

这些看起来都能够记录日志,但是 这真的就是我们需要的日志功能么?

接着我去问一个软件测试人员(非程序员),你理解的日志功能到底有哪些?
他给我的答案,大致如下:

记录信息: 能够在一个地方查看输出结果
分级输出: 能够过滤指定级别的日志记录
格式输出: 能够以不同的形式来输出,诸如 html,xml,txt等
报警提示: 错误并不能每次都能检测到,对于某些错误应该能够提醒应用维护人员

根据上述 4 条,其实 大部分框架中都基本实现了 1 - 3 这部分功能,比如

  1. 记录信息到本地文件,SAE环境,等等
  2. 过滤特定级别信息
  3. 格式输出,大部分使用场景都是 txt 格式的,扩展其它样式应该也不难

对于 **报警 **这一项基本都没有什么体现,而这一点我思索之后觉得其实是很重要的一个环节,拿我们日常开发来讲,假设此场景: 客服童鞋反映 一个线上bug突然出现,请你赶紧解决.

我们的解决思路大概是这样: 根据客服童鞋给的bug一些诸如截图,访问地址之类的信息去重现这个bug,如果能够重现成功,那么恭喜你;但是大部分线上bug很难重现,或者说是在某些特定环境下才能重现;

此时 我们就会去查应用日志(如果你没有记录,嘿嘿,那你...),我们要从庞大的日志文件中去定位记录的信息(如果按大小进行了分割的话就有"且众多"), 看到这里你是否想到了"报警" 是个多么有用的手段啊,不管是发邮件还是sms,抑或微信等消息....虽然不是个银弹,但是可以节省我们好多时间

在我的建议下 知名高性能日志组件Seaslog增加了报警机制.

此处给出我那时的实现代码,此处功能即将会被整理到一个单独的功能组件中实现ws-log

class Aert_Log
{
    const TRACE = 1;
    const DEBUG = 2;
    const INFO  = 3;
    const WARN  = 4;
    const ERROR = 5;
    const FATAL = 6;
        
    private $enable = false;
    private $level;
        
    /**
     * 日志存储器
     * @var Aert_LogAppender
     */
    private $appender;
    
    /**
     * 日志存储器
     * @var Aert_LogAppender
     */
    private $alert;
    private $alertLevel;
    private $enableAlert = false;
        
    private static $levelNames = array(
        1 => 'TRACE',
        2 => 'DBEUG',
        3 => 'INFO',
        4 => 'WARN',
        5 => 'ERROR',
        6 => 'FATAL',
    );
    
    /**
     * 返回指定的日志服务对象实例
     *
     * @param string $name
     * @param array $config
     *
     * @return Aert_Log
     */
    static function instance($name, array $config=array())
    {
        static $instances = array();
        if (!isset($instances[$name]))
        {
            $instances[$name] = new self($config);
        }
        return $instances[$name];
    }
    
    private function __construct(array $config)
    {
        $this->level = intval(val($config, 'level', self::WARN));
        $this->enable = (bool) val($config ,'enable' ,TRUE);
        
        if ($this->enable)
        {
            do {
                if ( empty($config['appender']) || empty($config['appender']['class']) )
                {
                    $this->enable = false;
                    break;
                }
                $class = $config['appender']['class'];
                $params = (array) val($config['appender'], 'config', NULL);
                
                $this->appender = new $class($params);
                
                if ( !empty($config['alert']) || !empty($config['alert']['class']) )
                {
                    $this->enableAlert = TRUE;                  
                    $this->alertLevel = (int) val($config['alert'] ,'level' ,self::ERROR);
                    
                    $class = $config['alert']['class'];
                    $params = (array) val($config['alert'], 'config', NULL);
                    
                    $this->alert = new $class($params);
                }
                
            } while (false);
        }
        
    }
    
    function log($level, $msg)
    {
        if (!$this->enable) return;
        if ($this->enableAlert && ($level >= $this->alertLevel))
        {
            $this->alert($level, $msg);
        }
        if ($level < $this->level) return;
        $this->appender->append(self::$levelNames[$level], $msg);
    }
    
    private function alert($level, $msg)
    {
        $this->alert->alert(self::$levelNames[$level], $msg);
    }
}

/**
 * 日志存储器
 */
class Aert_LogAppender
{
    function __construct(array $config)
    {
        $this->init($config);   
    }
    
    protected function init(array $config)
    {
        
    }
    
    function append($level, $msg)
    {
        
    }
}

/**
 * 日志警报器
 */
class Aert_LogAlert
{
    function __construct(array $config)
    {
        $this->init($config);   
    }
    
    protected function init(array $config)
    {
        
    }
    
    function alert($level, $msg)
    {
        
    }
}

将日志的存储以及警报进行了分离,可以大大简化自定义日志处理的复杂度以及增强处理的多样性.比如可以单独实现File存储,SAE存储等,对报警器则可以实现控制台(一般是浏览器)输出,邮件,SMS,QQ,微信,SMS等多种.

以下给出2种实现形式,其它的就由大家自己去抽象

控制台(一般是浏览器)输出实现

<?php
/**
 * 日志警报器 -- Console
 * 
 * 监听指定错误级别,并直接打印到控制台
 */
class LogAlert_Console extends Aert_LogAlert
{
    function alert($level, $msg)
    {
        if (AERT_ISCLI)
        {
            fwrite(STDOUT, PHP_EOL . "[$level]: " . print_r($msg,true) . PHP_EOL);
        }
        else
        {
            if (is_string($msg))
            {
                echo "<BR />[$level]: " . print_r($msg,true);
            }
            else
            {
                dump($msg,"[{$level}]");                    
            }               
        }       
    }
}

火狐插件FirePHP实现

<?php
#{{{
app_import_file('/Lib/FirePHP.class.php');
#}}}

/**
 * 日志警报器 -- FirePHP
 * 
 * 监听指定错误级别,并通过火狐扩展 FirePHP 通知开发人员
 */
class LogAlert_FirePHP extends Aert_LogAlert
{
    function alert($level, $msg)
    {
        if (is_object($msg) && $msg instanceof TableRows)
        {
            /* @var $msg TableRows */
            $caption = $msg->getCaption();
            if (empty($caption)) $caption = $level;
            
            FirePHP::getInstance(true)->table($caption, $msg->combingRows());
            return;
        }
        
        switch ($level)
        {
            case 'TRACE':
                FirePHP::getInstance(true)->log($msg,$level);
                break;
            case 'DBEUG':
                FirePHP::getInstance(true)->log($msg,$level);               
                break;
            case 'INFO':
                FirePHP::getInstance(true)->info($msg);
                break;
            case 'WARN':
                FirePHP::getInstance(true)->warn($msg);
                break;
            case 'ERROR':
                FirePHP::getInstance(true)->error($msg);
                break;
            case 'FATAL':
                FirePHP::getInstance(true)->error($msg,$level);
                break;
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容