各日志框架配置原则: 先看官网 --> 再看源代码 --> 最后中文博客
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)
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)。
org.slf4j
slf4j-api
1.7.21
1.3.2 slf4j+logback
org.slf4j
slf4j-api
1.7.21
ch.qos.logback
logback-classic
1.1.7
1.3.3 slf4j+log4j
org.slf4j
slf4j-api
1.7.21
org.slf4j
slf4j-log4j12
1.7.21
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
org.springframework.boot
spring-boot-starter-web
application.yml
logging:
config: classpath:logback.xml
logback.xml
%d %p (%file:%line\)- %m%n
UTF-8
log/kafka_producer_log.log
log/kafka_producer_log.%d.%i.log
30
1000MB
%m%n
UTF-8
log/alarm.log
log/alarm.%d.%i.log
30
1KB
%m%n
UTF-8
512
0
true
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 中
*/
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";
}
}
参考资料