一:前言
近期做一个项目打造项目的日志系统时,发现没有一个系统的学习,故准备系统学习一下日志系统,这主要是介绍了如何打造一个项目的日志系统。喜欢的话 点个赞 呗~
java 界里有许多实现日志功能的工具,最早得到广泛使用的是 log4j,许多应用程序的日志部分都交给了 log4j,不过作为组件开发者,他们希望自己的组件不要紧紧依赖某一个工具,毕竟在同一个时候还有很多其他很多日志工具,假如一个应用程序用到了两个组件,恰好两个组件使用不同的日志工具,那么应用程序就会有两份日志输出了。
为了解决这个问题,JCL和SLF4j就出现了,JCL只提供 log 接口,具体的实现则在运行时动态寻找。这样一来组件开发者只需要针对JCL或者slf4j的接口开发,而调用组件的应用程序则可以在运行时搭配自己喜好的日志实践工具。跟 JCL 一样,SLF4J 也是只提供 log 接口,具体的实现是在打包应用程序时所放入的绑定器(名字为 slf4j-XXX-version.jar)来决定,XXX 可以是 log4j12, jdk14, jcl, nop 等,他们实现了跟具体日志工具(比如 log4j)的绑定及代理工作。举个例子:如果一个程序希望用 log4j 日志工具,那么程序只需针对 slf4j-api 接口编程,然后在打包时再放入 slf4j-log4j12-version.jar 和 log4j.jar 就可以了。
项目中我们选择了SLF4j Log4j2来打造日志系统,log4j2的性能还是比Logback好一些的,下面有对比。
二:添加依赖
2.1:去除直接和间接依赖的log4j1和SLF4j
首先我们应该先删除项目已经依赖的其他日志组件,这里指的是没有用到的日志组件,例如janusgraph会间接依赖log4j1的组件,这个组件删除就会报错,所以我们只要删除没有使用的日志组件,这样可以使项目更加干净~ 方法:我们可以观察项目目录下的External Libraries下的依赖文件,如果有log4j1或者其他日志依赖,我们将他们在pom文件中找到删除即可。
如果依赖中有但是pom文件中找不到,就是被间接依赖进来的了,我们在pom 文件中右击鼠标,选中Diagrams->show dependences就可以看到整个项目的依赖图,在其中找到对应的log依赖,选中右击Exclude即可。
2.2:添加依赖
添加的所有依赖都是截止2018.11.22日最新的稳定版本
代码语言:javascript复制<!--slf4j-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.25</version>
<scope>runtime</scope>
</dependency>
<!--log4j2-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-web</artifactId>
<version>2.11.1</version>
<scope>runtime</scope>
</dependency>
<!--log4j2 slf4j-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.11.1</version>
</dependency>
上述的jcl-over-scf4j的作用以及原因: 即使现在你仍会看到很多程序应用 JCL log4j 这种搭配,不过当程序规模越来越庞大时,JCL的动态绑定并不是总能成功,具体原因大家可以 Google 一下,这里就不再赘述了。解决方法之一就是在程序部署时静态绑定指定的日志工具,这也是 SLF4J 产生的原因。
现在还有一个问题,假如你正在开发应用程序所调用的组件当中已经使用了 JCL 的,还有一些组建可能直接调用了 java.util.logging,这时你需要一个桥接器(名字为 XXX-over-slf4j.jar)把他们的日志输出重定向到 SLF4J,所谓的桥接器就是一个假的日志实现工具,比如当你把 jcl-over-slf4j.jar 放到 CLASS_PATH 时,即使某个组件原本是通过 JCL 输出日志的,现在却会被 jcl-over-slf4j “骗到”SLF4J 里,然后 SLF4J 又会根据绑定器把日志交给具体的日志实现工具,这样就可以实现日志的统一了。如果你的项目没有使用jcl那么就不必添加这个。
上述的log4j-web是在开发web项目的时候需要的,如果你不是web项目,可以酌情删除
三:xml配置
3.1:log4j2.xml常用demo
在类路径下新建文件:log4j2.xml ,注意“2”不要缺少,位置放的正确并且文件名符合要求的话,项目会自动扫描到该配置文件。
Log4j2能够在初始化期间自动配置自身。当Log4j2启动时,它将找到所有ConfigurationFactory插件并按加权顺序从最高到最低排列。在交付时,Log4j包含四个ConfigurationFactory实现:一个用于JSON,一个用于YAML,一个用于 properties,一个用于XML,下面为查找加载顺序:
- Log4j2将检查“log4j.configurationFile”系统属性,如果设置,将尝试使用与文件扩展名匹配的ConfigurationFactory加载配置。
- 如果未设置系统属性,则ConfigurationFactory属性将在类路径中查找 log4j2-test.properties。
- 如果没有找到这样的文件,YAML ConfigurationFactory将在类路径中查找 log4j2-test.yaml或log4j2-test.yml。
- 如果没有找到这样的文件,JSON ConfigurationFactory将在类路径中查找 log4j2-test.json或log4j2-test.jsn。
- 如果找不到这样的文件,XML ConfigurationFactory将在类路径中查找 log4j2-test.xml。
- 如果找不到测试文件,ConfigurationFactory属性将在类路径上查找 log4j2.properties。
- 如果找不到属性文件,YAML ConfigurationFactory将在类路径上查找 log4j2.yaml或log4j2.yml。
- 如果找不到YAML文件,JSON ConfigurationFactory将在类路径上查找 log4j2.json或log4j2.jsn。
- 如果找不到JSON文件,XML ConfigurationFactory将尝试在类路径上找到 log4j2.xml。
- 如果找不到配置文件,则将使用DefaultConfiguration。这将导致所有日志记录输出转到控制台
log4j2.xml 文件内容:
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO" monitorInterval="30">
<properties>
<!--设置容器日志在硬盘上输出的目录-->
<property name="logPath">/opt/logs/hrmapp/</property>
<!--设置项目日志在硬盘上输出的目录-->
<property name="logPathForProject">/opt/logs/hrmapp/project/</property>
<property name="logPathForTest">/opt/logs/hrmapp/test/</property>
</properties>
<Appenders>
<!--=====容器日志配置=====-->
<!--设置在控制台打印日志-->
<Console name="Console" target="SYSTEM_OUT">
<!--设置输出格式-->
<PatternLayout pattern="[%-5p]:%d{yyyy-MM-dd HH:mm:ss} [%t] %c{1}:%L - %msg%n" />
</Console>
<!--设置级别为INFO日志输出到info.log中,filename为输出日志的目录,filepattern为压缩文件的命名规范与目录 -->
<RollingFile name="INFO" filename="${logPath}/info.log"
filepattern="${logPath}/%d{yyyyMMdd}-info-%i.log.zip">
<!--设置日志级别-->
<Filters>
<ThresholdFilter level="INFO"/>
</Filters>
<!--输出日志的格式-->
<PatternLayout pattern="[ %-5p]:%d{yyyy-MM-dd HH:mm:ss} [%t] %c{1}:%L - %msg%n" />
<Policies>
<!--设置每天打包日志一次,**此处应注意一个问题,下面有描述**-->
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<!--设置最多保存20个日志文件,默认为7个-->
<DefaultRolloverStrategy max="20" />
</RollingFile>
<!--设置级别为WARN日志输出到warn.log中-->
<RollingFile name="WARN" filename="${logPath}/warn.log"
filepattern="${logPath}/%d{yyyyMMdd}-warn-%i.log.zip">
<Filters>
<!--设置只输出级别为WARN的日志-->
<ThresholdFilter level="WARN"/>
<ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<PatternLayout pattern="[ %-5p]:%d{yyyy-MM-dd HH:mm:ss} [%t] %c{1}:%L - %msg%n" />
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DefaultRolloverStrategy max="20" />
</RollingFile>
<!--设置级别为ERROR日志输出到error.log中-->
<RollingFile name="ERROR" filename="${logPath}/error.log"
filepattern="${logPath}/%d{yyyyMMdd}-error-%i.log.zip">
<!--设置只输出级别为ERROR的日志-->
<ThresholdFilter level="ERROR"/>
<PatternLayout pattern="[ %-5p]:%d{yyyy-MM-dd HH:mm:ss} [%t] %c{1}:%L - %msg%n" />
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DefaultRolloverStrategy max="20" />
</RollingFile>
<!--=====项目日志配置=====-->
<Console name="ConsolePro" target="SYSTEM_OUT">
<PatternLayout pattern="[%-5p]:%d{yyyy-MM-dd HH:mm:ss} [%t] %c{1}:%L - %msg%n" />
</Console>
<RollingFile name="INFOPro" filename="${logPathForProject}/info.log"
filepattern="${logPathForProject}/%d{yyyyMMdd}-info-%i.log.zip">
<Filters>
<ThresholdFilter level="INFO"/>
</Filters>
<PatternLayout pattern="[ %-5p]:%d{yyyy-MM-dd HH:mm:ss} [%t] %c{1}:%L - %msg%n" />
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DefaultRolloverStrategy max="20" />
</RollingFile>
<RollingFile name="WARNPro" filename="${logPathForProject}/warn.log"
filepattern="${logPathForProject}/%d{yyyyMMdd}-warn-%i.log.zip">
<Filters>
<ThresholdFilter level="WARN"/>
<ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<PatternLayout pattern="[ %-5p]:%d{yyyy-MM-dd HH:mm:ss} [%t] %c{1}:%L - %msg%n" />
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DefaultRolloverStrategy max="20" />
</RollingFile>
<RollingFile name="ERRORPro" filename="${logPathForProject}/error.log"
filepattern="${logPathForProject}/%d{yyyyMMdd}-error-%i.log.zip">
<ThresholdFilter level="ERROR"/>
<PatternLayout pattern="[ %-5p]:%d{yyyy-MM-dd HH:mm:ss} [%t] %c{1}:%L - %msg%n" />
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DefaultRolloverStrategy max="20" />
</RollingFile>
<!--配置自定义Logger打印-->
<RollingFile name="INFOTestAppender" filename="${logPathForTest}/logPathForTest.log"
filepattern="${logPathForTest}/%d{yyyyMMdd}-testInfo-%i.log.zip">
<ThresholdFilter level="INFO"/>
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} - %msg%n" />
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DefaultRolloverStrategy max="60"/>
</RollingFile>
</Appenders>
<Loggers>
<!--
添加项目日志
additivity="false" 表示只将该日志输出到该配置的路径下面,并不会再重复输出到root也就是容器日志中
-->
<logger name="com.test" level="debug" additivity="false">
<appender-ref ref = "ConsolePro"/>
<appender-ref ref = "INFOPro"/>
<appender-ref ref = "WARNPro" />
<appender-ref ref = "ERRORPro" />
</logger>
<logger name="INFOTest" level="debug" additivity="false">
<appender-ref ref = "INFOTestAppender"/>
</logger>
<!--添加容器日志-->
<root level="INFO">
<appender-ref ref = "Console"/>
<appender-ref ref = "INFO" />
<appender-ref ref = "WARN" />
<appender-ref ref = "ERROR" />
</root>
</Loggers>
</Configuration>
注释我写的应该比较清楚了,如果你还是不太明白,下面我会详细介绍一下。
<特别注意!!!>:
- 其中的
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
语句这里的“1”并不是特指一天,而是数量1,对于单位不管是“天”、“分”、“秒”,也就是1秒打包一次或者1分钟打印一次,都是取决于<RollingFile name="WARNPro" filename="${logPathForProject}/warn.log" filepattern="${logPathForProject}/%d{yyyyMMdd}-%i-warn.log.zip">
中filepattern最小单位。这里我们的filepattern最小单位是天,所以是每天打包一次。 - 其中的yyyyMMdd、HH:mm:ss类似的日期或者时间设置,其中的大小写不要写错了,比如YYYYMMdd这样在一些服务器可能就不识别,导致系统就直接使用默认的打印格式了。
3.2:demo的优点
- 将项目的日志和容器的日志分开打印到不同的文件夹中,这样便于查看与管理。比如,一个容器中部署了多个项目,如果不分开打印log的话所有的log都打印到容器的log中,所有项目和容器的log在一个文件中管理和查看的难度可以想象出来。如果每个项目一个对应的文件夹,所有的项目和容器都相互分开,将自己的日志打印到自己对应的日志文件中,简洁、方便查看、便于管理
- 将日志的info、warn、error级别的日志分开单独打印,INFO包含infowarnerror所有的日志,WARN使其只包含warn的日志,ERROR使其只包含error的日志,这样在发现错误和异常更加便利
- 将日志文件压缩存储,减少资源消耗
- 控制日志文件数量,在保证日志可追溯许可的范围下删除过早的日志文件,减少资源消耗
- 以天为单位打包日志,便于查找日志
- 自定义Logger打印,可以精确到一个Logger的日志打印控制
3.3:内容详解
1: 根节点Configuration有两个属性:status和monitorinterval
- status用来指定log4j2本身的打印日志的级别
- monitorinterval用于指定log4j自动重新配置的监测间隔时间,单位是s,最小是5s
2:根节点下的子节点properties,用于定义变量和修改变量,这里我只定义了两个路径变量,一个是容器log路径,一个是项目log路径 3:根节点下的子节点Appenders,主要用于定义Appender,常见的有三种子节点:Console、RollingFile、File
log4j组件提供了好多种appender供我们使用,介绍看官网吧特别详细: http://logging.apache.org/log4j/2.x/manual/appenders.html
- Console节点用来定义输出到控制台的Appender.
- name:指定Appender的名字.
- target:SYSTEM_OUT(输出所有) 或 SYSTEM_ERR(只输出错误),一般只设置默认:SYSTEM_OUT.
- PatternLayout:输出格式,不设置默认为:%m%n.
- File节点用来定义输出到指定位置的文件的Appender.
- name:指定Appender的名字.
- fileName:指定输出日志的目的文件带全路径的文件名.
- PatternLayout:输出格式,不设置默认为:%m%n.
- RollingFile节点用来定义超过指定大小自动删除旧的创建新的的Appender并且可以压缩文件.
- name:指定Appender的名字.
- fileName:指定输出日志的目的文件带全路径的文件名.
- PatternLayout:输出格式,不设置默认为:%m%n.
- filePattern:指定新建日志文件的名称格式.
- Policies:指定滚动日志的策略,就是什么时候进行新建日志文件输出日志.
- TimeBasedTriggeringPolicy:Policies子节点,基于时间的滚动策略,interval属性用来指定多久滚动一次,默认是1 hour。modulate=true用来调整时间:比如现在是早上3am,interval是4,那么第一次滚动是在4am,接着是8am,12am…而不是7am.
- SizeBasedTriggeringPolicy:Policies子节点,基于指定文件大小的滚动策略,size属性用来定义每个日志文件的大小.
- DefaultRolloverStrategy:用来指定同一个文件夹下最多有几个日志文件时开始删除最旧的,创建新的(通过max属性)。
其中:fileName和filePattern不同的作用: fileName指定的是当天日志输出的日志输出位置 filePattern指的根据配置,对每天的日志文件进行压缩存储的时候的文件名,也就是新建的文件名
4:根节点下的子节点Loggers,用于配置上述添加的appender,两种子节点:Root、Logger
- Root节点用来指定项目的根日志,如果没有单独指定Logger,那么就会默认使用该Root日志输出
- level:日志输出级别,共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF.
- AppenderRef(appender-ref):Root的子节点,用来指定该日志输出到哪个Appender.
- Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等
- level:日志输出级别,共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF.
- additivity : 设置是否继承,也就是是否将log也打印到Root下,“false”为不打印到Root下
- name:用来指定该Logger所适用的类或者类所在的包全路径,继承自Root节点.
- AppenderRef(appender-ref):Logger的子节点,用来指定该日志输出到哪个Appender,如果没有指定,就会默认继承自Root.如果指定了,那么会在指定的这个Appender和Root的Appender中都会输出,此时我们可以设置Logger的additivity="false"只在自定义的Appender中进行输出。
5:自定义控制Logger 可以将日志打印精确到 一个类,一个方法,一个Logger 。 上述demo中配置了对一个特定的Logger操作,只将此Logger的日志打印到对应的文件中。
代码语言:javascript复制 <!--配置自定义Logger打印-->
<RollingFile name="INFOTestAppender" filename="${logPathForTest}/logPathForTest.log"
filepattern="${logPathForTest}/%d{yyyyMMdd}-testInfo-%i.log.zip">
<ThresholdFilter level="INFO"/>
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} - %msg%n" />
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DefaultRolloverStrategy max="60"/>
</RollingFile>
......
<logger name="INFOTest" level="debug" additivity="false">
<appender-ref ref="INFOTestAppender"/>
</logger>
在java代码中,这样创建Logger:
代码语言:javascript复制private static final Logger INFO_TEST= LoggerFactory.getLogger("INFOTest");
然后,Logger : INFO_TEST 打印的日志就会单独的存放在对应的日志文件中。
注意: 注意对应关系,<logger name="INFOTest" level="debug" additivity="false">
此处定义了一个名为INFOTest
的自定义Logger,在java代码中获取该Logger进行日志打印。
用途: 这种配置可以用于埋点日志的配置,注意埋点日志的打印格式,尽量只包含关键信息并且分隔符统一,这样可以便于日志分析。例如:%d{yyyy-MM-dd HH:mm:ss} %msg%n
只包含打印时间和日志信息,并且采用四个空格分割,在 %msg
打印的日志信息里面,也尽量采用四个空格分隔不同部分,做到整体日志信息采用统一分隔符
6:输出格式相关:
- %t:线程名称
- %p:日志级别
- %c:日志消息所在类名
- %m:消息内容
- %M:输出执行方法
- %d:发生时间,%d{yyyy-MM-dd HH:mm:ss,SSS},输出类似:2011-10-18 22:10:28,921
- %x::输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中。
- %L:代码中的行数
- %n:换行
- %c{*}系列:显示LoggerName的格式
我平常使用的就是:[%-5p]:%d{YYYY-MM-dd HH:mm:ss} [%t] %c{2}:%L - %msg%n
3.4:demo变形
3.4.1:同步打印日志
同步打印日志是最消耗资源的方式,我们在开发的时候,可以选择使用全同步方式打印日志,这样便于我们debug。或者项目并发度不高的情况下也可以使用这种方式。但是,当并发量比较大、对项目响应速度敏感时并且对日志不是强实时性要求的话,最好还是使用全部异步或者混合方式。
上述的demo便是全部同步的案例。在此不再赘述。
3.4.2:全部异步打印日志
全部异步打印日志是对项目请求速度最理想的方式,在500个线程的情况下速度几乎是全同步打印log的10倍,是混合打印的2倍。下面是官网的比较图,可以对照着看一下:
异步Logger是让业务逻辑把日志信息放入Disruptor队列后可以直接返回,具有更高吞吐、调用log方法更低的延迟。但也有一些缺点比如:异常处理麻烦、 可变日志消息问题、更大的CPU开销、需要等待“最慢的Appender”消费完成。
所以我们在并发量高、日志实时性要求不高,并且所暴漏的缺点都可以容忍的情况下最好还是选用全部异步打印日志,这样可以获得更快的响应,也会给用户更好的体验。 异步打印配置有几种方式: 1:在你的classpath下面添加个log4j2.component.properties文件,并且添加以下内容:
代码语言:javascript复制这种方式不需要修改原来的log4j2.xml文件
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
2:使用异步标签,修改上面的demo的部分:
代码语言:javascript复制如果想要全部异步log的话,一定要所有的相关标签都是用异步标签
<Loggers>
<!--
添加项目日志
additivity="false" 表示只将该日志输出到该配置的路径下面,并不会再重复输出到root也就是容器日志中
-->
<asyncLogger name="com.test" level="debug" additivity="false">
<appender-ref ref = "ConsolePro"/>
<appender-ref ref = "INFOPro"/>
<appender-ref ref = "WARNPro" />
<appender-ref ref = "ERRORPro" />
</asyncLogger>
<!--添加容器日志-->
<asyncRoot level="INFO">
<appender-ref ref = "Console"/>
<appender-ref ref = "INFO" />
<appender-ref ref = "WARN" />
<appender-ref ref = "ERROR" />
</asyncRoot>
</Loggers>
3:JVM启动参数(boot.ini)加上:
代码语言:javascript复制-DLog4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
3.4.3:混合模式打印日志
混合模式就是既有一异步又有同步日志打印,那些部分需要同步或者异步,这需要根据具体对项目该部分的需求来定了.
下面我设置了项目日志同步打印,容器日志异步打印
代码语言:javascript复制 <Loggers>
<!--
添加项目日志
additivity="false" 表示只将该日志输出到该配置的路径下面,并不会再重复输出到root也就是容器日志中
-->
<logger name="com.test" level="debug" additivity="false">
<appender-ref ref = "ConsolePro"/>
<appender-ref ref = "INFOPro"/>
<appender-ref ref = "WARNPro" />
<appender-ref ref = "ERRORPro" />
</logger>
<!--添加容器日志-->
<asyncRoot level="INFO">
<appender-ref ref = "Console"/>
<appender-ref ref = "INFO" />
<appender-ref ref = "WARN" />
<appender-ref ref = "ERROR" />
</asyncRoot>
</Loggers>
四:其他
4.1:Log日志level
级别只输出“大于等于”自身级别的log,7中level的级别关系如下: OFF > FATAL > ERROR > WARN > INFO > DEBUG > ALL 1. DEBUG : DEBUG Level指出细粒度信息事件对调试应用程序是非常有帮助的。 2. INFO INFO level表明 消息在粗粒度级别上突出强调应用程序的运行过程。 3.WARN WARN level表明会出现潜在错误的情形。 4.ERROR ERROR level指出虽然发生错误事件,但仍然不影响系统的继续运行。 5.FATAL FATAL level指出每个严重的错误事件将会导致应用程序的退出。 6.ALL ALL Level是最低等级的,用于打开所有日志记录。 7.OFF OFF Level是最高等级的,用于关闭所有日志记录。
4.2:Log4j2与logback速度对比
Log4j2和logback都是日志组件,logback就是为了替代log4j1出现的,log4j2是log4j1的升级版,几乎相当于重构了log4j1。 log4j2的效率可以在多线程时,在线程数量大的情况下,超过logback10倍左右!下面是官网提供的数据对比: 速度对比图(来自官网):