java中的日志框架
2019-10-08 本文已影响0人
suxin1932
各日志框架配置原则: 先看官网 --> 再看源代码 --> 最后中文博客
1.java中日志概述
在开发过程中,应用系统关于log的jar包非常的混乱,而这种混乱常常会带来jar包冲突、多份日志输出等各种问题。
比如你应用采用了log4j作为日志实现,但是你又通过间接依赖的方式引入了logback的包,
这样开发者往往很难察觉,往往是出现了相应的异常现象才排查出log冲突的问题。
1.1 java日志框架的历史
>> Apache Commons Logging(Jakarta Commons Logging,JCL)
>> Simple Logging Facade for Java (SLF4J)
>> Apache Log4j(Log4j2)
>> Java Logging API(JUL)
>> Logback
>> tinylog
在这些日志组件当中,最早得到广泛应用的是log4j,
成为了Java日志的事实上的标准,现在可以看到很多应用都是依赖于log4j的日志实现。
然而当时Sun公司在jdk1.4中增加了JUL(java.util.logging),企图对抗log4j,于是造成了混乱,
当然此时也有其它的一些日志框架的出现,如simplelog等,简直是乱上加乱。
为了解决这种混乱Commons Logging出现了,他只提供日志的接口,而具体的实现则在运行过程中动态寻找。
这样在代码中全部使用Commons Logging的编程接口,而具体日志实现则在外部配置中体现。
这样还有一个好处,由于应用日志并不依赖具体的实现,那么应用日志的实现则可以轻松的切换。
所以现在也能看到很多应用基于Commons Logging+Log4j的搭配。
但是呢log4j的作者觉得Commons Loggin不够优秀,于是自己实现了一套更为优雅的,
这个就是SLF4J,并且还亲自实现了一个日志实现logback。
那么现在关于log的局面就更为混乱了。
为了让之前使用Commons Logging和JUL的能够很好的转到SLF4J的体系中来,
log4j的作者又对其他的日志工具做了桥接......
后来该作者又重写了log4j,即log4j2,同时log4j2也加进了SLF4J体系中......
1.2 主流日志工具介绍
1.2.1 Commons-logging
Commons-logging是Apache提供的一个日志抽象,他提供一组通用的日志接口。
应用自由选择第三方日志实现,像JUL、log4j等。
这样的好处是代码依赖日志抽象接口,并不是具体的日志实现,这样在更换第三方库时带来了很大便利。
工作原理:
1、查找名为org.apache.commons.logging.Log的factory属性配置
(可以是java代码配置,也可以是commons-logging.properties配置);
2、查找名为org.apache.commons.logging.Log的系统属性;
3、上述配置不存在则 classpath下是否有Log4j日志系统,如有则使用相应的包装类;
3、如果系统运行在JDK 1.4系统上,则使用Jdk1.4 Logger;
4、上述都没有则使用SimpleLog。
所以如果使用commons-logging+log4j的组合只需要在classpath中加入log4j.xml配置即可。
commons-logging的动态查找过程是在程序运行时自动完成的。
他使用ClassLoader来寻找和载入底层日志库,
所以像OSGI这样的框架无法正常工作,因为OSGI的不同插件使用自己的ClassLoader。
1.2.2 SLF4J(Simple logging facade for Java)
SLF4J类似于commons-logging,他也是日志抽象。
和commons-logging动态查找不同slf4j是静态绑定,他是在编译时就绑定真正的log实现。
同时slf4j还提供桥接器可以将基于commons-loggging、jul的日志重定向到slf4j。
比如程序中以前使用的commong-logging,那么你可以通过倒入jcl-over-slf4j包来讲日志重定向到slf4j。
SLF4J提供了统一的记录日志的接口(LoggerFactory),只要按照其提供的方法记录即可,
最终日志的格式、记录级别、输出方式等通过具体日志系统的配置来实现,因此可以在应用中灵活切换日志系统。
// SLF4J提供的桥接包:
• slfj-log4j12.jar (表示桥接 log4j)
• slf4j-jdk14.jar(表示桥接jdk Looging)
• sIf4j-jcl.jar(表示桥接 jcl)
• log4j-slf4j-impl(表示桥接log4j2)
• logback-classic(表示桥接 logback)
SLF4J与各种日志实现的使用.png
SLF4J桥接.png
1.2.3 Log4j & Log4j2
log4j是Apache的开源日志框架,其最新版本是在2012年5月更新的1.2.17版本。
log4j2在其基础之上进行了重写,其具有插件式的架构、强大的配置功能、锁的优化、java8支持等特性。
1.2.4 Logback
Logback是由log4j创始人设计的又一个开源日志组件。当前分成三个模块:
>> logback-core
>> logback- classic
>> logback-access
logback-core是其它两个模块的基础模块。
logback-classic是log4j的一个改良版本,此外logback-classic完整实现SLF4J API。
logback-access访问模块与Servlet容器集成提供通过Http来访问日志的功能。
Logback是要与SLF4J结合起来用。
1.3 最佳实现
1.3.1 二方库使用
二房库中建议不要绑定任何的日志实现,统一使用日志抽象(commons-logging、slf4j)。
<!-- 除此之外不要依赖别的log包 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
1.3.2 slf4j+logback
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<!-- logback-classic包含logback-core依赖 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.7</version>
</dependency>
1.3.3 slf4j+log4j
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.21</version>
</dependency>
<!--slf4j-log4j12包含了log4j依赖 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
1.4 问题与冲突
1.4.1 老应用日志改造
老应用则没有改变日志的必要,因为会有开发成本。但是开发需要保证三点:
1、应用依赖中同一个log包不能出现多个版本;
2、日志实现框架必须唯一,可以log4j、logback等,但是不能出现既有log4j又有logback的情况;
3、日志桥接不要出现循环重定向,比如你加入了jcl-over-slf4j.jar之后又加入了slf4j-jcl.jar。
1.4.2 日志系统的冲突
// 目前日志系统的冲突主要分为两种:
>> 同一个日志系统的多个实现
>> 桥接接口与实现类
// 冲突1: 同一个日志系统的多个实现
像slf4j接口实现的冲突,如:
slf4j-log4j、logback、slf4j-jdk14、log4j2之间的冲突
这几个包都实现了slf4j的接口,同一接口只能有一个实现才能被jvm正确识别,
与传统的jar冲突相同,当jvm发现两个一模一样的实现的时候,它就不知道选择哪个或选择了一个错误的,
就会提示ClassNotFound.
// 冲突2: 桥接jar与实现包
在日志系统中,最常见的就是桥接jar包与实现包的冲突,如:
>> jul-to-slf4j 与 slf4j-jdk14
>> log4j-over-slf4j 与 slf4j-log4j
>> jcl-over-slf4j 与 jcl
因为转接的实现就是将其余的日志系统调用进行一个转发,既然要转发,
就必须要定义与原有对象相同的类名、包名,才能正确的被调用,
所以桥接jar包就必然与实现包产生冲突。
// 其他冲突
slf4j-api和实现版本最好对应,尤其是1.6.x和1.5.x不兼容,直接升级到最新版本
https://yq.aliyun.com/articles/608736?spm=a2c4e.11153940.0.0.72182110hOwgxl (日志系统总结)
https://yq.aliyun.com/articles/57769?spm=a2c4e.11153940.0.0.72182110hOwgxl (日志系统常见问题)
2. log4j2 框架
2.1 org.apache.Log4j.Layout
模式转换字符
转换字符 | 含义 |
---|---|
%c | 使用它为输出的日志事件分类,比如对于分类 "a.b.c",模式 %c{2} 会输出 "b.c" 。 |
%C | 使用它输出发起记录日志请求的类的全名。比如对于类 "org.apache.xyz.SomeClass",模式 %C{1} 会输出 "SomeClass"。 |
%d | 使用它输出记录日志的日期,比如 %d{HH:mm:ss,SSS} 或 %d{dd MMM yyyy HH:mm:ss,SSS}。 |
%F | 在记录日志时,使用它输出文件名。 |
%l | 用它输出生成日志的调用者的地域信息。 |
%L | 使用它输出发起日志请求的行号。 |
%m | 使用它输出和日志事件关联的,由应用提供的信息。 |
%M | 使用它输出发起日志请求的方法名。 |
%n | 输出平台相关的换行符。 |
%p | 输出日志事件的优先级(DEBUG、INFO、WARN……)。 |
%r | 使用它输出从构建布局到生成日志事件所花费的时间,以毫秒为单位。 |
%t | 输出生成日志事件的线程名。 |
%x | 输出和生成日志事件线程相关的 NDC (嵌套诊断上下文)。 |
%X | 该字符后跟 MDC 键,比如 %X{clientIP} 会输出保存在 MDC 中键 clientIP 对应的值。 |
% | 百分号, %% 会输出一个 %。 |
格式修饰符 (pattern对齐修饰)
缺省情况下,信息保持原样输出。但是借助格式修饰符的帮助,就可调整最小列宽、最大列宽以及对齐。
格式修饰符 | 左对齐 | 最小宽度 | 最大宽度 | 注释 |
---|---|---|---|---|
%20c | 否 | 20 | 无 | 如果列名少于 20 个字符,左边使用空格补齐。 |
%-20c | 是 | 20 | 无 | 如果列名少于 20 个字符,右边使用空格补齐。 |
%.30c | 不适用 | 无 | 30 | 如果列名长于 30 个字符,从开头剪除。 |
%20.30c | 否 | 20 | 30 | 如果列名少于 20 个字符,左边使用空格补齐,如果列名长于 30 个字符,从开头剪除。 |
%-20.30c | 是 | 20 | 30 | 如果列名少于 20 个字符,右边使用空格补齐,如果列名长于 30 个字符,从开头剪除。 |
有些特殊符号不能直接打印,需要使用实体名称或者编号
& —— & 或者 &
< —— < 或者 <
> —— > 或者 >
“ —— " 或者 "
‘ —— ' 或者 '
2.2 MDC机制
https://blog.csdn.net/xiaolyuh123/article/details/80560662
https://logging.apache.org/log4j/2.x/manual/configuration.html (log4j2官网配置)
https://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout (log4j2 各种 %d%m 等配置来源参考)
3. logback 框架
http://logback.qos.ch/manual/introduction.html (logback 官网配置)
https://blog.csdn.net/wangyonglin1123/article/details/85119724 (logback.xml 配置)
4.实际应用
4.1 spring-boot 2.1.4.RELEASE中使用 logback 作为日志框架, 实现告警日志打印 (打印成 json 格式)
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
application.yml
logging:
config: classpath:logback.xml
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- %m输出的信息,%p日志级别,%t线程名,%d日期,%c类的全名,%i索引【从数字0开始递增】,,, -->
<!-- appender是configuration的子节点,是负责写日志的组件。 -->
<!-- ConsoleAppender:把日志输出到控制台 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %p (%file:%line\)- %m%n</pattern>
<!-- 控制台也要使用UTF-8,不要使用GBK,否则会中文乱码 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->
<!-- 以下的大概意思是:1.先按日期存日志,日期变了,将前一天的日志文件名重命名为XXX%日期%索引,新的日志仍然是demo.log -->
<!-- 2.如果日期没有发生变化,但是当前日志的文件大小超过1KB时,对当前日志进行分割 重命名-->
<appender name="kafka_producer_log" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>log/kafka_producer_log.log</File>
<!-- rollingPolicy:当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名。 -->
<!-- TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 活动文件的名字会根据fileNamePattern的值,每隔一段时间改变一次 -->
<!-- 文件名:log/demo.2017-12-05.0.log -->
<fileNamePattern>log/kafka_producer_log.%d.%i.log</fileNamePattern>
<!-- 每产生一个日志文件,该日志文件的保存期限为30天 -->
<maxHistory>30</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- maxFileSize:这是活动文件的大小,默认值是10MB,测试时可改成1KB看效果 -->
<maxFileSize>1000MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<!-- pattern节点,用来设置日志的输入格式 -->
<pattern>
<!--%d %p (%file:%line\)- %m%n-->
%m%n
</pattern>
<!-- 记录日志的编码:此处设置字符集 - -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- ////////// 异步告警日志开始 ////////// -->
<appender name="alarm" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>log/alarm.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>log/alarm.%d.%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>1KB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<pattern>
%m%n
</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!--其次配置一个异步的 appender,并指向上面的 appender-->
<appender name="ALARM" class="ch.qos.logback.classic.AsyncAppender">
<!--内部实现是一个有界ArrayBlockingQueue,queueSize是队列大小。该值会影响性能.默认值为256-->
<queueSize>512</queueSize>
<!--当队列的剩余容量小于这个阈值并且当前日志level TRACE, DEBUG or INFO,则丢弃这些日志。默认为queueSize大小的20%。-->
<discardingThreshold>0</discardingThreshold>
<!--neverBlock=true则写日志队列时候会调用阻塞队列的offer方法而不是put,如果队列满则直接返回,而不是阻塞,即日志被丢弃。-->
<neverBlock>true</neverBlock>
<!--实际负责写日志的 appender, 最多只能添加一个-->
<appender-ref ref="alarm" />
</appender>
<logger name="alarm" level="WARN">
<appender-ref ref="ALARM"/>
</logger>
<!-- ////////// 异步告警日志结束 ////////// -->
<!-- 控制台输出日志级别 -->
<root level="warn">
<appender-ref ref="STDOUT" />
</root>
<!-- 指定项目中某个包,当有日志操作行为时的日志记录级别 -->
<!-- com.zy 为根包,也就是只要是发生在这个根包下面的所有日志操作行为的权限都是DEBUG -->
<!-- 级别依次为【从高到低】:FATAL > ERROR > WARN > INFO > DEBUG > TRACE -->
<logger name="com.zy" level="DEBUG">
<appender-ref ref="kafka_producer_log" />
</logger>
</configuration>
AlarmManager
package com.zy.alarm;
import com.alibaba.fastjson.JSON;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
public class AlarmManager {
/**
* 这里的 alarm 对应于 logback.xml 中 <logger name="alarm" level="WARN">
*/
private static final Logger alarmLogger = LoggerFactory.getLogger("alarm");
/**
* 打印告警日志
* @param alarmBean
*/
public static void alarm(AlarmBean alarmBean) {
Optional.ofNullable(alarmBean).ifPresent(alarmBean1 -> {
alarmBean.setAlarmType(AlarmType.ALARM.getType());
alarmBean.setAlarmBeginTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
alarmLogger.warn(JSON.toJSONString(alarmBean));
});
}
/**
* 解除告警
* @param alarmBean
*/
public static void fire(AlarmBean alarmBean) {
Optional.of(alarmBean).ifPresent(alarmBean1 -> {
alarmBean.setAlarmType(AlarmType.FIRE.getType());
alarmBean.setAlarmEndTime(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
alarmLogger.warn(JSON.toJSONString(alarmBean));
});
}
@AllArgsConstructor
@Getter
private enum AlarmType {
/**
* 告警中
*/
ALARM("alarm"),
/**
* 告警解除
*/
FIRE("fire"),
;
private String type;
}
}
AlarmBean
package com.zy.alarm;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class AlarmBean {
private Integer id;
private String name;
private String alarmType;
private String alarmBeginTime;
private String alarmEndTime;
public AlarmBean(Integer id, String name) {
this.id = id;
this.name = name;
}
}
AlarmController
package com.zy.controller;
import com.zy.alarm.AlarmBean;
import com.zy.alarm.AlarmManager;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AlarmController {
@RequestMapping("alarm")
public String alarm() {
System.out.println("开始---------");
try {
AlarmManager.alarm(new AlarmBean(1, "alarmName"));
System.out.println("结束--------");
return "success";
} catch (Exception e) {
e.printStackTrace();
}
return "failure";
}
}
参考资料