logback输出彩色日志,每个请求一种颜色

Posted by hcy on June 28, 2020

logback输出彩色日志,每个请求一种颜色,提高工作效率

Java项目,日志使用的时slf4j + logback,在服务器上查看日志时,都是黑白的,想要在服务器上输出的日志是彩色的,可以大大提高查看效率。

​ 配合slf4jMDC功能可以实现,可以实现一个请求链输出同一种颜色。

输出颜色的基本原理

linux是自带ANSI功能是支持彩色日志的,可以这样测试下,下面的代码会输出绿色的 日志两个字。

想知道的更多,可以自行搜索下ANSI关键词

1
echo -e "\033[32m 日志 \033[0m"

​ 所以只要我们将要输出的日志字符串用\033[32m \033[0m]包起来,则查看就是绿色的,

​ 同理除了绿色外还有其他颜色,只要切换数字即可,具体颜色如下。

1
2
3
4
5
6
7
8
9
String BLACK_FG = "30";
String RED_FG = "31";
String GREEN_FG = "32";
String YELLOW_FG = "33";
String BLUE_FG = "34";
String MAGENTA_FG = "35";
String CYAN_FG = "36";
String WHITE_FG = "37";
String DEFAULT_FG = "39";

简单颜色输出测试

创建Test.java,在linux下编译运行下面函数,输出确实变成了绿色。

1
2
3
4
5
6
7
8
9
    public static void main(String[] args) {

        String start = "\033[32m";
        String end = "\033[0m";

        System.out.println(start + "日志1" + end);
        System.out.println(start + "日志2" + end);

    }

logback对彩色日志的支持

​ 一般logback的配置大概像下面这样,其中的encoder默认是PatternLayoutEncoder类,它包装了PatternLayout类对日志进行处理格式化。

1
2
3
4
5
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} %-5p %C:%L-%m%n</pattern>
        </encoder>
    </appender>

PatternLayout内有多个converter,每个converter处理模式的一部分,如常用的%d%p%C都是已经注册好的,还有一些如%red %blue就是处理颜色的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
public class PatternLayout extends PatternLayoutBase<ILoggingEvent> {

    public static final Map<String, String> defaultConverterMap = new HashMap<String, String>();
    public static final String HEADER_PREFIX = "#logback.classic pattern: ";

    static {
        defaultConverterMap.putAll(Parser.DEFAULT_COMPOSITE_CONVERTER_MAP);

        defaultConverterMap.put("d", DateConverter.class.getName());
        defaultConverterMap.put("date", DateConverter.class.getName());

        defaultConverterMap.put("r", RelativeTimeConverter.class.getName());
        defaultConverterMap.put("relative", RelativeTimeConverter.class.getName());

        defaultConverterMap.put("level", LevelConverter.class.getName());
        defaultConverterMap.put("le", LevelConverter.class.getName());
        defaultConverterMap.put("p", LevelConverter.class.getName());

        defaultConverterMap.put("t", ThreadConverter.class.getName());
        defaultConverterMap.put("thread", ThreadConverter.class.getName());

        defaultConverterMap.put("lo", LoggerConverter.class.getName());
        defaultConverterMap.put("logger", LoggerConverter.class.getName());
        defaultConverterMap.put("c", LoggerConverter.class.getName());

        defaultConverterMap.put("m", MessageConverter.class.getName());
        defaultConverterMap.put("msg", MessageConverter.class.getName());
        defaultConverterMap.put("message", MessageConverter.class.getName());

        defaultConverterMap.put("C", ClassOfCallerConverter.class.getName());
        defaultConverterMap.put("class", ClassOfCallerConverter.class.getName());

        defaultConverterMap.put("M", MethodOfCallerConverter.class.getName());
        defaultConverterMap.put("method", MethodOfCallerConverter.class.getName());

        defaultConverterMap.put("L", LineOfCallerConverter.class.getName());
        defaultConverterMap.put("line", LineOfCallerConverter.class.getName());

        defaultConverterMap.put("F", FileOfCallerConverter.class.getName());
        defaultConverterMap.put("file", FileOfCallerConverter.class.getName());

        defaultConverterMap.put("X", MDCConverter.class.getName());
        defaultConverterMap.put("mdc", MDCConverter.class.getName());

        defaultConverterMap.put("ex", ThrowableProxyConverter.class.getName());
        defaultConverterMap.put("exception", ThrowableProxyConverter.class.getName());
        defaultConverterMap.put("rEx", RootCauseFirstThrowableProxyConverter.class.getName());
        defaultConverterMap.put("rootException", RootCauseFirstThrowableProxyConverter.class.getName());
        defaultConverterMap.put("throwable", ThrowableProxyConverter.class.getName());

        defaultConverterMap.put("xEx", ExtendedThrowableProxyConverter.class.getName());
        defaultConverterMap.put("xException", ExtendedThrowableProxyConverter.class.getName());
        defaultConverterMap.put("xThrowable", ExtendedThrowableProxyConverter.class.getName());

        defaultConverterMap.put("nopex", NopThrowableInformationConverter.class.getName());
        defaultConverterMap.put("nopexception", NopThrowableInformationConverter.class.getName());

        defaultConverterMap.put("cn", ContextNameConverter.class.getName());
        defaultConverterMap.put("contextName", ContextNameConverter.class.getName());

        defaultConverterMap.put("caller", CallerDataConverter.class.getName());

        defaultConverterMap.put("marker", MarkerConverter.class.getName());

        defaultConverterMap.put("property", PropertyConverter.class.getName());

        defaultConverterMap.put("n", LineSeparatorConverter.class.getName());

        defaultConverterMap.put("black", BlackCompositeConverter.class.getName());
        defaultConverterMap.put("red", RedCompositeConverter.class.getName());
        defaultConverterMap.put("green", GreenCompositeConverter.class.getName());
        defaultConverterMap.put("yellow", YellowCompositeConverter.class.getName());
        defaultConverterMap.put("blue", BlueCompositeConverter.class.getName());
        defaultConverterMap.put("magenta", MagentaCompositeConverter.class.getName());
        defaultConverterMap.put("cyan", CyanCompositeConverter.class.getName());
        defaultConverterMap.put("white", WhiteCompositeConverter.class.getName());
        defaultConverterMap.put("gray", GrayCompositeConverter.class.getName());
        defaultConverterMap.put("boldRed", BoldRedCompositeConverter.class.getName());
        defaultConverterMap.put("boldGreen", BoldGreenCompositeConverter.class.getName());
        defaultConverterMap.put("boldYellow", BoldYellowCompositeConverter.class.getName());
        defaultConverterMap.put("boldBlue", BoldBlueCompositeConverter.class.getName());
        defaultConverterMap.put("boldMagenta", BoldMagentaCompositeConverter.class.getName());
        defaultConverterMap.put("boldCyan", BoldCyanCompositeConverter.class.getName());
        defaultConverterMap.put("boldWhite", BoldWhiteCompositeConverter.class.getName());
        defaultConverterMap.put("highlight", HighlightingCompositeConverter.class.getName());

        defaultConverterMap.put("lsn", LocalSequenceNumberConverter.class.getName());

    }

    public PatternLayout() {
        this.postCompileProcessor = new EnsureExceptionHandling();
    }

    public Map<String, String> getDefaultConverterMap() {
        return defaultConverterMap;
    }

    public String doLayout(ILoggingEvent event) {
        if (!isStarted()) {
            return CoreConstants.EMPTY_STRING;
        }
        return writeLoopOnConverters(event);
    }

    @Override
    protected String getPresentationHeaderPrefix() {
        return HEADER_PREFIX;
    }
}

测试logback自带的颜色处理

​ 上面的PatternLayout自带了一些颜色,我们测试下能否正常使用。

​ 创建项目,添加logback依赖,配置文件和代码如下,可以看到输出的颜色被改变了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
	<!--使用%red()将要变色的模式包起来-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <Pattern>%red(%d{yyyy-MM-dd HH:mm:ss}) %-5p %C:%L-%m%n</Pattern>
        </encoder>
    </appender>

    <root level="debug">
        <appender-ref ref="STDOUT"/>
    </root>

</configuration>
1
2
3
4
5
6
7
    public static void main(String[] args) {

        Logger log = LoggerFactory.getLogger(Test.class);

        log.info("日志");

    }

image-20200629105812064

可以看到,使用%red()将想变色的部分包起来,就会变色。这个颜色在idea的控制台,或者linux下能看到。

logback变色的实现原理

​ 下面是ForegroundCompositeConverterBase的源码,可以看到它会在消息两端拼接上\033[xm这样的字符,和我们上面实例里手动拼接是一样的,所以才能展示不同的日志颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    
    public final static String ESC_START = "\033[";
    public final static String ESC_END = "m";
    String SET_DEFAULT_COLOR = ESC_START + "0;" + DEFAULT_FG + ESC_END;

    
    @Override
    protected String transform(E event, String in) {
        StringBuilder sb = new StringBuilder();
        sb.append(ESC_START);
        sb.append(getForegroundColorCode(event));
        sb.append(ESC_END);
        sb.append(in);
        sb.append(SET_DEFAULT_COLOR);
        return sb.toString();
    }

根据日志Level展示不同的颜色

SpringBoot是可以根据不同的日志级别展示不同颜色的,如error级别显示红色,info级别显示绿色。查看它的源码,发现它注册了自己写的Converterlogback里,名字叫clr

1
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />

​ 配置Pattern是这样的,后面的{faint}是这个转换器的参数,通过getFirstOption()能获取到。

1
%clr(%d{${yyyy-MM-dd HH:mm:ss.SSS}}){faint}

​ 这里逻辑很简单,她先调用getFirstOption()拿到配置的颜色,如果没有配置,则获取日志等级,从LEVELS里获取该等级对应的颜色,然后用该等级对应的颜色拼接在日志两端。

1
2
3
4
5
6
7
8
9
10
11
	@Override
	protected String transform(ILoggingEvent event, String in) {
        //ELEMENTS是一个map
		AnsiElement element = ELEMENTS.get(getFirstOption());
		if (element == null) {
			// LEVELS是一个map
			element = LEVELS.get(event.getLevel().toInteger());
			element = (element != null) ? element : AnsiColor.GREEN;
		}
		return toAnsiString(in, element);
	}

MDC功能

servlet的线程模型是每个请求创建一个线程处理,如果我们在请求开始向ThreadLocal里存入一个值,则整个请求链都能拿到相同的值,利用这一点可以实现一个请求链显示同一种颜色的日志。

MDC就是slf4j提供的ThreadLocal,存入里面的值可以在配置中配置输出到日志里。我们可以在请求开始时存入颜色到MDC里,输出日志时根据我们配置的颜色输出。

实验MDC功能

​ 在输出日志前将当前时间戳存入mdc,请求结束后移除mdc中对应的key。

1
2
3
4
5
6
7
8
9
    public static void main(String[] args) {
		//存入当前时间戳到MDC里,键为 mdc_key
        MDC.put("mdc_key", String.valueOf(System.currentTimeMillis()));

        Logger log = LoggerFactory.getLogger(Test.class);
        log.info("日志");

        MDC.remove("mdc_key");
    }

logback里使用%X{}取出存入的值 。

1
2
3
4
5
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <Pattern>%X{mdc_key} %d{yyyy-MM-dd HH:mm:ss} %-5p %C:%L-%m%n</Pattern>
        </encoder>
    </appender>

image-20200629112323480

日志中成功的输出了输出日之前存入MDC里的内容。

使用MDC实现每个请求输出一种颜色

创建一个Converter 继承ForegroundCompositeConverterBase,并将此Converter配置到logback的配置文件里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MdcColorConverter extends ForegroundCompositeConverterBase<ILoggingEvent> {

    public static final String key = "MDCCOLORCONVERTER_KEY";

    @Override
    protected String getForegroundColorCode(ILoggingEvent event) {
        //从event中获取MDCMap,再获取里面的key对应的值
        String color = event.getMDCPropertyMap().get(key);
        //如果没有返回默认颜色数值
        if (color == null) {
            return ANSIConstants.DEFAULT_FG;
        }
        //否则返回对应颜色的数值
        return color;
    }
}

创建一个filter,在请求进入servelt前,将随机颜色存入MDC内,并配置到spring里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MdcColorFilter implements Filter {

    private static List<String> list = new ArrayList<>();
    private static AtomicInteger atm = new AtomicInteger();

    {
        list.add(ANSIConstants.MAGENTA_FG); //洋红/紫色
        list.add(ANSIConstants.BLUE_FG); //蓝色
        list.add(ANSIConstants.CYAN_FG); //青色
        list.add(ANSIConstants.GREEN_FG); //绿色
        list.add(ANSIConstants.YELLOW_FG); //黄色
        list.add(ANSIConstants.RED_FG); //红色
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            //循环调用list内的颜色
            int index = atm.getAndIncrement() % list.size();
			//将颜色对用的数值存入MDC
            MDC.put(MdcColorConverter.key, list.get(Math.abs(index)));
            chain.doFilter(request, response);
        } finally {
            //从MDC中删除键值
            MDC.remove(MdcColorConverter.key);
        }
    }
}

创建一个servlet进行测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class TestController {

    private static Logger logger = LoggerFactory.getLogger(TestController.class);

    @RequestMapping("test")
    public void test() {
        logger.info("日志1");
        logger.warn("日志1");
        logger.error("日志1");
    }

}

Converter注册到logback上,配置如下,模式上使用我们注册的Converter将模式包起来。

1
2
3
4
5
6
7
8
9
10
<!-- 注册converter -->
<conversionRule conversionWord="mdcColor" converterClass="com.test.util.MdcColorConverter"/>

<property name="console_pattern" value="%mdcColor(%d{yyyy-MM-dd HH:mm:ss} %-5p %C:%L-%m%n)"/>
   
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
        <pattern>${console_pattern}</pattern>
    </encoder>
</appender>

输出日志如下,每个请求一种颜色,非常便于人类查看。

image-20200629113356747

总结

​ 以上就是输出彩色日志的方法,除了输出到控制台,也可以输出到文件里,在linux下使用tail -f 查看文件,或者使用 less -r 查看就是彩色的,注意这里的less -r需要加-r参数才可以。

​ 在本机调试时,因为只有自己访问,按照日志Level切换颜色看起来比较清晰,生产环境下,访问量大,多个请求日志之间的会交叉,使用MDC实现每个请求一种颜色比较便于人类观察。


转载请注明出处:https://www.huangchaoyu.com/2020/06/28/logback输出彩色日志/