MelonBlog

优雅的打印业务日志

前言

开发和维护业务系统时,经常需要根据日志来排查业务问题,这时候日志会起到非常大的作用,但是打印日志的格式没有一个标准,普遍都是五花八门的,所以我写了一个打印关键日志的工具类,来辅助打印一些关键的业务日志

设计思路

业务逻辑会沿着代码逻辑递进,从外层业务逐步走到内层子业务,一个复杂的业务可能包含许多子业务

image

平常我们打印日志,很难直观体现业务流程,一般都是打印参数或者某个临时变量,排查简单的问题,看变量的值是能够推导出问题的,但是如果碰到业务特别复杂,流程很长的场景,光看变量的值对于排查问题来说帮助就有限。


我想到一个思路,就是在日志里增加业务上下文。

实现方法就是,在Threadlocal里,保存一个业务的栈,栈数据结构可以很好的存储上图的流程,例如通过入栈操作就可以模拟出从某一个业务执行到另外一个业务。无论是嵌套还是组合关系都能够很好的体现出来。

每一个业务里面可能还有很多执行分支,所以业务数据结构里我加了一个标签栈,用来标记业务流程走到了哪个分支。

代码

package com.demo.util;
import com.demo.exception.BaseRuntimeException;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import java.util.Formatter;
import java.util.Stack;
public class Footprint {
    private Logger logger;
    private static final ThreadLocal<Stack<Logic>> logicsThreadLocal = ThreadLocal.withInitial(Stack::new);
    public static Footprint bind(Logger logger) {
        Footprint footprint = new Footprint();
        footprint.logger = logger;
        return footprint;
    }
    public void newLogic(String logic) {
        Footprint.newLogic(this.logger, logic);
    }
    public static void newLogic(Logger logger, String logic) {
        Stack<Logic> logics = logicsThreadLocal.get();
        logics.clear();
        Logic newLogic = new Logic(logic);
        logics.push(newLogic);
        logger.info("{}", newLogic);
    }
    public void pushLogic(String logic) {
        Footprint.pushLogic(this.logger, logic);
    }
    public static void pushLogic(Logger logger, String logic) {
        Stack<Logic> logics = logicsThreadLocal.get();
        if (logics.isEmpty()) {
            newLogic(logger, logic);
            return;
        }
        // 防止logic异常未被释放
        if (logics.size() > 10) {
            logger.warn("logics size is too large, clear all logics");
            logics.clear();
            logics.push(new Logic(logic));
            return;
        }
        Logic currentLogic = logics.peek();
        Logic newLogic = new Logic(currentLogic.logicName + "->" + logic);
        logics.push(newLogic);
        logger.info("{}", newLogic);
    }
    public void popLogic() {
        Footprint.popLogic(this.logger);
    }
    public static void popLogic(Logger logger) {
        Stack<Logic> logics = logicsThreadLocal.get();
        if (logics.isEmpty()) {
            logger.warn("exit logic failed, logics is empty");
            return;
        }
        logics.pop();
    }
    public void popLogic(String logic) {
        Footprint.popLogic(this.logger, logic);
    }
    public static void popLogic(Logger logger, String logic) {
        Stack<Logic> logics = logicsThreadLocal.get();
        if (logics.isEmpty()) {
            logger.warn("exit logic {} failed, logics is empty", logic);
            return;
        }
        if (logics.peek().getLogicName().equals(logic)) {
            logics.pop();
        }
    }
    public void newTag(String... newTags) {
        Footprint.newTag(this.logger, newTags);
    }
    public static void newTag(Logger logger, String... newTags) {
        Logic logic = getCurrentLogic(logger);
        if (logic == null) {
            return;
        }
        Stack<String> tags = logic.getTags();
        if (!tags.isEmpty()) {
            tags.clear();
        }
        for (String tag : newTags) {
            tags.push(tag);
        }
        logger.info("{}", logic);
    }
    public void pushTag(String tag) {
        Footprint.pushTag(this.logger, tag);
    }
    public static void pushTag(Logger logger, String tag) {
        Logic logic = getCurrentLogic(logger);
        if (logic == null) {
            return;
        }
        logic.getTags().push(tag);
        logger.info("{}", logic);
    }
    public void popTag() {
        Footprint.popTag(this.logger);
    }
    public static void popTag(Logger logger) {
        Logic logic = getCurrentLogic(logger);
        if (logic == null) {
            return;
        }
        Stack<String> tags = logic.getTags();
        if (tags.isEmpty()) {
            logger.warn("pop tag failed, tags is empty");
            return;
        }
        tags.pop();
    }
    public void popTag(String tag) {
        Footprint.popTag(this.logger, tag);
    }
    public static void popTag(Logger logger, String tag) {
        Logic logic = getCurrentLogic(logger);
        if (logic == null) {
            return;
        }
        Stack<String> tags = logic.getTags();
        if (tags.peek().equals(tag)) {
            tags.pop();
        }
    }
    public void clearTag() {
        Footprint.clearTag(this.logger);
    }
    public static void clearTag(Logger logger) {
        Logic logic = getCurrentLogic(logger);
        if (logic == null) {
            return;
        }
        Stack<String> tags = logic.getTags();
        tags.clear();
    }
    private static Logic getCurrentLogic(Logger logger) {
        Stack<Logic> logics = logicsThreadLocal.get();
        if (logics.isEmpty()) {
            logger.warn("push tag failed, logics is empty");
            return null;
        }
        return logics.peek();
    }
		
		public void args(Object... args) {
        Footprint.args(this.logger, args);
    }
    public static void args(Logger logger, Object... args) {
        if (args.length % 2 != 0) {
            logger.error("args length must be even");
            return;
        }
        JSONObject jsonArgs = new JSONObject();
        for (int i = 0; i < args.length; i += 2) {
            jsonArgs.set(args[i].toString(), args[i + 1]);
        }
        Stack<Logic> logics = logicsThreadLocal.get();
        if (logics.isEmpty()) {
            logger.debug("\n{}", jsonArgs.toJSONString(4));
        } else {
            Logic logic = logics.peek();
            logger.debug("{} ==> \n{}", logic, jsonArgs.toJSONString(4));
        }
    }
    public void info(String format, Object... args) {
        Footprint.info(this.logger, new Formatter().format(format, args).toString());
    }
    public static void info(Logger logger, String format, Object... args) {
        Stack<Logic> logics = logicsThreadLocal.get();
        if (logics.isEmpty()) {
            logger.warn("print failed, logics is empty");
            logger.info(new Formatter().format(format, args).toString());
            return;
        }
        Logic logic = logics.peek();
        logger.info("{} ==> {}", logic, new Formatter().format(format, args));
    }
    public void error(String format, Object... args) {
        Footprint.error(this.logger, format, args);
    }
    public static void error(Logger logger, String format, Object... args) {
        Stack<Logic> logics = logicsThreadLocal.get();
        if (logics.isEmpty()) {
            logger.warn("print failed, logics is empty");
            logger.error(new Formatter().format(format, args).toString());
            return;
        }
        Logic logic = logics.peek();
        logger.error("{} ==> {}", logic, new Formatter().format(format, args));
    }
    public void throwError(String log) throws BaseRuntimeException {
        Footprint.throwError(this.logger, log);
    }
    public static void throwError(Logger logger, String log) throws BaseRuntimeException {
        Stack<Logic> logics = logicsThreadLocal.get();
        if (logics.isEmpty()) {
            logger.error("{}", log);
            throw new BaseRuntimeException(log);
        }
        Logic logic = logics.peek();
        logger.error("{} ==> {}", logic, log);
        logger.error("{} ==> 异常退出", logic);
        logics.pop();
        throw new BaseRuntimeException(log);
    }
    public static void clear(Logger logger) {
        Stack<Logic> logics = logicsThreadLocal.get();
        if (logics.isEmpty()) {
            return;
        }
        Logic logic = logics.peek();
        logger.error("{} ==> 异常退出", logic);
        logicsThreadLocal.get().clear();
    }
    @Data
    public static class Logic {
        private String logicName;
        private Stack<String> tags;
        public Logic(String logicName) {
            this.logicName = logicName;
            this.tags = new Stack<>();
        }
        public String toString() {
            if (tags.isEmpty()) {
                return String.format("[fp][%s]", logicName);
            }
            return String.format("[fp][%s]#[%s]", logicName, StringUtils.join(tags, "|"));
        }
    }
}

测试代码:

package com.demo.util;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FootprintTest {
    private final Footprint fp = Footprint.bind(log);
    public void businessA() {
        fp.pushLogic("子业务A");
        fp.newTag("标签1");
        fp.info("白日依山尽");
        fp.pushTag("标签2");
        fp.info("黄河入海流");
        fp.popTag();
        businessB();
        fp.newTag("标签5");
        fp.info("欲穷千里目");
        fp.info("更上一层楼");
        fp.popLogic();
    }
    @Data
    public static class Person {
        private String name;
        private int age;
    }
    public void businessB() {
        fp.pushLogic("子业务B");
        fp.newTag("标签3");
        fp.info("抽刀断水水更流");
        fp.newTag("标签4");
        fp.info("举杯消愁愁更愁");
        Person person = new Person();
        person.age = 18;
        person.name = "李白";
        fp.arg("person", person);
        fp.popLogic();
    }
    public static void main(String[] args) {
        Footprint.newLogic(log, "测试Footprint");
        FootprintTest footprintTest = new FootprintTest();
        footprintTest.businessA();
    }
}

打印内容:

[fp][测试Footprint]
[fp][测试Footprint->子业务A]
[fp][测试Footprint->子业务A]#[标签1]
[fp][测试Footprint->子业务A]#[标签1] ==> 白日依山尽
[fp][测试Footprint->子业务A]#[标签1|标签2]
[fp][测试Footprint->子业务A]#[标签1|标签2] ==> 黄河入海流
[fp][测试Footprint->子业务A->子业务B]
[fp][测试Footprint->子业务A->子业务B]#[标签3]
[fp][测试Footprint->子业务A->子业务B]#[标签3] ==> 抽刀断水水更流
[fp][测试Footprint->子业务A->子业务B]#[标签4]
[fp][测试Footprint->子业务A->子业务B]#[标签4] ==> 举杯消愁愁更愁
[fp][测试Footprint->子业务A->子业务B]#[标签4] ==> 
{
    "person": {
        "name": "李白",
        "age": 18
    }
}
[fp][测试Footprint->子业务A]#[标签5]
[fp][测试Footprint->子业务A]#[标签5] ==> 欲穷千里目
[fp][测试Footprint->子业务A]#[标签5] ==> 更上一层楼