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

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

  • TOC
    {:toc}
    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实现每个请求一种颜色比较便于人类观察。


logback输出彩色日志,每个请求一种颜色
https://www.huangchaoyu.com/2412438885.html
作者
hcy
发布于
2020年6月28日
更新于
2024年8月17日
许可协议