Logback使用MDC动态变量根据业务、接口、任务生成相应日志文件
前言
近日,项目上遇到个关于日志生成的特定要求:最初要求日志按日志级别进行生成相应文件,后来要求按业务、接口、服务代码生成对应的日志文件。
对于后者一下有点懵,从来没有这么玩过,因为需要动态从请求接口中获取服务代码,生成该服务代码对应的日志文件。
从网上查询了相关资料,都说使用
org.slf4j.MDC
变量,但是根据配置后,发现压根儿就不生效、不成功!于是采用了自定义Logger+ThreadLocal存储方案实现了该需求,但是总感觉代码不够优雅、不够灵活。
直到再次提了个需要:因为并发情况下日志混乱问题,要求每行日志中有相关接服务代码,方便区分、排查。这时候虽然目前方案可行,但是再次起了使用动态变量来实现的心思,因此查阅大量资料,终于成功实现,于是特此记录。
按照日志级别生成日志文件
创建一个Logback日志框架的配置文件,这个配置可以让应用程序的日志输出到控制台和按天生成的日志文件中,并根据日志级别分别记录到不同的文件中。这样可以更好地管理和分析应用程序的日志信息。
主要包含以下功能:
1.定义日志文件的存储地址LOG_PATH
2.配置控制台输出CONSOLE,输出格式为%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
3.配置按天生成日志文件APP,文件名格式为 ${LOG_PATH}/%d{yyyy-MM-dd}/app.%i.log,最大文件大小为 100MB,保存30天
4.配置按日志级别分别记录到不同文件:
ERROR_FILE: 记录错误级别的日志
WARN_FILE: 记录警告级别的日志
INFO_FILE: 记录信息级别的日志
DEBUG_FILE: 记录调试级别的日志
5.配置根Logger的日志级别为 INFO,并引用上述的所有Appender
配置logback.xml
创建日志配置文件:logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 定义日志文件的存储地址 -->
<property name="LOG_PATH" value="./logs"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按天生成日志文件 -->
<appender name="APP" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志名称 -->
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/app.%i.log</fileNamePattern>
<!-- 日志保存天数 -->
<maxHistory>30</maxHistory>
<!-- 日志文件大小 -->
<maxFileSize>100MB</maxFileSize>
</rollingPolicy>
<encoder>
<!-- 日志输出格式 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按日志级别分别记录到不同文件 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志名称 -->
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/error.%i.log</fileNamePattern>
<!-- 日志保存天数 -->
<maxHistory>30</maxHistory>
<!-- 日志文件大小 -->
<maxFileSize>100MB</maxFileSize>
</rollingPolicy>
<encoder>
<!-- 日志输出格式 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<!-- 输出错误日志 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/warn.%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<maxFileSize>100MB</maxFileSize>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>WARN</level>
</filter>
</appender>
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/info.%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<maxFileSize>100MB</maxFileSize>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/debug.%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<maxFileSize>100MB</maxFileSize>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
</appender>
<!-- 根Logger配置 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="APP"/>
<appender-ref ref="ERROR_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="DEBUG_FILE"/>
</root>
</configuration>
配置application.yaml
配置项目使用指定的日志配置文件
logging:
config: classpath:logback.xml
输出
启动项目进行测试,如期生成相关日志文件
使用MDC动态变量
使用MDC(Mapped Diagnostic Context) 可以为每个线程关联一些上下文信息,在日志输出时可以包含这些信息,从而区分不同线程的日志输出。
设置MDC变量
只需要通过MDC.put()
进行变量设置,然后就可以将该变量传递给Logback日志框架,当不需要的时候再通过MDC.remove()
进行删除
import org.slf4j.MDC;
@RequestMapping(value = "/test")
public String index() {
MDC.put("requestId", System.currentTimeMillis()+"");
log.info("---------------------- hello ----------------------");
// 业务逻辑
MDC.remove("requestId");
return "OK";
}
使用MDC变量
在logback.xml中,通过
%X{requestId}
形式的配置即可动态获取设置的MDC变量。
在日志输出格式中使用动态变量
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] -%X{requestId}- %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
输出
使用压测工具进行大批量的发送请求测试,可如期得到预计结果。
自定义Logger
通过自定义Logger对象,在相关业务、接口中使用该日志对象,即可实现按业务、接口生成对应日志文件。但是此种方式需要定义大量相关Logger对象,非常不够优雅。
配置logback.xml
配置定义了一个名为 “ONEAPI” 的Appender,再将称为 "ONEAPI"的 Appender 与称为 "apiLog"的 Logger关联起来,将日志输出到 “ONEAPI” 关联的Appender定义文件中。
在特定的模块、包、类中使用这个指定的自定义Logger
<!-- 自定义某个接口日志文件 -->
<appender name="ONEAPI" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志名称 -->
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/apiLog.%i.log</fileNamePattern>
<!-- 日志保存天数 -->
<maxHistory>30</maxHistory>
<!-- 日志文件大小 -->
<maxFileSize>100MB</maxFileSize>
</rollingPolicy>
<encoder>
<!-- 日志输出格式 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="apiLog" leve="info" additivity="false">
<appender-ref ref="ONEAPI"/>
</logger>
也可以直接指定包、类使用的Logger,在包中、类中使用Logger将自动关联这个自定义Logger
<logger name="com.xxx.face.web.HelloControler" leve="info" additivity="false">
<appender-ref ref="ONEAPI"/>
</logger>
使用指定Logger
@RequestMapping(value = "/test")
public String index() {
Logger apiLog = LoggerFactory.getLogger("apiLog");
apiLog.info("---------------------- apiLog ----------------------");
return "OK";
}
输出
经过测试也会如期得到期望的结果。
根据业务、接口划分日志文件
基于自定义Logger+TreadLocal方案实现后,由于需求、代码不优雅等原因,还是想找其他方法,在查询大量相关资料后,终于使用上述的MDC动态变量相关东西实现根据业务、接口划分日志文件。
SiftingAppender可用于根据给定的运行时属性来分离(或筛选)日志记录。例如, SiftingAppender 可以根据用户会话分离日志事件,以便不同用户生成的日志进入不同的日志文件,每个用户一个日志文件。
参阅logback官网:Appender
配置logback.xml
配置分析:
当requestId这个MDC变量不存在时,就会使用other作为默认值。这可以确保每个请求都有独立的日志文件。
在appender的文件名中使用 ${requestId},这样可以根据requestId这个MDC变量的值,动态生成对应的日志文件名。
在rollingPolicy中配置按日期滚动,可以实现按天滚动日志文件,避免单个日志文件过大。
在encoder的pattern中使用%X{requestId},可以在日志输出中包含 requestId 这个 MDC 变量的值,方便追踪和定位问题。
<!-- 使用 MDC 的 appender -->
<appender name="FILE_CUSTOM" class="ch.qos.logback.classic.sift.SiftingAppender">
<!-- discriminator称为鉴别器,设置运行时动态属性,siftingAppender根据这个属性来输出日志到不同文件 -->
<discriminator>
<key>requestId</key>
<defaultValue>other</defaultValue>
</discriminator>
<sift>
<!-- 标准的文件输出Appender, 文件名根据MDC动态生成 -->
<appender name="FILE-%{requestId}" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 滚动策略:根据时间来制定滚动策略.既负责滚动也负责出发滚动 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志输出位置 可相对和绝对路径 -->
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/${requestId}.log</fileNamePattern>
</rollingPolicy>
<encoder charset="UTF-8">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] -%X{requestId}- %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
</sift>
</appender>
设置MDC变量
通过MDC.put()
进行变量设置,然后就可以将该变量传递给Logback日志框架,当不需要的时候再通过MDC.remove()
进行删除
@RequestMapping(value = "/test")
public String index() {
MDC.put("requestId", System.currentTimeMillis()+"");
log.info("---------------------- HelloControler ----------------------");
MDC.remove("requestId");
return "OK";
}
输出
同样经过测试得出预期结果。
按日期滚动、按大小分割
这里模拟测试一个称为000001
服务接口,指定文件大小1MB,进行循环请求生成日志
<appender name="FILE_CUSTOM" class="ch.qos.logback.classic.sift.SiftingAppender">
<discriminator>
<key>requestId</key>
<defaultValue>other</defaultValue>
</discriminator>
<sift>
<appender name="FILE-%{requestId}" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/${requestId}-%i.log</fileNamePattern>
<maxFileSize>1MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder charset="UTF-8">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] -%X{requestId}- %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
</sift>
</appender>
同样如期得出正确结果:
同时使用多个MDC变量
同时设置多个MDC变量
@RequestMapping(value = "/test")
public String index() {
MDC.put("requestId", System.currentTimeMillis()+"");
MDC.put("IpAddr", "6.6.6.6");
log.info("---------------------- HelloControler ----------------------");
MDC.remove("requestId");
MDC.remove("IpAddr");
return "OK";
}
在控制台日志输出中与指定生成日志文件中使用自定义的多个MDC变量
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] -%X{IpAddr} -%X{requestId}- %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE_CUSTOM" class="ch.qos.logback.classic.sift.SiftingAppender">
<discriminator>
<key>requestId</key>
<defaultValue>other</defaultValue>
</discriminator>
<sift>
<appender name="FILE_MDC" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/${requestId}-%i.log</fileNamePattern>
<maxFileSize>1MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder charset="UTF-8">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] -%X{IpAddr} -%X{requestId} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
</sift>
</appender>
查看控制台输出:
查看日志文件输出:
自定义Discriminator
MDCBasedDiscriminator类
在SiftingAppender实例中,有个discriminator属性,它是设置运行时动态属性,siftingAppender根据这个属性来输出日志到不同文件
<appender name="FILE_CUSTOM" class="ch.qos.logback.classic.sift.SiftingAppender">
<discriminator>
<key>requestId</key>
<defaultValue>other</defaultValue>
</discriminator>
</appender>
在SiftingAppender中,默认使用MDDCBasedDiscriminator鉴别器,它是使用默认的MDCBasedDiscriminator
类
自定义LogThreadLocal
原来的MDCBasedDiscriminator鉴别器是从MDC中获取key,现在自定义一个LogThreadLocal并从中获取key
public class LogThreadLocal {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void set(String value) {
threadLocal.set(value);
}
public static String get() {
return threadLocal.get();
}
public static void remove() {
threadLocal.remove();
}
}
自定义LogDiscriminator
参考鉴别器MDCBasedDiscriminator类
实现类似功能的自定义鉴别器LogDiscriminator
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.sift.AbstractDiscriminator;
import org.apache.commons.lang3.StringUtils;
public class LogDiscriminator extends AbstractDiscriminator<ILoggingEvent> {
private static String DEFAULT_KEY = "default_key";
private static String DEFAULT_VALUE = "default_value";
@Override
public String getKey() {
return DEFAULT_KEY;
}
@Override
public String getDiscriminatingValue(ILoggingEvent iLoggingEvent) {
String value = LogThreadLocal.get();
if (StringUtils.isNotBlank(value)) {
return value;
} else {
return DEFAULT_VALUE;
}
}
}
修改logback.xml
这里就不再使用默认的MDCBasedDiscriminator,而是使用自定义的LogDiscriminator。
注意:这里使用的default_key名需与LogDiscriminator的默认key名保持一致
<appender name="FILE_CUSTOM" class="ch.qos.logback.classic.sift.SiftingAppender">
<!-- 基于上下文获取key -->
<discriminator class = "com.xxx.xxx.config.LogDiscriminator"/>
<sift>
<appender name="FILE-%{default_key}" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/%d{yyyy-MM-dd}/${default_key}-%i.log</fileNamePattern>
<maxFileSize>1MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>3GB</totalSizeCap>
</rollingPolicy>
<encoder charset="UTF-8">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] -%X{default_key}- %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
</sift>
</appender>
使用LogThreadLocal
这里模拟设置LogThreadLocal的值
@RequestMapping(value = "/test")
public String index() {
double random = Math.random() * 10;
if (random > 5) {
LogThreadLocal.set("Test_Log1");
log.info("---------------Test_Log1-----------------");
} else {
LogThreadLocal.set("Test_Log2");
log.info("---------------Test_Log2-----------------");
}
LogThreadLocal.remove();
return "OK";
}
输出
启动项目进行测试,如期得到结果