因为SLF4J就是门面设计模式的应用,因此先整理下这种设计模式。参考原文:https://blog.csdn.net/jason0539/article/details/22775311
部分截图:
核心就是外部(客户端)与一个子系统通信时,通过一个统一的外观对象进行,从而隐藏子系统的具体逻辑,使得子系统更易于使用。客户端不再需要了解子系统内部的实现,也不需要跟众多子系统内部的模块进行交互,只需要跟门面类交互就可以了。
SLF4J,Simple Logging Facade For Java,即简单日志门面,是一套存取日志的标准
接口
。
注意是接口,不是具体实现,就像JDBC一样只是统一的接口,想要使用就需要搭配其他具体的日志实现方案,如logback、log4j
slf4j-simple、logback都是slf4j的具体实现,log4j并不直接实现slf4j,但是有专门的一层桥接slf4j-log4j12来实现slf4j。关于这些具体实现框架的信息,贴个图:
关于SLF4J的使用目的,看这个场景:
- 自己开发的系统中,日志框架使用logback实现
- 这个系统中使用了A.jar,A.jar中使用了log4j框架
- 这个系统也使用了B.jar,B.jar中使用了slf4j-simple
此时,我们就得维护多套日志框架,使用不便。就像这样:
// 使用log4j,需要log4j.jar
import org.apache.log4j.Logger;
Logger logger_log4j = Logger.getLogger(Test.class);
logger_log4j.info("Hello World!");
// 使用log4j2,需要log4j-api.jar、log4j-core.jar
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
Logger logger_log4j2 = LogManager.getLogger(Test.class);
logger_log4j2.info("Hello World!");
// logback,需要logback-classic.jar、logback-core.jar
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
Logger logger_logback = new LoggerContext().getLogger(Test.class);
logger_logback.info("Hello World!");
使用不同的日志框架,就要引入不同的jar包,使用不同的代码获取Logger。如果项目升级需要更换不同的框架,那么就需要修改所有的地方来获取新的Logger,这将会产生巨大的工作量。 因此,加入门面的设计模式,让调用端只关心如何调用,而不关心怎么实现打印日志,这个抽象层就是slf4j。slf4j只是一个日志标准,并不是日志系统的具体实现,它只负责:
先看下只有slf4j依赖的情况,创建个小模块,仅引入单元测试与slf4j的依赖,不引入任何日志的具体实现的依赖。
pom.xml内容:
在单元测试中打印行日志看下效果:
@Test
public void testSlf4j(){
Logger logger = LoggerFactory.getLogger(Object.class);
logger.info("log...");
}
运行单元测试:
在没有给slf4j提供具体的实现时,打印日志失败,大致意思是找不到绑定器和实现。接下来随便再引入一个日志实现框架的依赖:
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classic<artifactId>
<version>1.2.3<version>
dependency>
再运行单元测试,日志打印成功:
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-simple<artifactId>
<version>1.7.25<version>
dependency>
再运行上面的UT,发现控制台显示有多个SLF4J绑定器,最终生效的是logback下的:
默认情况下,SpringBoot使用了 SLF4J+logback 的日志框架组合。查看SpringBoot起步依赖:
往下跟logging-starter:
<dependencies>
<dependency>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
<version>1.2.3version>
<scope>compile
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-to-slf4jartifactId>
<version>2.11.1version>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>jul-to-slf4jartifactId>
<version>1.7.25version>
<scope>compilescope>
dependency>
dependencies>
Maven依赖图:
切换日志框架,即切换实现,就是排除默认的依赖,引入自己需要的框架的依赖。
logback切换为log4j2:
<dependencies>
<dependency>
<!‐‐starter‐web里面自动添加starter‐logging ,向下就是logback的依赖‐‐>
<groupId>org.springframework.bootgroupId>
<artifactId>spring‐boot‐starter‐webartifactId>
<exclusions>
<!‐‐排除starter‐logging 也就是logback的依赖‐‐>
<exclusion>
<artifactId>spring‐boot‐starter‐loggingartifactId>
<groupId>org.springframework.bootgroupId>
exclusion>
exclusions>
dependency>
<!‐‐Log4j2的场景启动器‐‐>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring‐boot‐starter‐log4j2artifactId>
dependency>
dependencies>
再添加log4j2的配置文件log4j2-spring.xml即可。
logback切换为log4j:
要将logback的桥接器排除,添加log4j的桥接器:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<artifactId>logback-classicartifactId>
<groupId>ch.qos.logbackgroupId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
dependency>
再添加log4j的配置文件。
关于桥接:
logback是log4j的作者写的另一日志框架,对之前的框架进行了完善和优化。关于logback的加载,当我们使用logback-classic.jar时,应用启动,那么logback会按照如下顺序进行扫描:
logback.xml
以上任何一项找到了,就不进行后续扫描,按照对应的配置进行logback的初始化,具体代码实现可见ch.qos.logback.classic.util.ContextInitializer类的findURLOfDefaultConfigurationFile方法。
当所有以上四项都找不到的情况下,logback会调用ch.qos.logback.classic.BasicConfigurator的configure方法,构造一个ConsoleAppender用于向控制台输出日志,默认日志输出格式为:
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
整个配置拆为三大块:Appender、Logger、Pattern。先看下一个logback的
的三个属性。
的三个属性:
用来设置某一个包或者具体某一个类的日志打印级别(分组的那个味儿)、以及指定所用的
。
包含一个元素和三个属性:
元素:0个或多个,用来说明这个logger分组下的日志往哪儿输出、怎么输出name不是随便起的,它表示的是LoggerFactory.getLogger(XXX.class),XXX类的包路径,包路径越少越是父级,传入的xxx.class用哪个logger,就是看这个类属于哪个name包下
也是元素,但是它是根logger,只有一个level属性,因为它的name就是ROOT
写个UT来测试下效果:
public class Slf4jTest {
@Test
public void testSlf4j() {
Logger logger = LoggerFactory.getLogger(Object.class);
logger.trace("=====trace=====");
logger.debug("=====debug=====");
logger.info("=====info=====");
logger.warn("=====warn=====");
logger.error("=====error=====");
}
}
类路径下写logback.xml:
<configuration scan="false" scanPeriod="60000" debug="false">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%npattern>
layout>
appender>
<root level="info">
<appender-ref ref="STDOUT" />
root>
configuration>
以上这个配置,即root这个logger把日志级别大于info的信息交给STDOUT这个appender处理(STDOUT将信息打印到控制台),此时运行上面的UT:
再调整下上面的logback.xml,新加一个logger:
<logger name="java" additivity="false" />
此时运行UT,无输出结果:
传递再调整下上面的logback.xml,再新加一个logger:
<logger name="java" additivity="false" />
<logger name="java.lang" level="warn">
<appender-ref ref="STDOUT" />
logger>
运行UT:
最后,注意这种导致重复打印的不合理写法:
是负责写日志的组件。
有两个必要属性name和class:
常用的appender有:
ConsoleAppender用来将日志输出到控制台:
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%npattern>
encoder>
appender>
FileAppender用于将日志写到文件中:
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>D:/server.logfile>
<append>trueappend>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%npattern>
encoder>
appender>
其中:
RollingFileAppender的作用是滚动记录文件,先将日志记录到指定文件,当符合某个条件(如达到每个文件的大小)时再将日志记录到其他文件:
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/data-svc-error.logfile>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/data-svc-error.%d{yyyy-MM-dd}.logfileNamePattern>
<maxHistory>7maxHistory>
rollingPolicy>
<encoder>
<pattern>${log.pattern}pattern>
encoder>
appender>
其中:
AsyncAppender采用异步写日志的方式,减少性能损耗: 之前的appender,如RollingFileAppender,每写一次日志就发生一次磁盘IO
<appender name ="ASYNC" class= "ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0discardingThreshold>
<queueSize>256queueSize>
<appender-ref ref ="myRollingFileAppender"/>
appender>
异步写入实现原理:
当我们配置了AsyncAppender,系统启动时会初始化一条名为"AsyncAppender-Worker-ASYNC
"的线程
当Logging Event进入AsyncAppender后,AsyncAppender会调用appender方法,appender方法中再将event填入Buffer(使用的Buffer为BlockingQueue,具体实现为ArrayBlockingQueye)
不过填入之前,会先判断当前Buffer的容量以及丢弃日志特性是否开启,当消费能力不如生产能力时,AsyncAppender会将超出Buffer容量的Logging Event的级别进行丢弃,作为消费速度一旦跟不上生产速度导致Buffer溢出处理的一种方式。
上面的线程的作用,就是从Buffer中取出Event,交给对应的appender进行后面的日志推送
从上面的描述可以看出,AsyncAppender并不处理日志,只是将日志缓冲到一个BlockingQueue里面去,并在内部创建一个工作线程从队列头部获取日志,之后将获取的日志循环记录到附加的其他appender上去,从而达到不阻塞主线程的效果。
因此AsyncAppender仅仅充当的是事件转发器,必须引用另外一个appender来做事
。(如上面引用了定义的RollingAppender类型的appender)
基于这个原理,看这个appender的参数:
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%npattern>
layout>
appender>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%npattern>
encoder>
appender>
encoder表示对参数进行格式化,上面第一种使用了
是
的一个子节点,表示在当前给到的日志级别下再进行一次过滤,最基本的Filter有:
LevelFilter这种过滤器:
<configuration scan="false" scanPeriod="60000" debug="false">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%npattern>
encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARNlevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
appender>
<logger name="java" additivity="false" />
<logger name="java.lang" level="DEBUG">
<appender-ref ref="STDOUT" />
logger>
<root level="INFO">
<appender-ref ref="STDOUT" />
root>
configuration>
以上即,过滤WARN级别的日志,匹配时接收这个记录,不匹配时拒绝这个记录,因此最后的输出只有WARN日志:
注意,有了过滤器之后,这里最终输出的信息,是logger和Filter交集的关系,不是logger配info就输出info及以上了。再看ThresholdFilter过滤器,Threshold即门槛:
<configuration scan="false" scanPeriod="60000" debug="false">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%npattern>
encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFOlevel>
filter>
appender>
<logger name="java" additivity="false" />
<logger name="java.lang" level="DEBUG">
<appender-ref ref="STDOUT" />
logger>
<root level="INFO">
<appender-ref ref="STDOUT" />
root>
configuration>
此时,门槛设置为INFO,则小于INFO级别的被过滤掉了,尽管logger中设置了DEBUG,最终输出的结果也没有DEBUG:
logback.xml文件各项配置的含义备份:
<configuration scan="false" scanPeriod="60 seconds" debug="false">
<property name="LOG_HOME" value="app/log"/>
<property name="appName" value="logbackBootText"/>
<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%npattern>
springProfile>
<springProfile name="!dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%npattern>
springProfile>
layout>
appender>
<appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.logfileNamePattern>
<MaxHistory>365MaxHistory>
<totalSizeCap>20GBtotalSizeCap>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MBmaxFileSize>
timeBasedFileNamingAndTriggeringPolicy>
rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] ->> [ %-5level ] [ %logger{50} : %line ] ->> %msg%n
pattern>
layout>
appender>
<appender name="appLogAppenderBoot" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/boot-${appName}-%d{yyyy-MM-dd}-%i.logfileNamePattern>
<MaxHistory>365MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MBmaxFileSize>
timeBasedFileNamingAndTriggeringPolicy>
rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] -> [ %-5level ] [ %logger{50} : %line ] -> %msg%npattern>
layout>
appender>
<root level="DEBUG">
<appender-ref ref="stdout"/>
<appender-ref ref="appLogAppender"/>
<appender-ref ref="appLogAppenderBoot"/>
root>
<logger name="org.springframework.boot" level="debug" additivity="false">
<appender-ref ref="appLogAppenderBoot"/>
logger>
<logger name="com.tuniu" level="debug" additivity="false">
<appender-ref ref="appLogAppender"/>
<appender-ref ref="stdout"/>
logger>
configuration>
贴个精简版的,目前工作中服务正在用的:
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<property name="log.path" value="/logs"/>
<property name="log.pattern"
value="%d{HH:mm:ss.SSS} [%thread] %-5level [%X{X-B3-TraceId:-},%X{X-B3-SpanId:-}] %logger{20} - [%method,%line] - %msg%n"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}pattern>
encoder>
appender>
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/data-service-ent-svc-info.logfile>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/data-service-xxx-svc-info.%d{yyyy-MM-dd}.logfileNamePattern>
<maxHistory>7maxHistory>
rollingPolicy>
<encoder>
<pattern>${log.pattern}pattern>
encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFOlevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
appender>
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/data-service-xxx-svc-error.logfile>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/data-service-ent-svc-error.%d{yyyy-MM-dd}.logfileNamePattern>
<maxHistory>7maxHistory>
rollingPolicy>
<encoder>
<pattern>${log.pattern}pattern>
encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERRORlevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
appender>
<logger name="com.plat" level="info"/>
<logger name="org.springframework" level="warn"/>
<root level="info">
<appender-ref ref="console"/>
root>
<root level="info">
<appender-ref ref="file_info"/>
<appender-ref ref="file_error"/>
root>
configuration>
上面日志存放路径是/logs,而deploy部署文件则是将这个路径持久化到hostpath中,以便后续排查问题:
apiVersion: apps/v1
kind: Deployment
metadata:
name: xxx-svc-deployment
namespace: mynamespace
spec:
selector:
matchLabels:
app: xxx-svc-deployment
template:
metadata:
labels:
app: xxx-svc-deployment
spec:
dnsPolicy: ClusterFirst
containers:
- name: xxx-svc-deployment
image: repoistory.xxx-svc-deployment:release-2.0
ports:
- containerPort: 9527
volumeMounts:
- mountPath: /logs
name: go-logs # 持久化到宿主机的hostpath
volumes:
- name: go-logs
hostPath:
path: /data/logs
参考文章: