自研日志打印器工具类


作者:刘仁鹏
GitHub地址:https://github.com/LingPaicoder/agile-base-4j

转载请注明出处


1. 前言

  • 今天为大家介绍的是一款自研的日志打印器。相信大家在工作中,少不了要和日志打交道。正所谓“日志打得好,bug没地儿跑”,清晰、简洁、全面的日志有助于快速定位问题,而混乱、冗余、丢三落四的日志则会浪费大量时间。
  • 那么一款好用的日志打印器应该具有哪些特性呢?笔者认为主要有以下几点:
  1. 支持多维分析
    比如,有时我们想从用户(userId)的维度来查看某一个用户对系统进行过哪些操作;有时我们想从请求的维度来查看一次请求的具体细节;有时我们又想从统计的维度来查看某些页面/接口的PV、UV。
  2. 支持上下文收集
    上下文是指在一次请求过程中,与之相关的信息,例如:START日志、事务Id、接口名、接口描述、接口实参、运行时参数、业务进度、耗时时长、END日志等等。有些信息在前置拦截器中就可获得,有些必须在业务代码中获得,而像耗时时长这样的信息,则适合放到后置拦截器中计算。
    日志打印,主要是为了记系统在当前上下文里的运行情况。如果系统出现问题,我们通过日志最想要得到的信息就是清晰的上下文。如果没办法做到这一点,日志只会越多越乱。
  3. 日志要易读、要格式化
    日志主要是面向人的,给人来看的。格式混乱、不易断句的日志,容易让人丢失掉排查问题的关键线索。
  4. 要对AOP友好
    AOP对Access日志、End日志、请求参数、处理时间等信息的打印更灵活。一个对AOP友好的日志打印器,可以事半功倍。

2. 简介

  • 今天要说的这款日志打印器是怎样做到上面那些特性的呢?先来看一下它都提供了哪些接口吧:
public interface LoggerDecorator extends Logger {
    /**
     * 设置logId
     */
    LoggerDecorator setLogId(String logId);

    /**
     * 设置前缀.可通过拦截器由{@link LogDesc#value()}提供
     */
    LoggerDecorator setBusinessPrefix(String prefix);

    /**
     * 注册参数
     */
    LoggerDecorator register(String key, Object val);

    /**
     * 清除
     */
    LoggerDecorator clear();
}
  1. setLogId
    setLogId接口通常在一个事务开始时调用。例如一次请求,可以在前置拦截器中通过该接口传入一个唯一性较强的logId(一般不需要绝对唯一,因为日志文件一般都会以天为单位截断,只要保持一天内不会重复即可)。然后在该请求内的任何日志打印,都会自动带上此logId。当需要查看某次请求的具体过程时,利用该功能可以很方便的实现。该接口是为事务/请求维度服务的。
  2. setBusinessPrefix
    setBusinessPrefix接口与setLogId类似,也是在事务开始时调用,事务内的任何日志都会自动带上接口传入的参数。不同的是,它是为功能/接口维度服务的。例如对于“用户注册”功能,可以将“用户注册”作为参数传入,或者直接传入接口名(eg:registUser)也可以。
    一般无论是web框架还是服务容器,都会提供在拦截器中获取Function对象的接口,因此能够获得方法名。或者自定义一个注解,加在方法上,就可以自定义功能描述信息了。像下面这样:
@LogDesc("查询商家信息")
public Response<Custom> getCustomById(long customId)
  1. register
    regist接口用来收集关键的运行时参数,例如会决定代码跳转的boolean类型变量(eg:isCustomExist)、用户主键等等。所有被注册了的K-V,都会在后续的任何打印(info/error/warn)时自动追加在日志内容之后。

  2. clear
    clear通常在事务结束时使用,例如后置拦截器。这个接口的功能是,将本次事务中的所有收集的内容(即通过之前三个接口收集的内容)清空。以免影响下次事务的日志打印。

3. 演示

  • 来看一下示例代码:
    下面的代码,模拟了1000次事务处理,日志打印分三个部分:前置拦截器日志打印、业务代码日志打印、后置拦截器日志打印。
public class LOGTest {

    private static final LoggerDecorator LOG = BaseLoggerDecorator.getLogger(LOGTest.class);

    public static void main(String[] args) throws Exception {
        for (int i = 1; i <= 1000; i++) {
            // 模仿前置拦截器
            LOG.setLogId(RandomUtil.shortUUID())
                    .setBusinessPrefix("具体业务描述")
                    .register("currNum", i)
                    .info("START");
            long startTime = System.currentTimeMillis();

            // 模仿业务代码
            LOG.register("userId", RandomUtil.nextInt(1000, 10000));
            Thread.sleep(RandomUtil.nextInt(5, 10));
            if (i % 2 == 0) {
                LOG.info("currNum是偶数");
            }
            if (i % 3 == 0) {
                LOG.info("currNum是3的倍数");
            }

            // 模仿后置拦截器
            long endTime = System.currentTimeMillis();
            LOG.register("usedTime", endTime - startTime).info("END");
            LOG.clear();
        }
    }

}
  • 部分日志截图:
consoleLog.png-403.6kB
consoleLog.png-403.6kB
  • 我们截取部分(前6次)日志来进行说明:
logId:6056ff3f52e6 【 具体业务描述 - START 】 - logArgs:{"currNum":1}
logId:6056ff3f52e6 【 具体业务描述 - END 】 - logArgs:{"usedTime":5,"userId":6404,"currNum":1}
logId:62971514bf59 【 具体业务描述 - START 】 - logArgs:{"currNum":2}
logId:62971514bf59 【 具体业务描述 - currNum是偶数 】 - logArgs:{"userId":3433,"currNum":2}
logId:62971514bf59 【 具体业务描述 - END 】 - logArgs:{"usedTime":5,"userId":3433,"currNum":2}
logId:59fd7fdc357f 【 具体业务描述 - START 】 - logArgs:{"currNum":3}
logId:59fd7fdc357f 【 具体业务描述 - currNum是3的倍数 】 - logArgs:{"userId":1736,"currNum":3}
logId:59fd7fdc357f 【 具体业务描述 - END 】 - logArgs:{"usedTime":6,"userId":1736,"currNum":3}
logId:adf9a8e07d1a 【 具体业务描述 - START 】 - logArgs:{"currNum":4}
logId:adf9a8e07d1a 【 具体业务描述 - currNum是偶数 】 - logArgs:{"userId":1805,"currNum":4}
logId:adf9a8e07d1a 【 具体业务描述 - END 】 - logArgs:{"usedTime":10,"userId":1805,"currNum":4}
logId:83f8366c488f 【 具体业务描述 - START 】 - logArgs:{"currNum":5}
logId:83f8366c488f 【 具体业务描述 - END 】 - logArgs:{"usedTime":6,"userId":7144,"currNum":5}
logId:c649de0705f2 【 具体业务描述 - START 】 - logArgs:{"currNum":6}
logId:c649de0705f2 【 具体业务描述 - currNum是偶数 】 - logArgs:{"userId":4828,"currNum":6}
logId:c649de0705f2 【 具体业务描述 - currNum是3的倍数 】 - logArgs:{"userId":4828,"currNum":6}
logId:c649de0705f2 【 具体业务描述 - END 】 - logArgs:{"usedTime":8,"userId":4828,"currNum":6}

1.多维分析:

  • 查看某一个用户(例如6404)对系统进行过哪些操作:
more log.txt | grep 'END' | grep '6404'

输出:
logId:6056ff3f52e6 【 具体业务描述 - END 】 - logArgs:{"usedTime":5,"userId":6404,"currNum":1}
  • 查看某次请求(例如第6次请求)的具体细节:
more log.txt | grep 'c649de0705f2'

输出:
logId:c649de0705f2 【 具体业务描述 - START 】 - logArgs:{"currNum":6}
logId:c649de0705f2 【 具体业务描述 - currNum是偶数 】 - logArgs:{"userId":4828,"currNum":6}
logId:c649de0705f2 【 具体业务描述 - currNum是3的倍数 】 - logArgs:{"userId":4828,"currNum":6}
logId:c649de0705f2 【 具体业务描述 - END 】 - logArgs:{"usedTime":8,"userId":4828,"currNum":6}
  • 查看请求的PV:
more log.txt | grep 'START' | grep '具体业务描述' | wc -l

输出:
6
  • ......

2.上下文收集:

  • setLogId接口可用来收集事务Id
  • setBusinessPrefix接口用来收集接口描述
  • register接口用来收集关键运行时参数
  • clear接口用来清空本次事务的信息

3.日志格式化:

  • 这里说的格式化,是指对业务代码的格式化。至于时间、日志级别、线程信息、类信息等,可以通过底层日志框架的配置文件来进行配置。例如:

    <Pattern>[%d{MM-dd HH:mm:ss SSS} %-5level] [%t] %c{3} - %m%n%ex</Pattern>
    
  • 日志打印器会按照下面的格式进行打印:

    logId:具体事务Id 【 具体业务描述 - 具体业务进度 】 - logArgs:{具体参数}
    //例如:
    logId:adf9a8e07d1a 【 用户注册 - 校验失败:姓名为空 】 - logArgs:{"userName":"","phone":"13200000000"}
    

4.AOP支持

  • 日志打印器内置三个ThreadLocal,分别存储logId、businessPrefix、关键运行时参数:
private static final ThreadLocal<String> LOG_ID_THREAD_LOCAL = new ThreadLocal<>();
private static final ThreadLocal<String> BUSINESS_PREFIX_THREAD_LOCAL = new ThreadLocal<>();
private static final ThreadLocal<Map<String, Object>> LOG_ARGS_THREAD_LOCAL = new ThreadLocal<>();
  • 具体使用方式可随业务场景自定义。

4. GitHub

  • 日志打印器的GitHub地址是https://github.com/LingPaicoder/agile-base-4j
  • 所有的校验器相关代码都放在 com.lpcoder.agile.base.forj.log 包下。
  • 使用示例代码,放在test的 com.lpcoder.agile.base.forj.log 包下。

end
欢迎留言反馈对日志打印器的改进意见。

扫码可关注微信公众号:


wx.jpg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343