日志整理
分享思路
- 日志前言
- 日志框架的简单介绍
- 日志级别
- 日志demo整合
- springboot的日志加载过程
- slf4j的加载过程
- Log4j2的标签详解
日志前言
- 什么是日志 ?
日志用来记录用户操作、系统运行状态等,是一个系统的重要组成部分。 - 日志的作用 ?
好的日志可以帮助我们:了解线上系统的运行状态;快速准确定位线上问题;发现系统瓶颈;预警系统潜在风险;挖掘产品最大价值……
不好的日志导致:对系统的运行状态一知半解;系统出现问题无法定位,或需要花费巨大的时间和精力;无法发现系统瓶颈;不知优化从何做起;无法对错误进行监控和报警;对挖掘用户行为和提升产品价值毫无帮助…… - 日志分类
日志从功能来说,可分为诊断日志、统计日志、审计日志。
- 诊断日志
3.1.1 请求的入口出口
3.1.2 服务调用和返回
3.1.3 资源消耗操作
3.1.4 容错行为
3.1.5 程序异常
3.1.6 后台操作
3.1.7 启动关闭、配置加载
… - 统计日志
3.2.1用户统计访问
3.2.2计费日志
… - 审计日志
3.3.1按照指定格式输出日志
3.3.2按照需求输出日志
…
注意:日志我们需要记录我们正好需要的日志信息,尽量避免繁杂无用的日志信息出现
日志框架的简单介绍
目前在在市场上目前的日志框架有:JCL(Jakarta Commons Logging)、JUL(Java util Logging)、Jboss-Logging、SLF4J、Log4j、Logback、Log4j2 …
日志门面框架:JCL、SLF4J…
日志实现框架:JUL、Log4J、Logback、Log4j2…
Remember a person and an institution:
Ceki Gulcu -->SLF4J、Log4J、Logback
Apache -->JCL、Log4J2
- JCL:采用适配器模式,是Java自身的一些包用了JUL,提供一套API来实现不同Logger之间的切换。JCL致命的缺点就是算法复杂,出现问题难以排除。
- SLF4J:简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只服务于各种各样的日志系统。解决了JCL的缺点。
- JUL:是JDK 中自带的log功能,JDK1.4 才开始加入(2002年),但使用不够广泛。早期存在性能问题,到JDK1.5上才有了不错的进步,与Logback/Log4j2相比还是有所不如的。
- Log4J:2001年由Ceki Gulcu发布,在2014年12月发行了最后一版。springboot从1.4版本之后不再支持。短板在于它的性能问题。
- Logback:与Log4j相比,Logback重新了内核,使它的性能提升了很多,大约是Log4j的10倍,同时占用更小的内存,并且完整的实现了SLF4J API是你可以很方便的切换日志框架。
- Log4j2:Log4j的升级版本,比Log4j 1.x有重大改进,修复了Logback中一些问题。但是不兼容Log1.x版本。Log4J2是现在最优秀的Java日志框架是Log4j2,没有之一。根据官方的测试表明,在多线程环境下,Log4j2的异步日志表现更加优秀。在异步日志中,Log4j2使用独立的线程去执行I/O操作,可以极大地提升应用程序的性能。
Logback和Log4J2性能对比
当线程数量较少时:Logback和Log4j2的性能相差无几
当线程数量较大时,很明显Log4j2的性能要10倍于Logback。Log4j2在独立应用程序中基本是无垃圾的,在web项目中是低垃圾的,减少了GC的内存消耗。
springboot集成slf4j抽象日志框架
- 导入springboot启动依赖
- 写测试类进行原生slf4j抽象日志框架的日志打印
日志打印结果:
springboot集成log4j2日志框架
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 排除掉SpringBoot的默认log配置 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--log4j2启动类-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
- xml的配置
注意:springboot的加载顺序是会先去默认查找classes目录下是否存在log4j2.properties、log4j2.xml。如果存在,springboot便会直接使用该文件中的配置进行日志打印;如果不存在,会变回去springboot工程的配置文件application.properties中根据相关配置去匹配对象的日志配置文件。
private String[] getCurrentlySupportedConfigLocations() {
List<String> supportedConfigLocations = new ArrayList();
supportedConfigLocations.add("log4j2.properties");
if (this.isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) {
Collections.addAll(supportedConfigLocations, new String[]{"log4j2.yaml", "log4j2.yml"});
}
if (this.isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) {
Collections.addAll(supportedConfigLocations, new String[]{"log4j2.json", "log4j2.jsn"});
}
supportedConfigLocations.add("log4j2.xml");
return StringUtils.toStringArray(supportedConfigLocations);
}
- 配置log4j2.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="WARN" monitorInterval="30">
<Properties>
<property name="LOG_INFO_STYLE" value="[time:] %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n" />
<Property name="logFilePath">D:/logs</Property>
<Property name="logFileName">loggerTest</Property>
</Properties>
<appenders>
<console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_INFO_STYLE}"/>
</console>
<RollingFile name="RollingFileInfo"
fileName="${logFilePath}/${logFileName}.log"
filePattern="${logFilePath}/${logFileName}_%d{yyyy-MM-dd}/${logFileName}.log">
<PatternLayout pattern="${LOG_INFO_STYLE}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10 MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingFile>
</appenders>
<loggers>
<logger name="org.springframework" level="INFO"></logger>
<logger name="org.mybatis" level="INFO"></logger>
<root level="ALL">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileInfo"/>
</root>
</loggers>
</configuration>
- 我们这里写一个测试类来进行日志框架的打印输出
package com.fdd.test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class log4j2Demo {
public static void main(String[] args) {
Logger logger= LoggerFactory.getLogger(log4j2Demo.class);
//日志级别由低到高 trace<debug<info<warn<error
logger.trace("这个是trace级别的日志信息....");
logger.debug("这个是debug级别的日志信息....");
logger.info("这个是info级别的日志信息....");
logger.warn("这个是warn级别的日志信息....");
logger.error("这个是error级别的日志信息....");
}
}
- 输出结果如下:
D:\develop\Java\jdk-1.8\bin\java
[time:] 2020-01-06 14:40:06.555 TRACE com.fdd.test.log4j2Demo 13 main - 这个是trace级别的日志信息....
[time:] 2020-01-06 14:40:06.602 DEBUG com.fdd.test.log4j2Demo 14 main - 这个是debug级别的日志信息....
[time:] 2020-01-06 14:40:06.603 INFO com.fdd.test.log4j2Demo 15 main - 这个是info级别的日志信息....
[time:] 2020-01-06 14:40:06.603 WARN com.fdd.test.log4j2Demo 16 main - 这个是warn级别的日志信息....
[time:] 2020-01-06 14:40:06.603 ERROR com.fdd.test.log4j2Demo 17 main - 这个是error级别的日志信息....
springboot集成logback日志框架
- 导入依赖
由于springboot是默认采用上slf4j+logback进行日志打印的,所以我们只需要采用springboot的日志依赖即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring-boot-starter-web包下引入了spring-boot-starter-logging依赖,集成了logback日志框架。
- xml配置
注意:springboot的加载顺序是会先去默认查找classes目录下是否存在logback-test.groovy、logback-test.xml、logback.groovy、logback.xml。如果存在,springboot便会直接使用该文件中的配置进行日志打印;如果不存在,会变回去springboot工程的配置文件application.properties中根据相关配置去匹配对象的日志配置文件。 - 配置logback.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration >
<property name="PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level%logger{50} - %msg%n"/>
<property name="TEST_FILE_PATH" value="D:/logback"/>
<property name="PRO_FILE_PATH" value="/opt/test/log"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${PATTERN}</pattern>
</encoder>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${PATTERN}</pattern>
</layout>
</appender>
<appender name="TEST-FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${TEST_FILE_PATH}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
<MaxHistory>100</MaxHistory>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>${PATTERN}</pattern>
</layout>
</appender>
<logger name="com.fdd" level="info"/>
<root level="info">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="TEST-FILE"/>
</root>
</configuration>
- 日志打印结果如下:
2020-01-06 15:32:52.553 [main] INFO com.fdd.springlogger001.Demo - 自定义 info ........................
2020-01-06 15:32:52.557 [main] WARN com.fdd.springlogger001.Demo - 自定义 warn ........................
2020-01-06 15:32:52.558 [main] ERRORcom.fdd.springlogger001.Demo - 自定义 error ........................
springboot日志配置的加载过程
- LoggingApplicationListener类:
在run();方法启动的时候,日志监听类LoggingApplicationListener便会对工程中的配置文件进行监听加载的操作,这里我们主要看启动事件的方法onApplicationStartingEvent();
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
this.loggingSystem.beforeInitialize();
}
在这里我们主要关注前置初始化的方法beforeInitialize();以logback框架的实现来看:
public void beforeInitialize() {
LoggerContext loggerContext = this.getLoggerContext();
if (!this.isAlreadyInitialized(loggerContext)) {
super.beforeInitialize();
loggerContext.getTurboFilterList().add(FILTER);
}
}
此初始化方法可以获得一个Context的日志容器。在此类中存在加载本地配置文件的方法getStandardConfigLocations();读取springboot的默认文件配置名称。
protected String[] getStandardConfigLocations() {
return new String[]{"logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml"};
}
在获取日志容器的过程中,ch.qos.logback.classic包下存在一个ContextSelector类,内部含有getLoggerContextautoConfig()方法,为默认自动配置信息。
public LoggerContext getLoggerContext() {
String contextName = null;
Context ctx = null;
LoggerContext lc = (LoggerContext)threadLocal.get();
if (lc != null) {
return lc;
} else {
try {
ctx = JNDIUtil.getInitialContext();
contextName = JNDIUtil.lookup(ctx, "java:comp/env/logback/context-name");
} catch (NamingException var8) {
;
}
if (contextName == null) {
return this.defaultContext;
} else {
LoggerContext loggerContext = (LoggerContext)this.synchronizedContextMap.get(contextName);
if (loggerContext == null) {
loggerContext = new LoggerContext();
loggerContext.setName(contextName);
this.synchronizedContextMap.put(contextName, loggerContext);
URL url = this.findConfigFileURL(ctx, loggerContext);
if (url != null) {
this.configureLoggerContextByURL(loggerContext, url);
} else {
try {
(new ContextInitializer(loggerContext)).autoConfig();
} catch (JoranException var7) {
;
}
}
if (!StatusUtil.contextHasStatusListener(loggerContext)) {
StatusPrinter.printInCaseOfErrorsOrWarnings(loggerContext);
}
}
return loggerContext;
}
}
}
slf4j的加载过程
- org.slf4j.LoggerFactory包下的getLogger()方法为我们打印日志时候的入口;
public static Logger getLogger(Class<?> clazz) {
Logger logger = getLogger(clazz.getName());
if (DETECT_LOGGER_NAME_MISMATCH) {
Class<?> autoComputedCallingClass = Util.getCallingClass();
if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(), autoComputedCallingClass.getName()));
Util.report("See http://www.slf4j.org/codes.html#loggerNameMismatch for an explanation");
}
}
return logger;
}
- 点击getLogger()方法
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
- 进入到getLoggerFactory()方法中,我们可以看到:
public ILoggerFactory getLoggerFactory() {
if (!this.initialized) {
return this.defaultLoggerContext;
} else if (this.contextSelectorBinder.getContextSelector() == null) {
throw new IllegalStateException("contextSelector cannot be null. See also http://logback.qos.ch/codes.html#null_CS");
} else {
return this.contextSelectorBinder.getContextSelector().getLoggerContext();
}
}
此时,在日志工厂方法中我门看到了获取日志容器的方法getLoggerContext();
public LoggerContext getLoggerContext() {
String contextName = null;
Context ctx = null;
LoggerContext lc = (LoggerContext)threadLocal.get();
if (lc != null) {
return lc;
} else {
try {
ctx = JNDIUtil.getInitialContext();
contextName = JNDIUtil.lookup(ctx, "java:comp/env/logback/context-name");
} catch (NamingException var8) {
;
}
if (contextName == null) {
return this.defaultContext;
} else {
LoggerContext loggerContext = (LoggerContext)this.synchronizedContextMap.get(contextName);
if (loggerContext == null) {
loggerContext = new LoggerContext();
loggerContext.setName(contextName);
this.synchronizedContextMap.put(contextName, loggerContext);
URL url = this.findConfigFileURL(ctx, loggerContext);
if (url != null) {
this.configureLoggerContextByURL(loggerContext, url);
} else {
try {
(new ContextInitializer(loggerContext)).autoConfig();
} catch (JoranException var7) {
;
}
}
if (!StatusUtil.contextHasStatusListener(loggerContext)) {
StatusPrinter.printInCaseOfErrorsOrWarnings(loggerContext);
}
}
return loggerContext;
}
}
}
此方法中含有autoConfig()自动配置的方法。这里就是我们日志加载过程中slf4j的默认自动配置信息。
标签详解
这里我们以log4j2的标签作为代表进行介绍。在目前市场上的日志实现框架中它们的配置文件标签大都相差无几。我们学会log4j2的标签之后,其他日志框架的标签也是很好去学习的。
- log4j.xml的实例代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--status="WARN" :用于设置log4j2自身内部日志的信息输出级别,默认是OFF-->
<!--monitorInterval="30" :间隔秒数,自动检测配置文件的变更和重新配置本身-->
<configuration status="WARN" monitorInterval="30">
<Properties>
<!--自定义一些常量,之后使用${变量名}引用-->
<Property name="logFilePath">log</Property>
<Property name="logFileName">test.log</Property>
</Properties>
<!--appenders:定义输出内容,输出格式,输出方式,日志保存策略等,常用其下三种标签[console,File,RollingFile]-->
<appenders>
<!--console :控制台输出的配置-->
<console name="Console" target="SYSTEM_OUT">
<!--PatternLayout :输出日志的格式,LOG4J2定义了输出代码,详见第二部分-->
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
</console>
<!--File :同步输出日志到本地文件-->
<!--append="false" :根据其下日志策略,每次清空文件重新输入日志,可用于测试-->
<File name="log" fileName="${logFilePath}/${logFileName}" append="false">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
</File>
<!--SMTP :邮件发送日志-->
<SMTP name="Mail" subject="****SaaS系统正式版异常信息" to="message@message.info" from="message@lengjing.info" smtpUsername="message@message.info" smtpPassword="LENG****1234" smtpHost="mail.lengjing.info" smtpDebug="false" smtpPort="25" bufferSize="10">
<PatternLayout pattern="[%-5p]:%d{YYYY-MM-dd HH:mm:ss} [%t] %c{1}:%L - %msg%n" />
</SMTP>
<!-- ${sys:user.home} :项目路径 -->
<RollingFile name="RollingFileInfo" fileName="${sys:user.home}/logs/info.log"
filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log">
<!--ThresholdFilter :日志输出过滤-->
<!--level="info" :日志级别,onMatch="ACCEPT" :级别在info之上则接受,onMismatch="DENY" :级别在info之下则拒绝-->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
<!-- Policies :日志滚动策略-->
<Policies>
<!-- TimeBasedTriggeringPolicy :时间滚动策略,默认0点小时产生新的文件,interval="6" : 自定义文件滚动时间间隔,每隔6小时产生新文件, modulate="true" : 产生文件是否以0点偏移时间,即6点,12点,18点,0点-->
<TimeBasedTriggeringPolicy interval="6" modulate="true"/>
<!-- SizeBasedTriggeringPolicy :文件大小滚动策略-->
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件,这里设置了20 -->
<DefaultRolloverStrategy max="20"/>
</RollingFile>
<RollingFile name="RollingFileWarn" fileName="${sys:user.home}/logs/warn.log"
filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log">
<ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
</RollingFile>
<RollingFile name="RollingFileError" fileName="${sys:user.home}/logs/error.log"
filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log">
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
</RollingFile>
</appenders>
<!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
<loggers>
<!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
<!--Logger节点用来单独指定日志的形式,name为包路径,比如要为org.springframework包下所有日志指定为INFO级别等。 -->
<logger name="org.springframework" level="INFO"></logger>
<logger name="org.mybatis" level="INFO"></logger>
<!-- Root节点用来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该Root日志输出 -->
<root level="all">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileInfo"/>
<appender-ref ref="RollingFileWarn"/>
<appender-ref ref="RollingFileError"/>
</root>
<!--AsyncLogger :异步日志,LOG4J有三种日志模式,全异步日志,混合模式,同步日志,性能从高到底,线程越多效率越高,也可以避免日志卡死线程情况发生-->
<!--additivity="false" : additivity设置事件是否在root logger输出,为了避免重复输出,可以在Logger 标签下设置additivity为”false”-->
<AsyncLogger name="AsyncLogger" level="trace" includeLocation="true" additivity="false">
<appender-ref ref="RollingFileError"/>
</AsyncLogger>
</loggers>
</configuration>
(未完待续…)