❝笔者印象尤深的就是去年某个同事,收到了客户反馈的紧急bug。尽管申请到了日志文件,但因为很多关键步骤没有打印日志,导致排查进度很慢,数个小时都没能排查到问题,也无法给出解决对策。导致了客户程序一直阻断,最终产生了不少损失。 事后,经过仔细推敲,成功复现了这个bug,其实是一个很不起眼的数据转换导致的。可因为日志内容的匮乏,排查起来难度很大。其实只要在数据转换前后进行日志输出,这个问题就是一眼的事。但可惜没如果,故事的最后,开发部门还是遭到了客户的投诉,影响到了部门绩效❞。
<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.32</version> </dependency>然后在我们要打印日志的类里加上一行 ;private static final Logger logger = LoggerFactory.getLogger(XXXX.class); 即可使用,如下:
// 堆代码 duidaima.com import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyClass { private static final Logger log = LoggerFactory.getLogger(MyClass.class); //... public static void main(String[] args) { log.info("This is an info message."); } }如果我们引用了lombok的话,也可以使用lombok的注解@Slf4j 代替上面那句话来使用 SLF4J ,如下:
import lombok.extern.slf4j.Slf4j; @Slf4j public class MyClass { public static void main(String[] args) { log.info("This is an info message."); } }但是,我们都知道SLF4J仅仅是个门面,换句话说,仅有接口而没有实现,如果此刻我们直接运行,打印日志是没有用处的。
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.12</version> </dependency>Logback 的核心模块为 logback-classic,它提供了一个 SLF4J 的实现,兼容 Log4j API,可以无缝地替换 Log4j。它自身已经包含了 logback-core 模块,而 logback-core,顾名思义就是 logback 的核心功能,包括日志记录器、Appender、Layout 等。其他 logback 模块都依赖于该模块。
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!--定义日志文件的存储地址,使用Spring的属性文件配置方式--> <springProperty scope="context" name="log.home" source="log.home" defaultValue="logs"/> <!--定义日志文件的路径--> <property name="LOG_PATH" value="${log.home}"/> <!--定义控制台输出--> <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%-5relative [%thread] %-5level %logger{35} - %msg%n</pattern> </encoder> </appender> <!--定义 INFO 及以上级别信息输出到控制台--> <root level="INFO"> <appender-ref ref="console"/> </root> <!--定义所有组件的日志级别,如所有 DEBUG--> <logger name="com.example" level="DEBUG"/> <!-- date 格式定义 --> <property name="LOG_DATEFORMAT" value="yyyy-MM-dd"/> <!-- 定义日志归档文件名称格式,每天生成一个日志文件 --> <property name="ARCHIVE_PATTERN" value="${LOG_PATH}/%d{${LOG_DATEFORMAT}}/app-%d{${LOG_DATEFORMAT}}-%i.log.gz"/> <!--定义文件输出,会根据定义的阈值进行切割,支持自动归档压缩过期日志--> <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <!--定义日志文件切割的阈值,本例是 50MB--> <maxFileSize>50MB</maxFileSize> <!--定义日志文件保留时间,本例是每天生成一个日志文件--> <fileNamePattern>${ARCHIVE_PATTERN}</fileNamePattern> <maxHistory>30</maxHistory> <!-- zip 压缩生成的归档文件 --> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>50MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> <!-- 删除过期文件 --> <cleanHistoryOnStart>true</cleanHistoryOnStart> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{35} - %msg%n</pattern> </encoder> </appender> <!--定义 ERROR 级别以上信息输出到文件--> <logger name="com.example.demo" level="ERROR" additivity="false"> <appender-ref ref="file"/> </logger> <!--异步输出日志信息--> <appender name="asyncFile" class="ch.qos.logback.classic.AsyncAppender"> <discardingThreshold>0</discardingThreshold> <queueSize>256</queueSize> <appender-ref ref="file"/> </appender> <!--定义INFO及以上级别信息异步输出到文件--> <logger name="com.example" level="INFO" additivity="false"> <appender-ref ref="asyncFile"/> </logger> </configuration>其中,主要包括以下配置:
queueSize 定义了异步输出队列的大小,当队列满时,会等待队列中的数据被消费后再将数据放入队列中,此处设置为 256。
public class Main { private static final Logger log = LoggerFactory.getLogger(Main.class); public static void main(String[] args) { log.trace("This is a Main trace message."); log.debug("This is a Main debug message."); log.info("This is a Main info message."); log.warn("This is a Main warn message."); log.error("This is a Main error message."); Slave.main(args); } } public class Slave { private static final Logger log = LoggerFactory.getLogger(Slave.class); public static void main(String[] args) { log.trace("This is a Slave trace message."); log.debug("This is a Slave debug message."); log.info("This is a Slave info message."); log.warn("This is a Slave warn message."); log.error("This is a Slave error message."); } }我们想实现这样的效果,首先日志要同时 输出到控制台 及 日志文件,且不同层级的代码,输出的日志层级也不同。那么我们可以对上述的xml做出一些调整:
<!--定义日志文件的路径--> <property name="LOG_PATH" value="D://logs/log"/>去掉原有的那些root、logger标签,我们自己新建两个logger,用于两个不同的层级。我们想里层输出 debug 级别,外层输出info 级别,我们可以这么设置。并且同时输出到控制台及日志文件。
<logger name="com.zhanfu" level="INFO"> <appender-ref ref="file" /> <appender-ref ref="console"/> </logger> <logger name="com.zhanfu.child" level="DEBUG" additivity="false"> <appender-ref ref="file" /> <appender-ref ref="console"/> </logger>当我们运行Main.main的时候,就可以得到以下日志,slave 能输出debug级别,Main 只能输出 info及以上级别。
4. 细节点
<logger name="com.zhanfu.child" level="DEBUG" additivity="false">使用了一个 additivity="false" 的属性,这其实是因为 logger 这个标签在锁定某个目录时,可能会发生层级关系。比如我们的两个 logger, 一个针对的目录是 com.zhanfu 另一个是 com.zhanfu.child ,后者就会被前者包含。
<logger name="com.zhanfu" level="INFO"> <appender-ref ref="file" /> <appender-ref ref="console"/> </logger> <logger name="com.zhanfu.child" level="DEBUG"> <appender-ref ref="file" /> <appender-ref ref="console"/> </logger> <root level="WARN"> <appender-ref ref="console"/> </root>结果控制台的输出日志,Main会重复两次,Slave 会重复三次,如下
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.13.3</version> </dependency>其内包含 Log4j2 的实现,和 SLF4J 的 API,如下:
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="INFO" monitorInterval="30"> <Properties> <Property name="logPath">logs</Property> </Properties> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{ISO8601} [%t] %-5level %logger{36} - %msg%n" /> </Console> <RollingFile name="File" fileName="${logPath}/example.log" filePattern="${logPath}/example-%d{yyyy-MM-dd}-%i.log"> <PatternLayout pattern="%d{ISO8601} [%t] %-5level %logger{36} - %msg%n"/> <Policies> <SizeBasedTriggeringPolicy size="10 MB"/> </Policies> <DefaultRolloverStrategy max="4"/> </RollingFile> </Appenders> <Loggers> <Logger name="com.zhanfu.child" level="DEBUG"> <AppenderRef ref="File"/> <AppenderRef ref="Console"/> </Logger> <Logger name="com.zhanfu" level="INFO"> <AppenderRef ref="File"/> <AppenderRef ref="Console"/> </Logger> <Root level="WARN"> <AppenderRef ref="Console" /> </Root> </Loggers> </Configuration>Properties
3. 对比
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.12</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.13.3</version> </dependency>可以看到,SLF4J 发现了系统中同时存在两个插件框架,并最终选择了使用 Logback