logback输出彩色日志,每个请求一种颜色,提高工作效率
Java
项目,日志使用的时slf4j + logback
,在服务器上查看日志时,都是黑白的,想要在服务器上输出的日志是彩色的,可以大大提高查看效率。
配合slf4j
的MDC
功能可以实现,可以实现一个请求链输出同一种颜色。
输出颜色的基本原理
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("日志");
}
可以看到,使用%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
级别显示绿色。查看它的源码,发现它注册了自己写的Converter
到logback
里,名字叫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>
日志中成功的输出了输出日之前存入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>
输出日志如下,每个请求一种颜色,非常便于人类查看。
总结
以上就是输出彩色日志的方法,除了输出到控制台,也可以输出到文件里,在linux
下使用tail -f
查看文件,或者使用 less -r
查看就是彩色的,注意这里的less -r
需要加-r
参数才可以。
在本机调试时,因为只有自己访问,按照日志Level切换颜色看起来比较清晰,生产环境下,访问量大,多个请求日志之间的会交叉,使用MDC
实现每个请求一种颜色比较便于人类观察。