介绍
Logback Manual
三个模块
- logback-core模块为其他两个模块奠定了基础
- logback-classic模块可以被同化为log4j的显着改进版本。logback-classic本身实现了SLF4J API,因此您可以在logback和其他日志框架(如log4j或java.util.logging(JUL))之间来回切
- logback-access模块与Servlet容器(如Tomcat和Jetty)集成,以提供HTTP访问日志功能可以在logback-core之上轻松构建自己的模块
核心对象
- Logger:日志的记录器,把它关联到应用的对应的context上后,主要用于存放日志对象,也可以定义日志类型、级别。Logger对象一般多定义为静态常量.
- Appender:用于指定日志输出的目的地,目的地可以是控制台、文件、远程套接字服务器、 MySQL、 PostreSQL、Oracle和其他数据库、邮箱、 JMS和远程UNIX Syslog守护进程等。
- Layout:负责把事件转换成字符串,格式化的日志信息的输
pom
springBoot
默认引入了 logback框架
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
传统
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.3.0-alpha4</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.3.0-alpha4</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>1.3.0-alpha4</version>
</dependency>
配置
模板
<?xml version="1.0" encoding="UTF-8"?>
<configuration xmlns="http://ch.qos.logback/xml/ns/logback"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback
https://raw.githubusercontent.com/enricopulatzo/logback-XSD/master/src/main/xsd/logback.xsd">
<property name="SERVICE_NAME" value="metadataServer"/>
<property name="BASE_LOG_PATH" value="logs" />
<property name="pattern" value="%d{HH:mm:ss.SSS} [%t] %-3le %c{20} - [%M,%L] - %m%n %ex{full}" />
<property name="pattern-color" value="[%X{REQUEST_ID}] %yellow(%d{HH:mm:ss.SSS}) [%t] %highlight(%-2le) %green(%c{50}) - [%M,%L] - %highlight(%m) %n %ex{full}"/>
<property name="CHARSET" value="utf-8"/>
<property name="MAX_FILE_SIZE" value="200Mb"/>
<property name="MAX_HISTORY" value="90"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${pattern-color}}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>${pattern}</pattern>
<charset>${CHARSET}</charset>
</encoder>
<file>${BASE_LOG_PATH}/${SERVICE_NAME}Info/${SERVICE_NAME}-info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${BASE_LOG_PATH}/${SERVICE_NAME}Info/${SERVICE_NAME}-info-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<maxFileSize>200MB</maxFileSize>
<maxHistory>90</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>${pattern}</pattern>
<charset>${CHARSET}</charset>
</encoder>
<file>${BASE_LOG_PATH}/${SERVICE_NAME}Error/${SERVICE_NAME}-error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${BASE_LOG_PATH}/${SERVICE_NAME}Error/${SERVICE_NAME}-error-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<maxFileSize>200MB</maxFileSize>
<maxHistory>90</maxHistory>
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- additivity =
这里配置不会经过root-->
<logger name="com.du.controller" level="info" additivity="false">
<appender-ref ref="console"/>
<appender-ref ref="info"/>
<appender-ref ref="error"/>
</logger>
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="info"/>
<appender-ref ref="error"/>
</root>
</configuration>
文件位置
logback-spring.xml
logback-spring.xml:日志框架就不直接加载日志的配置项,由SpringBoot解析日志配置,可以使用SpringBoot的高级Profile功能
<springProfile name="staging">
<!-- configuration to be enabled when the "staging" profile is active -->
可以指定某段配置只在某个环境下生效
</springProfile>
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<springProfile name="dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ----> [%thread] ---> %-5level %logger{50} - %msg%n</pattern>
</springProfile>
<springProfile name="!dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n</pattern>
</springProfile>
</layout>
</appender>
如果使用logback.xml作为日志配置文件,还要使用profile功能,会有以下错误
no applicable action for [springProfile]
<configuration>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!--其他配置省略-->
</configuration>
- scan: 当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
- scanPeriod: 设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟
- debug: 当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false
property 定义公共的属性
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="SERVICE_NAME" value="metadataServer"/>
<property name="BASE_LOG_PATH" value="logs" />
<property name="pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<property name="pattern-color" value="%yellow(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %green(%logger{50}) - [%method,%line] - %highlight(%msg) %n"/>
<property name="CHARSET" value="utf-8"/>
<property name="MAX_FILE_SIZE" value="10Mb"/>
<property name="MAX_HISTORY" value="1"/>
</configuration>
Appender
负责写日志的组件,它有两个必要属性name和class。name指定appender名称,class指定appender的全限定名
ConsoleAppender
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="pattern-color" value="%yellow(%d{yyyy-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %green(%logger{50}) - [%method,%line] - %highlight(%msg) %n"/>
<property name="CHARSET" value="utf-8"/>
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${pattern-color}</pattern>
<charset>${CHARSET}</charset>
</encoder>
</appender>
<!--系统操作日志-->
<root level="debug">
<appender-ref ref="console" />
</root>
</configuration>
FileAppender
- <file>:被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值
- <append>:如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true
- <prudent>:如果是 true,日志会被安全的写入文件,即使其他的FileAppender也在向此文件做写入操作,效率低,默认是 false
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>testFile.log</file>
<append>true</append>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>
RollingFileAppender
滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件
- <file>:被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值
- <append>:如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true
- <rollingPolicy>:当发生滚动时,决定RollingFileAppender的行为,涉及文件移动和重命名。属性class定义具体的滚动策略类
SizeAndTimeBasedRollingPolicy
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${BASE_LOG_PATH}/${SERVICE_NAME}Info/${SERVICE_NAME}-info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${BASE_LOG_PATH}/${SERVICE_NAME}Info/${SERVICE_NAME}-info.log.%d{yyyy-MM-dd}.log.%i.gz</fileNamePattern>
<maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
<maxHistory>${MAX_HISTORY}</maxHistory>
<totalSizeCap>100MB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${pattern}</pattern>
<charset>${CHARSET}</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
ch.qos.logback.core.rolling.TimeBasedRollingPolicy
fileNamePattern:日志归档文件名
maxHistory:日志存活时间,大于这个时间的日志都将会删除 单位为天
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${BASE_LOG_PATH}/${SERVICE_NAME}Info/${SERVICE_NAME}-info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${BASE_LOG_PATH}/${SERVICE_NAME}Info/${SERVICE_NAME}-info.log.%d{yyyy-MM-dd}.log.%i.gz</fileNamePattern>
<maxHistory>${MAX_HISTORY}</maxHistory>
<totalSizeCap>100MB</totalSizeCap>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>${pattern}</pattern>
<charset>${CHARSET}</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
</configuration>
logger
这里配置不会经过root-->
<logger name="com.du.controller" level="info" additivity="false">
<appender-ref ref="console"/>
<appender-ref ref="info"/>
<appender-ref ref="error"/>
</logger>
Encoder
对日志进行格式化
<encoder>
<pattern>${pattern-color}</pattern>
<charset>${CHARSET}</charset>
</encoder>
root
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="info"/>
<appender-ref ref="error"/>
</root>
AsyncAppender
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>myapp.log</file>
<encoder>
<pattern>%logger{35} -%kvp -%msg%n</pattern>
</encoder>
</appender>
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE" />
</appender>
<root level="DEBUG">
<appender-ref ref="ASYNC" />
</root>
</configuration>
- 记录异步日志撑爆内存;
- 记录异步日志出现日志丢失;
- 记录异步日志出现阻塞。
默认队列大小为 256,达到 80% 容量后开始丢弃 <=INFO 级别的日志后
queueSize 设置得特别大,就可能会导致 OOM。
queueSize 设置得比较小(默认值就非常小),且 discardingThreshold 设置为大于 0 的值(或者为默认值),队列剩余容量少于 discardingThreshold 的配置就会丢弃 <=INFO 的日志。这里的坑点有两个。一是,因为 discardingThreshold 的存在,设置queueSize 时容易踩坑。比如,本例中最大日志并发是 1000,即便设置 queueSize 为 1000 同样会导致日志丢失。二是,discardingThreshold 参数容易有歧义,它不是百分比,而是日志条数。对于总容量 10000 的队列,如果希望队列剩余容量少于 1000 条的
时候丢弃,需要配置为 1000。
neverBlock 默认为 false,意味着总可能会出现阻塞。如果 discardingThreshold 为0,那么队列满时再有日志写入就会阻塞;如果 discardingThreshold 不为 0,也只会丢弃 <=INFO 级别的日志,那么出现大量错误日志时,还是会阻塞程序。
可以看出 queueSize、discardingThreshold 和 neverBlock 这三个参数息息相关,务必按需进行设置和取舍,到底是性能为先,还是数据不丢为先:
如果考虑绝对性能为先,那就设置 neverBlock 为 true,永不阻塞。
如果考虑绝对不丢数据为先,那就设置 discardingThreshold 为 0,即使是 <=INFO 的级别日志也不会丢,但最好把 queueSize 设置大一点,毕竟默认的 queueSize 显然太小,太容易阻塞。
如果希望兼顾两者,可以丢弃不重要的日志,把 queueSize 设置大一点,再设置一个合理的 discardingThreshold。
MDC
Mapped Diagnostic Context,可以粗略的理解成是一个线程安全的存放诊断日志的容器
接口
- put
- get
- remove
- clear
用途
- 在WEB应用中,如果想在日志中输出请求用户IP地址、请求URL、统计耗时等等
- MDC基本都能支撑可以在MDC中填充REQUEST ID追踪单个请求的执行轨迹
- 微服务场景下,使用MDC埋点,做到链路跟踪
- 最好是有日志收集工具、将多实例、多系统的日志实现收集,再根据MDC埋点,进行grep,就可以打印一个完整的微服务请求链路
微服务日志链路
分布式日志链路
- 搞清楚当前工程的对于普通的web应用来说,给每个请求添加一条标识符(例如UUID),可以快速的grep到某一个请求的完整链路
- 对于分布式应用来说,一个链路会经过多个系统,此时也可以利用MDC实现多个系统日志的“拼接”
MDC-使用
启用
日志配置文件
[%X{REQUEST_ID}]
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="UTF-8">
<pattern>[%X{REQUEST_ID}] %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} %L - %msg%n</pattern>
</encoder>
</appender>
拦截器
public class TraceIdInterceptor implements HandlerInterceptor {
private static final String FLAG = "REQUEST_ID";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
// 清理之前的请求 id
MDC.clear();
String traceId = request.getHeader(FLAG);
if (StrUtil.isEmpty(traceId)) {
if (null == MDC.get(FLAG)) {
MDC.put(FLAG, IdUtil.fastSimpleUUID());
}
} else {
MDC.put(FLAG, traceId);
}
return true;
}
}
@Configuration
public class MyInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TraceIdInterceptor()).addPathPatterns("/**").order(0);
}
}
restTemplate配置
@Configuration
public class MyRestTemplateConfig {
/**
* 请求链路的id
*/
private static final String FLAG = "REQUEST_ID";
/**
* 远程调用,添加请求id,形成请求链路可追溯
* @return RestTemplate
*/
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> list = new ArrayList<>();
list.add(((request, body, execution) -> {
String traceId = MDC.get(FLAG);
if (StrUtil.isNotEmpty(traceId)) {
request.getHeaders().add(FLAG, traceId);
}
return execution.execute(request, body);
}));
restTemplate.setInterceptors(list);
return restTemplate;
}
}