Java日志框架总结

日志的概念

日志文件是用于记录系统操作事件的文件集合,可分为事件日志消息日志

Java日志演进历史

print、alert、echo

互联网发展的早期,不管是C/S模式(客户端+服务端模式)还是B/S模式(浏览器+服务端模式),因为只有前端和后端交互这一层,验证逻辑基本上用的是前端alert,后台用System.out.print,服务器用echo命令回显。链路短,基本上够用。

JUL

Java Util Logging简称JUL,是JDK 中自带的log功能。虽然是官方自带的log lib,JUL的使用确不广泛。主要原因:

  • JUL从JDK1.4 才开始加入(2002年),当时各种第三方log lib已经被广泛使用了。Java Logging API提供了七个日志级别用来控制输出。这七个级别分别是:SEVEREWARNINGINFOCONFIGFINEFINERFINEST
  • JUL早期存在性能问题,到JDK1.5上才有了不错的进步,但现在和Logback/Log4j2相比还是有所不如。
  • JUL的功能不如Logback/Log4j2等完善,比如Output Handler就没有Logback/Log4j2的丰富,有时候需要自己来继承定制,又比如默认没有从ClassPath里加载配置文件的功能。

Log4j

Log4j 是在 Logback 出现之前被广泛使用的 Log Lib, 由 Gülcü 于2001年发布,后来成为Apache 基金会的顶级项目。Log4j 在设计上非常优秀,对后续的 Java Log 框架有长久而深远的影响,也产生了Log4c, Log4s, Log4perl 等到其他语言的移植。

通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。Log4j也有七种日志级别:OFF、FATAL、ERROR、WARN、INFO、DEBUG和TRACE。Log4j 的短板在于性能,在Logback 和 Log4j2 出来之后,Log4j的使用也减少了。

通过使用Log4j,可控制日志信息输送的目的地是控制台、文件、数据库等。也可以控制每一条日志的输出格式,通过定义每一条日志信息的级别,能够更加细致的控制日志的生成过程。Log4j支持两种格式的配置文件:properties和xml。包含三个主要的组件:LoggerappenderLayout

JCL

JCL是Jakarta Commons-Logging的缩写,Jakarta是一个早期的Apache开源项目,用于管理各个Java子项目,诸如Tomcat, Ant, Maven, Struts, JMeter, Velocity, JMeter, Commons等。2011年12月,在所有子项目都被迁移为独立项目后,Jakarta名称就不再使用了。

JCL诞生的初衷是因为Java自身的一些包用了JUL,而Log4j用户使用的有很多,那么JCL就是提供一套API来实现不同Logger之间的切换。JCL 是一个Log Facade,只提供 Log API,不提供实现,然后有 Adapter 来使用 Log4j 或者 JUL 作为Log Implementation。在程序中日志创建和记录都是用JCL中的接口,在真正运行时,会看当前ClassPath中有什么实现,如果有Log4j 就是用 Log4j, 如果啥都没有就是用 JDK 的 JUL。

这样,在你的项目中,还有第三方的项目中,大家记录日志都使用 JCL 的接口,然后最终运行程序时,可以按照自己的需求(或者喜好)来选择使用合适的Log Implementation。如果用Log4j, 就添加 Log4j 的jar包进去,然后写一个 Log4j 的配置文件;如果喜欢用JUL,就只需要写个 JUL 的配置文件。如果有其他的新的日志库出现,也只需要它提供一个Adapter,运行的时候把这个日志库的 jar 包加进去。

image-20210304221941184

Logback

SLF4J(The Simple Logging Facade for Java) 和 Logback 也是Gülcü创立的项目,其创立主要是为了提供更高性能的实现。其中,SLF4j 是类似于JCL 的Log Facade,Logback 是类似于Log4j 的 Log Implementation。

Gülcü 认为 JCL 的 API 设计得不好,容易让使用者写出性能有问题的代码:

logger.debug("start process request, url:" + url);

一般生产环境 log 级别都会设到 info 或者以上,那这条 log 是不会被输出的。然而不管会不会输出,这其中都会做一个字符串连接操作,然后生产一个新的字符串。如果这条语句在循环或者被调用很多次的函数中,就会多做很多无用的字符串连接,影响性能。

所以 JCL 的最佳实践推荐这么写:

if (logger.isDebugEnabled()) {
    logger.debug("start process request, url:" + url);
}

然而开发者常常忽略这个问题或是觉得麻烦而不愿意这么写。所以SLF4J提供了新的API,方便开发者使用:

logger.debug(“start process request, url:{}”, url);

这样的话,在不输出 log 的时候避免了字符串拼接的开销;在输出的时候需要做一个字符串format,代价比手工拼接字符串大一些,但是可以接受。

而 Logback 则是作为 Log4j 的继承者来开发的,提供了性能更好的实现,异步 logger,Filter等更多的特性。

Logback有三个模块:

  • logback-core:日志处理核心组件
  • logback-classic:完整的实现了SLF4j API,用于切换日志实现。
  • logback-access:与Servlet容器集成提供通过http来访问日志的功能。

因为logback比log4j大约快10倍、消耗更少的内存,迁移成本也很低,自动压缩日志、支持多样化配置、不需要重启就可以恢复I/O异常等优势,又名噪一时。

SLF4J

我们有了两个流行的 Log Facade,以及三个流行的 Log Implementation。Gülcü 是个追求完美的人,他决定让这些Log之间都能够方便的互相替换,所以做了各种 Adapter 和 Bridge 来连接:

image-20210304222706928

Log4j2

维护 Log4j 的人不这样想,他们不想坐视用户一点点被 SLF4J /Logback 蚕食,继而搞出了 Log4j2。

Log4j2 和 Log4j1.x 并不兼容,设计上很大程度上模仿了 SLF4J/Logback,性能上也获得了很大的提升。Log4j2与Logback非常类似,但是它有自己的亮点:如插件式结构、配置文件优化、异步日志。

Log4j2 也做了 Facade/Implementation 分离的设计,分成了 log4j-api 和 log4j-core。Log4j2是Log4j的升级,它比其前身Log4j 1.x提供了重大改进,并提供了Logback中可用的许多改进,同时修复了Logback架构中的一些固有问题。

从GitHub的更新日志来看,Logback已经有半年没有更新了,而作为知名组织的Apache下的Log4j2的更新却是非常活跃的,Log4j 1.x 于2015年8月停止维护更新了。

image-20210304223150126

历史顺序

image-20210312162709543

JUL

JUL全称Java util Logging是java原生的日志框架,使用时不需要另外引用第三方类库,相对其他日志框架使用方便,学习简单,能够在小型应用中灵活使用。

简单使用

image-20210306153440837

用户使用Logger来进行日志记录,Logger持有若干个Handler,日志的输出操作是由Handler(也称作Appender)完成的。在Handler在输出日志前,会经过Filter的过滤,判断哪些日志级别过滤放行哪些拦截,Handler会将日志内容输出到指定位置(日志文件、控制台等)。Handler在输出日志时会使用Layout,将输出内容进行排版。

@Test
public void testQuick() {
    // 获取日志记录器
    Logger logger = Logger.getLogger("com.yangsx95.notes.javalog.JULTests");
    // 日志记录输出
    logger.info("Hello JUL!");

    // 通用方法进行日志记录
    logger.log(Level.INFO, "Hello JUL!");

    // 通过占位符方法输出变量值
    logger.log(Level.INFO, "用户信息 {0}, {1}", new Object[]{"张三", 11});
}

日志级别

log.log("错误");
log.log("普通信息");
log.log("调试信息");

为了方便区分日志消息的重要程度,日志框架将消息分为了不同的级别,并通过更改日志记录器的日志级别,来控制不同级别的日志消息的输出

JUL 的日志级别在java.util.loggin.Level枚举中定义(从高到低), 每个级别对应一个Integer值:

// 表示严重故障的消息级别。SEVERE消息应描述非常重要的事件,这些事件将阻止正常的程序执行
public static final Level SEVERE = new Level("SEVERE",1000, defaultBundle);
// 潜在故障的消息级别,需要特殊关注的日志消息。
public static final Level WARNING = new Level("WARNING", 900, defaultBundle);
// 信息性消息,info信息应是对管理员有意义的重要信息。
public static final Level INFO = new Level("INFO", 800, defaultBundle);
// 静态配置消息,提供各种静态配置信息,以帮助调试可能与特定配置关联的问题
public static final Level CONFIG = new Level("CONFIG", 700, defaultBundle);
// 跟踪信息消息级别,DEBUG
public static final Level FINE = new Level("FINE", 500, defaultBundle);
// 详细跟踪信息,DEBUG
public static final Level FINER = new Level("FINER", 400, defaultBundle);
// 高度详细跟踪信息,DEBUG
public static final Level FINEST = new Level("FINEST", 300, defaultBundle);

// 日志开关:关闭日志记录的特殊日志级别
public static final Level OFF = new Level("OFF",Integer.MAX_VALUE, defaultBundle);
// 日志开关:打开所有等级日志记录的特殊日志级别
public static final Level ALL = new Level("ALL", Integer.MIN_VALUE, defaultBundle);

默认的日志输出级别为Info, 小于日志记录器设置的级别的日志,都不会被输出:

@Test
public void testLevel() {
    Logger logger = Logger.getLogger(JULTests.class.getName());

    logger.severe("severe"); // 输出
    logger.warning("warning"); // 输出
    logger.info("info"); // 输出
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");

    // JUL 默认的日志级别为info,比info高的级别将会被输出,低得级别将会隐藏
}

更改日志记录器的日志级别:

/**
 * 更改日志记录器的日志级别
 */
@Test
public void testModifyLoggerLevel() {
    Logger logger = Logger.getLogger(JULTests.class.getName());
    logger.setUseParentHandlers(false); // 关闭系统默认配置
    ConsoleHandler consoleHandler = new ConsoleHandler(); // 创建自己的Handler
    consoleHandler.setFormatter(new SimpleFormatter());
    consoleHandler.setLevel(Level.ALL);
    logger.addHandler(consoleHandler);
    logger.setLevel(Level.ALL); //打开所有日志

    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}

Logger的父子关系

JUL中Logger之间存在父子关系,这种父子关系通过树状结构存储,JUL在初始化时会创建一个顶层RootLogger作为所有Logger父Logger,存储上作为树状结构的根节点。并父子关系通过路径来关联。

  • com.testcom.test.aaa的父logger
  • 可以通过logger.getParent()获取父logger
  • 根节点为rootLogger,他的日志级别为INFO,默认有一个ConsoleHandler
  • 新创建的Logger的Level以及Handler配置都为空
  • 子logger在调用时,如果没有配置,会使用父logger的配置,如果父logger也没有会继续向上查找,直到RootLogger
Logger logger = Logger.getLogger("com.yangsx95.notes.javalog.jul");
// logger的父logger
Logger parentLogger = Logger.getLogger("com.yangsx95.notes.javalog");
// 根logger
Logger rootLogger = parentLogger.getParent();

System.out.println(logger.getParent() == parentLogger);
System.out.println(parentLogger.getParent() == rootLogger);

Logger的父子关系完全由包路径关联,给Logger贴上父子关系,不仅可以对指定的包进行范围处理,而且这样也避免了为每一个Logger进行配置。

配置文件

JUL也支持properties配hi文件,用来替代硬编码的Logger配置,其日志配置读取流程如下(参见LogManager#readConfiguration()方法):

  1. 读取java.util.logging.config.class变量,找寻Java配置类
  2. 读取java.util.logging.config.file变量, 在此路径中找寻Java配置文件:logging.properties
  3. 读取$JAVAHOME\jre\lib\logging.properties下的配置文件。默认的logger配置就是由该配置文件初始化的

自定义logging.properties:

# RootLogger 顶级父元素指定的默认处理器为:ConsoleHandler
handlers=java.util.logging.FileHandler
# RootLogger 顶级父元素默认的日志级别为:ALL
.level=ALL
# 自定义 Logger 使用
com.yangsx95.notes.javalog.jul.handlers=java.util.logging.ConsoleHandler
com.yangsx95.notes.javalog.jul.level=CONFIG
# 关闭默认配置
com.itheima.useParentHanlders=false
# 向日志文件输出的 handler 对象
# 指定日志文件路径 /logs/java0.log
java.util.logging.FileHandler.pattern=%h/java%u.log
# 指定日志文件内容大小
java.util.logging.FileHandler.limit=50000
# 指定日志文件数量
java.util.logging.FileHandler.count=1
# 指定 handler 对象日志消息格式对象
java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter
# 指定以追加方式添加日志内容
java.util.logging.FileHandler.append=true
# 向控制台输出的 handler 对象
# 指定 handler 对象的日志级别
java.util.logging.ConsoleHandler.level=ALL
# 指定 handler 对象的日志消息格式对象
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
# 指定 handler 对象的字符集
java.util.logging.ConsoleHandler.encoding=UTF-8
# 指定日志消息格式
java.util.logging.SimpleFormatter.format=%4$s: %5$s [%1$tc]%n
// 读取配置文件
InputStream in = JULTests.class.getClassLoader().getResourceAsStream("logging.properties");
// 获取logManager
LogManager logManager = LogManager.getLogManager();
// 加载配置
logManager.readConfiguration(in);

基本原理

  1. 初始化LogManager

    1. LogManager加载logging.properties配置
    2. 添加Logger到LogManager
  2. 从单例LogManager获取Logger

  3. 设置级别Level,并指定日志记录LogRecord

  4. Filter提供了日志级别之外更细粒度的控制

  5. Handler是用来处理日志输出位置

  6. Formatter是用来格式化LogRecord的

image-20210307093757064

Log4j

Log4j是Apache下的一款开源的日志框架,通过在项目中使用 Log4J,我们可以控制日志信息输出到控制台、文件、甚至是数据库中。我们可以控制每一条日志的输出格式,通过定义日志的输出级别,可以更灵活的控制日志的输出过程。方便项目的调试。

官方网站http://logging.apache.org/

简单使用

@Test
public void testQuick() {
    // 初始化一个基本配置,从而不需要配置文件
    BasicConfigurator.configure();
    // 创建日志记录器
    Logger logger = Logger.getLogger(Log4jTests.class);

    // 日志输出,共有6种日志级别
    logger.fatal("fatal"); // 严重错误,一般会造成系统崩溃和终止运行
    logger.error("error"); // 错误信息,但不会影响系统运行
    logger.warn("warn");// 警告信息,可能会发生问题
    logger.info("info");// 程序运行信息,数据库的连接、网络、IO操作等
    logger.debug("debug");// 调试信息,一般在开发阶段使用,记录程序的变量、参 数等
    logger.trace("trace");// 追踪信息,记录程序的所有流程信息

    // log4j的默认日志级别为debug
}

日志级别

在类org.apache.log4j.Level类中,定义了log4j的日志级别,每个级别对应一个int数值:

//fatal  指出每个严重的错误事件将会导致应用程序的退出。
public static final Level FATAL = new Level(50000, "FATAL", 0);
//error 指出虽然发生错误事件,但仍然不影响系统的继续运行。
public static final Level ERROR = new Level(40000, "ERROR", 3);
//warn 表明会出现潜在的错误情形。
public static final Level WARN = new Level(30000, "WARN", 4);
//info 一般和在粗粒度级别上,强调应用程序的运行全程。
public static final Level INFO = new Level(20000, "INFO", 6);
//debug 一般用于细粒度级别上,对调试应用程序非常有帮助。
public static final Level DEBUG = new Level(10000, "DEBUG", 7);
//trace 是程序追踪,可以用于输出程序运行中的变量,显示执行的流程。
public static final Level TRACE = new Level(5000, "TRACE", 7);

// 此外,还包含两个特殊级别
public static final Level OFF = new Level(2147483647, "OFF", 0); // 可用来关闭日志记录。
public static final Level ALL = new Level(-2147483648, "ALL", 7); // 启用所有消息的日志记录

Level类继承自org.apache.log4j.Priority类,该类已经不推荐使用

Log4j组件

Log4J 主要由

  • Loggers (日志记录器):控制日志的输出级别与日志是否输出
  • Appenders(输出端):指定日志的输出方式(输出到控制台、文件等)
  • Layout(日志格式化器):Layout 控制日志信息的输出格式

组成。

Loggers

日志记录器,负责收集处理日志记录,实例的命名就是类“XX”的full quailied name(类的全限定名),Logger的名字大小写敏感,其命名有继承机制:例如:name为org.apache.commons的logger会继承name为org.apache的logger。

Log4J中有一个特殊的logger叫做“root”,他是所有logger的根,也就意味着其他所有的logger都会直接或者间接地继承自root。root logger可以用Logger.getRootLogger()方法获取。

早期loggers是categories的别名

Appenders

Appender 用来指定日志输出到哪个地方,可以同时指定日志的输出目的地。Log4j 常用的输出目的地有以下几种:

输出端类型作用
ConsoleAppender将日志输出到控制台
FileAppender将日志输出到文件中
DailyRollingFileAppender将日志输出到一个日志文件,并且每天输出到一个新的文件
RollingFileAppender将日志信息输出到一个日志文件,并且指定文件的尺寸,当文件大小达到指定尺寸时,会自动把文件改名,同时产生一个新的文件
JDBCAppender把日志信息保存到数据库中

Layouts

布局器 Layouts用于控制日志输出内容的格式,让我们可以使用各种需要的格式输出日志。Log4j常用的Layouts:

格式化类型作用
HTMLLayout格式化日志输出为HTML表格形式
HTMLLayout简单的日志输出格式化,打印的日志格式为(info – message)
PatternLayout最强大的格式化期,可以根据自定义格式输出日志,如果没有指定转换格式,就使用默认的转换格式
* log4j 采用类似 C 语言的 printf 函数的打印格式格式化日志信息,具体的占位符及其含义如下: 
    %m 输出代码中指定的日志信息 
    %p 输出优先级,及 DEBUG、INFO 等 
    %n 换行符(Windows平台的换行符为 "\n",Unix 平台为 "\n") 
    %r 输出自应用启动到输出该 log 信息耗费的毫秒数 
    %c 输出打印语句所属的类的全名 
    %t 输出产生该日志的线程全名 
    %d 输出服务器当前时间,默认为 ISO8601,也可以指定格式,如:%d{yyyy年MM月dd日 HH:mm:ss}
    %l 输出日志时间发生的位置,包括类名、线程、及在代码中的行数。如: Test.main(Test.java:10) 
    %F 输出日志消息产生时所在的文件名称 
    %L 输出代码中的行号 
    %% 输出一个 "%" 字符 
* 可以在 % 与字符之间加上修饰符来控制最小宽度、最大宽度和文本的对其方式。如: 
    %5c 输出category名称,最小宽度是5,category<5,默认的情况下右对齐 
    %-5c 输出category名称,最小宽度是5,category<5,"-"号指定左对齐,会有空格 
    %.5c 输出category名称,最大宽度是5,category>5,就会将左边多出的字符截掉,<5不会有空格 
    %20.30c category名称<20补空格,并且右对齐,>30字符,就从左边交远销出的字符截掉

配置文件

配置文件加载顺序(参考LogManager静态代码块):

  • OptionConverter.getSystemProperty("log4j.configuration", (String)null);
    • 指定的位置的配置文件
    • 可以通过 java -Dlog4j.configuration=file:/full_path/log4j.properties指定
    • 代码中可以使用PropertyConfigurator.configure(param);来指定。
  • 从classpath下寻找log4j.xml
  • 从classpath下寻找log4j.properties
# 指定 RootLogger 顶级父元素默认配置信息
# 指定日志级别=trace,使用的 apeender 为=console
log4j.rootLogger = trace,console

# 自定义 logger 对象设置
log4j.logger.com.yangsx95.notes = info,console
log4j.logger.org.apache = error

# 指定控制台日志输出的 appender
log4j.appender.console = org.apache.log4j.ConsoleAppender
# 指定消息格式 layout
log4j.appender.console.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.console.layout.conversionPattern = [%-10p]%r  %l %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n

# 日志文件输出的 appender 对象
log4j.appender.file = org.apache.log4j.FileAppender
# 指定消息格式 layout
log4j.appender.file.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.file.layout.conversionPattern = [%-10p]%r  %l %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n
# 指定日志文件保存路径
log4j.appender.file.file = /logs/log4j.log
# 指定日志文件的字符集
log4j.appender.file.encoding = UTF-8

# 按照文件大小拆分的 appender 对象
# 日志文件输出的 appender 对象
log4j.appender.rollingFile = org.apache.log4j.RollingFileAppender
# 指定消息格式 layout
log4j.appender.rollingFile.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.rollingFile.layout.conversionPattern = [%-10p]%r  %l %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n
# 指定日志文件保存路径
log4j.appender.rollingFile.file = /logs/log4j.log
# 指定日志文件的字符集
log4j.appender.rollingFile.encoding = UTF-8
# 指定日志文件内容的大小
log4j.appender.rollingFile.maxFileSize = 1MB
# 指定日志文件的数量
log4j.appender.rollingFile.maxBackupIndex = 10

# 按照时间规则拆分的 appender 对象
log4j.appender.dailyFile = org.apache.log4j.DailyRollingFileAppender
# 指定消息格式 layout
log4j.appender.dailyFile.layout = org.apache.log4j.PatternLayout
# 指定消息格式的内容
log4j.appender.dailyFile.layout.conversionPattern = [%-10p]%r  %l %d{yyyy-MM-dd HH:mm:ss.SSS} %m%n
# 指定日志文件保存路径
log4j.appender.dailyFile.file = /logs/log4j.log
# 指定日志文件的字符集
log4j.appender.dailyFile.encoding = UTF-8
# 指定日期拆分规则
log4j.appender.dailyFile.datePattern = '.'yyyy-MM-dd-HH-mm-ss

日志门面

当我们的系统变的更加复杂的时候,我们的日志就容易发生混乱。随着系统开发的进行,可能会更新不同的日志框架,造成当前系统中存在不同的日志依赖,让我们难以统一的管理和控制。就算我们强制要求所有的模块使用相同的日志框架,系统中也难以避免使用其他类似spring,mybatis等其他的第三方框架,它们依赖于我们规定不同的日志框架,而且他们自身的日志系统就有着不一致性,依然会出来日志体系的混乱。

所以我们需要借鉴JDBC的思想,为日志系统也提供一套门面,那么我们就可以面向这些接口规范来开发,避免了直接依赖具体的日志框架。这样我们的系统在日志中,就存在了日志的门面和日志的实现。

JCL

全称为Jakarta Commons Logging,是Apache提供的一个通用日志API。它是为 "所有的Java日志实现"提供一个统一的接口,它自身也提供一个日志的实现,但是功能非常常弱(SimpleLog)。所以一般不会单独使用它。他允许开发人员使用不同的具体日志实现工具: Log4j, Jdk

JCL 有两个基本的抽象类:

  • Log(基本记录器)
  • LogFactory(负责创建Log实例)

使用日志门面的优点:

  • 面向接口开发,不再依赖具体的实现类。减少代码的耦合
  • 项目通过导入不同的日志实现类,可以灵活的切换日志框架
  • 统一API,方便开发者学习和使用
  • 统一配置便于项目日志的管理

image-20210312153213417

简单使用

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1.1</version>
</dependency>
@Test
public void testQuick() {
    Log log = LogFactory.getLog(JCLTest.class);
    log.fatal("fatal");
    log.error("error");
    log.warn("warn");
    log.info("info");
    log.debug("debug");
}

实现原理

不同的日志实现框架,对于Log来说,就拥有不同的Log实现。LoggerFactory根据顺序一次查找实现,加载不同的Log对象。

image-20210312154904727

日志门面支持的日志实现数组:

private static final String[] classesToDiscover = new String[]  {
                "org.apache.commons.logging.impl.Log4JLogger",
                "org.apache.commons.logging.impl.Jdk14Logger",
                "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
                "org.apache.commons.logging.impl.SimpleLog"
        };

LogFactory在初始化时,会通过如下代码获取日志实现,Log4j优先级最高:

for(int i = 0; i < classesToDiscover.length && result == null; ++i) {
        result = this.createLogFromClass(classesToDiscover[i], logCategory, true); 
}

SLF4J

简单日志门面(Simple Logging Facade For Java) SLF4J主要是为了给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等。当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到。对于一般的Java项目而言,日志框架会选择slf4j-api作为门面,配上具体的实现框架(log4j、logback等),中间使用桥接器完成桥接。

https://www.slf4j.org


SLF4J提供两大功能:

  1. 日志框架绑定
  2. 日志框架桥接

基本使用

<!-- 添加日志门面 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>

<!--绑定日志实现:slf4j内置简单实现-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.21</version>
</dependency>
@Test
public void testQuick() {
    Logger logger = LoggerFactory.getLogger(Slf4JTest.class);
    logger.error("error");
    logger.warn("warn");
    logger.info("info");
    logger.debug("debug");
    logger.trace("trace");

    int age = 10;
    String name = "张三";
    logger.info("姓名:{}, 年龄:{}", name, age);

    // 输出系统异常信息
    try {
        int a = 10 / 0;
    } catch (Exception e) {
        logger.error("不能除以0", e);
    }
}

为什么使用SLF4J

  1. 使用SLF4J框架,可以在部署时迁移到所需的日志记录框架。
  2. SLF4J提供了对所有流行的日志框架的绑定,例如log4j,JUL,Simple logging和NOP。因此可以 在部署时切换到任何这些流行的框架。
  3. JCL只支持 JUL、Logback、SimpleLog几种日志实现
  4. 无论使用哪种绑定,SLF4J都支持参数化日志记录消息。由于SLF4J将应用程序和日志记录框架分离, 因此可以轻松编写独立于日志记录框架的应用程序。而无需担心用于编写应用程序的日志记录框架。
  5. SLF4J提供了一个简单的Java工具,称为迁移器。使用此工具,可以迁移现有项目,这些项目使用日志框架(如Jakarta Commons Logging(JCL)或log4j或Java.util.logging(JUL))到SLF4J。

SLF4J绑定日志实现

image-20210312165600491

  1. 添加slf4j-api依赖
  2. 使用slf4j的API在项目中进行统一的日志记录
  3. 绑定具体的日志实现框架
    1. 绑定已经实现了slf4j的日志框架,直接添加对应依赖
    2. 绑定没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
  4. slf4j有且仅有一个日志实现框架的绑定(如果出现多个默认使用第一个依赖日志实现,并且在日志中显示警告信息)

桥接旧的日志框架

如果项目中存在其他日志框架API,可以通过如下几个步骤替换为SLF4j:

  1. 去除之前来的日志依赖
  2. 添加SLF4J提供的桥接组件(比如如果是JCL里面就包含JCL的API,这样代码就不用做变动)
  3. 为项目添加SLF4J的具体实现

image-20210313125617540

日志绑定原理

  1. SLF4J通过LoggerFactory加载日志具体的实现对象。
  2. LoggerFactory在初始化的过程中,会通过performInitialization()方法绑定具体的日志实现。
  3. 在绑定具体实现的时候,通过类加载器,加载org/slf4j/impl/StaticLoggerBinder.class(该类由日志实现框架提供)
  4. 该类StaticLoggerBinder中存在一个ILoggerFactory getLoggerFactory()的方法,用来返回具体日志实现的ILoggerFactory对象。
  5. 所以,只要是一个日志实现框架,在org.slf4j.impl包中提供一个自己的StaticLoggerBinder类,在其中提供具体日志实现的LoggerFactory就可以被SLF4J所加载

Logback

Logback是由log4j创始人设计的另一个开源日志组件,性能比log4j要好。

https://logback.qos.ch/index.html

Logback主要分为三个模块:

  • logback-core:其它两个模块的基础模块
  • logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API
  • logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能

简单使用

同SLF4J,默认级别为debug:

<!-- 可选,由classic传递依赖 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.30</version>
</dependency>
<!-- 可选,由classic传递依赖 -->
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.2.3</version>
</dependency>

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
@Test
public void testQuick() {
    Logger logger = LoggerFactory.getLogger(LogbackTests.class);
    logger.error("error");
    logger.warn("warn");
    logger.info("info");
    logger.debug("debug");
    logger.trace("trace");
}

logback组件

  1. Logger:日志的记录器,把它关联到应用的对应的context上后,主要用于存放日志对象,也可以定义日志类型、级别。

  2. Appender:用于指定日志输出的目的地,目的地可以是控制台、文件、数据库等等。

  3. Layout:负责把事件转换成字符串,格式化的日志信息的输出。在logback中Layout对象被封装在encoder中。

配置文件

logback会依次从classpath路径下读取以下配置文件:

  1. logback.groovy
  2. logback-test.xml
  3. logback.xml
  4. 如果均不存在,则会采用默认配置
<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <!--
        集中管理属性 property,可以使用 ${name}获取集中管理的属性
    -->

    <!--日志输出格式:
        %-5level 日志级别,五个字符宽度,负号代表文字靠左,如果不足五个右侧补充空格
        %d{yyyy-MM-dd HH:mm:ss.SSS} 日期
        %c类的完整名称
        %M为method
        %L为行号
        %thread线程名称
        %m或者%msg为日志消息
        %n换行
    -->
    <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %c %M %L [%thread] %msg%n"/>

    <!--
        定义 Appender,决定日志输出的位置
    -->

    <!-- 控制台Appender 对象的xml参数都是class对象的setter方法 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!--控制台输出流对象, 默认为System.out,可以更改为System.error-->
        <target>System.err</target>
        <!--日志消息格式-->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
        </encoder>
    </appender>

    <!--
        定义并配置logger
    -->

    <!--根日志记录器,级别为ALL,appender为控制台输出-->
    <root level="ALL">
        <appender-ref ref="console"/>
    </root>
</configuration>

文件日志

<!--日志输出格式:html使用,去除多余的空格-->
<property name="html_pattern" value="%level%d{yyyy-MM-dd HH:mm:ss.SSS}%c%M%L%thread%msg"/>

<!-- 文件Appender -->
<appender name="file" class="ch.qos.logback.core.FileAppender">
    <!--日志文件保存路径-->
    <file>${log_dir}/logback.log</file>
    <!--日志消息格式-->
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>${pattern}</pattern>
    </encoder>
</appender>

<!-- html文件Appender -->
<appender name="html_file" class="ch.qos.logback.core.FileAppender">
    <!--日志文件保存路径-->
    <file>${log_dir}/logback.html</file>
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
        <layout class="ch.qos.logback.classic.html.HTMLLayout">
            <pattern>${html_pattern}</pattern>
        </layout>
    </encoder>
</appender>

日志拆分与归档压缩

<!-- 日志拆分与归档压缩 -->
<appender name="roll_file" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <!--日志文件保存路径-->
    <file>${log_dir}/rollingFile.log</file>
    <!--日志消息格式-->
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>${pattern}</pattern>
    </encoder>
    <!--指定拆分规则 SizeAndTimeBasedRollingPolicy -->
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <!--按照时间与压缩格式声明拆分的文件名-->
        <fileNamePattern>${log_dir}/rolling.%d{yyyy-MM-dd-HH-mm-ss}.log%i.gz</fileNamePattern>
        <!--按照文件大小拆分-->
        <maxFileSize>1MB</maxFileSize>
    </rollingPolicy>
</appender>
  • 每隔一秒钟就是一个新的日志文件
  • 当在这一秒钟内日志大小超过1MB,将会重新拆分一个新的日志文件(%i代表滚动次数)

过滤器

Appender中可以指定Filter,用于细粒度过滤日志。

日志级别过滤器:过滤单一级别

<!-- 控制台Appender 对象的xml参数都是class对象的setter方法 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    <!--控制台输出流对象, 默认为System.out,可以更改为System.error-->
    <target>System.err</target>
    <!--日志消息格式-->
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>${pattern}</pattern>
    </encoder>
    <!--filter日志过滤,日志级别过滤-->
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
        <!--过滤规则,只保留error信息-->
        <level>error</level>
        <onMatch>ACCEPT</onMatch>
        <onMismatch>DENY</onMismatch>
    </filter>
</appender>

日志级别过滤器:过滤高于某个级别的日志

<!-- 文件Appender -->
<appender name="file" class="ch.qos.logback.core.FileAppender">
    <!--日志文件保存路径-->
    <file>${log_dir}/logback.log</file>
    <!--日志消息格式-->
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>${pattern}</pattern>
    </encoder>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
        <!--过滤高于info级别的日志,包含info-->
        <level>info</level>
    </filter>
</appender>

异步日志记录

可以提高性能 。

<!--异步日志appender-->
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref>roll_file</appender-ref>
</appender>

AsyncAppender中包含一个BloackingQueue对象,是负责接受日志消息,Appender会从队列中异步取出消息然后执行输出。2

自定义Logger

<!--自定义logger, additivity false代表不会从父logger继承appender-->
<logger name="com.yangsx95.notes.test" level="ERROR" additivity="false">
    <appender-ref ref="console"/>
    <appender-ref ref="file"/>
</logger>

自定义属性

package com.yangsx95.notes.javalog.logback;

import ch.qos.logback.core.PropertyDefinerBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.UUID;

/**
 * 自定义属性,获取当前服务器ip地址
 */
public class IPLogDefiner extends PropertyDefinerBase {

    private static final Logger LOG = LoggerFactory.getLogger(IPLogDefiner.class);

    private String getUniqName() {
        String localIp = null;
        try {
            localIp = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            LOG.error("fail to get ip...", e);
        }
        String uniqName = UUID.randomUUID().toString().replace("-", "");
        if (localIp != null) {
            uniqName = localIp + "-" + uniqName;
        }
        return uniqName;
    }

    @Override
    public String getPropertyValue() {
        return getUniqName();
    }

}

在配置文件中声明属性,并使用属性:

<!--自定义属性, 使用${ip}使用-->
<define name="ip" class="com.yangsx95.notes.javalog.logback.IPLogDefiner"/>

配置日志颜色

如何挑选日志框架

  1. 使用Log Facade,而不是具体Log Implementation。 这样可以方便的切换具体的日志实现。推荐使用Log4j-API 和 SLF4j, 不推荐使用JCL。

  2. 只添加一个 Log Implementation依赖

  3. 依赖推荐配置

    
         
         
       
          
          
           
           org.apache.logging.log4j
          
          
       
          
          
           
           log4j-core
          
          
       
          
          
           
           ${log4j.version}
          
          
       
          
          
           
           runtime
          
          
          
          
       
          
          
           
           true
          
           
          
          
    
         
         
    
         
         
       
          
          
           
           org.apache.logging.log4j
          
          
       
          
          
           
           log4j-slf4j-impl
          
          
       
          
          
           
           ${log4j.version}
          
          
       
          
          
           
           runtime
          
          
       
          
          
           
           true
          
          
    
         
         
  4. 使用exclude排除第三方框架中的 Log Implementation:第三方库的开发者未必会把具体的日志实现或者桥接器的依赖设置为optional,然后你的项目继承了这些依赖——具体的日志实现未必是你想使用的。

  5. 如果是小项目,可以使用JUL作为日志框架,无需以来jar包。当项目迭代增大,可以从JUL向Slf4J迁移,日志实现推荐是用Logback以及log4j2,其他不建议使用,已过时。

怎么打印日志

why to log

  • 记录代码执行流程

  • 处理逻辑时序和问题发生节点

  • 准确定位并解决处理问题

  • 提供报警机制信息

when to log

在什么情况下需要进行打印日志:

  1. 调试日志:目的是开发期调试程序使用,这种日志量比较大,且没有什么实质性的意义,只应该出现在开发期,而不应该在项目上线之后输出。

  2. 用户行为日志:记录用户的操作行为,用于大数据分析,比如监控、风控、推荐等等。这种日志,一般是给其他团队分析使用,而且可能是多个团队,因此一般会有一定的格式要求,开发者应该按照这个格式来记录,便于其他团队的使用。当然,要记录哪些行为、操作,一般也是约定好的,因此,开发者主要是执行的角色。这种日志,又叫事件上报,或埋点。

  3. 程序运行日志:记录程序的运行状况,特别是非预期的行为、异常情况,这种日志,主要是给开发、维护人员使用。什么时候记录,记录什么内容,完全取决于开发人员,开发者具有高度自主性。

  4. 记录系统或者机器的状态:比如网络请求、系统CPU、内存、IO使用情况等等,这种日志主要是给运维人员使用,生成各种更直观的展现形式,在系统出问题的时候报警。

what to log

一条有用的日志信息应该包含的元素:

  • when: the time event happens,事件发生的时间,注意不是日志最终打印的时间

  • where: where the event happens,时间产生的地点,也就是具体到哪个模块、哪个文件、类、方法,甚至是某一行

  • level:how importance of the event,每一条日志都应该有log level,log level代表了日志的重要性、紧急程度。例如:debug,info,warn,error,fatal(critical)。调试日志是最不重要的,是不应该出现在线上项目的,但是程序运行报错日志却需要认真对待,因为代表程序已经出现了异常;如果是fatal日志,即使是在大半夜,也得立刻起来分析、处理。

  • who:the uniq identify,代表了事件产生者的唯一标识(identity),用于区分同样的事件

    logger.warn("user_login failed due to password"); // 正确
    logger.warn("user {} login failed due to password", userId);  // 错误
  • what:what is the log message,日志的主体内容,应该简明扼要的描述发生的什么事情。要求可以通过日志本身,而不是重新阅读产生日志的代码,来大致搞清楚发生了什么事情。

    logger.error("发生错误"); // 错误
    logger.error("获取订单列表失败", e); // 正确
  • context: environment when event happens:告知事件是在什么样的情况发生的,专指高度依赖于具体的日志内容的信息,这些信息,是用于定位问题的具体原因。需要结合具体项目思考加上什么内容能定位到问题发生的原因。

Tips

日志文件名

日志文件名可以通过一下几个关键信息进行区分:

  • 类型标识(logTypeName):

    指此日志文件的功能或者用途,比如一个web服务,记录http请求的日志通常命名为request.log或者access.log,request、access就是类型标识,而java的gc日志通常命名为gc.log,这样看一目了然;而通常用来记录服务的整体运行的日志一般用服务名称(serviceName、appKey)或者机器名(hostName)来命名,如 nginx.log;

  • 日志级别(logLevel)

    打印日志的时候直接通过文件来区分级别是一种比较推荐的方式,如果把所有级别的日志打到同一个日志文件中,在定位问题时,还需要去文件中进行查找操作,相对繁琐。日志级别一般包括DEBUG、INFO、WARN、ERROR、FATAL这五个级别,在实际编写代码中,可以采取严格匹配模式或者非严格匹配模式,严格匹配模式即INFO日志文件中只打印INFO日志,ERROR日志文件只打印ERROR日志;非严格匹配模式即INFO日志文件可以打印INFO日志、WARN日志、ERROR日志、FATAL日志,WARN日志文件可以打印WARN日志、ERROR日志、FATAL日志,以此类推

  • 日志生成时间(logCreateTime)

    即在日志文件名称中附带上日志文件创建的时间,方便在查找日志文件时进行排序;

  • 日志备份编号(logBackupNum)

    当进行日志切割时,如果是以文件大小进行滚动,此时可以在日志文件名称末尾加上编号;

日志滚动

  • 按照时间滚动

    按照时间滚动,即每隔一定的时间建立一个新的日志文件,通常可以按照小时级别滚动或者天级别滚动,具体采取哪种方式取决于系统日志的打印量。如果系统日志比较少,可以采取天级别滚动;而如果系统日常量比较大,则建议采取小时级别滚动

  • 按照单个日志文件大小滚动

    按照单个日志文件大小滚动,即每当日志文件达到一定大小则建立一个新的日志文件,通常建议单个日志文件大小不要超过500M,日志文件过大的话,对于日志监控或者问题定位排查都可能会造成一定影响。

  • 同时按照时间和单个日志文件大小滚动

    按照时间和单个日志文件大小滚动,这种模式通常适用于希望保留一定时间的日志,但是又不希望单个日志文件过大的场景。

日志打印的时机

  1. http调用或者rpc接口调用

    在程序调用其他服务或者系统的时候,需要打印接口调用参数和调用结果(成功/失败)。

  2. 程序异常

    在程序出现exception的时候,要么选择向上抛出异常,要么必须在catch块中打印异常堆栈信息。不过需要注意的是,最好不要重复打印异常日志,比如在catch块里既向上抛出了异常,又去打印错误日志(对外rpc接口函数入口处除外)。

  3. 特殊的条件分支

    程序进入到一些特殊的条件分支时,比如特殊的else或者switch分支。比如接口返回的数据List为空。

  4. 关键执行路径及中间状态

    比如某个业务操作或者算法步骤繁琐,需要记录中间状态,从而方便后续定位跟踪算法执行状态

  5. 请求入口和出口

    在函数或者对外接口的入口/出口处需要打印入口/出口日志,一来方便后续进行日志统计,同时也更加方便进行系统运行状态的监控。

使用占位符而不是用字符串拼接

logger.debug("start process request, url: " + url); // 即使日志级别高于debug也会做一个字符串连接操作
logger.debug("receive request: {}", toJson(request)); // 用了SLF4J/Log4j2 中的懒求值方式来避免不必要的字符串拼接开销,但是toJson()这个函数却是都会被调用并且开销更大。

推荐的写法:

logger.debug("start process request, url:{}", url); // SLF4J/LOG4J2
logger.debug("receive request: {}", () -> toJson(request)); // LOG4J
logger.debug(() -> "receive request: " + toJson(request)); // LOG4J2
if (logger.isDebugEnabled()) { // SLF4J/LOG4J2
    logger.debug("receive request: " + toJson(request));
}

禁止输出古怪的字符,容易造成日志混乱

logger.debug("========start process request=======");

快速定位一条日志,保证日志唯一性

  1. 使用日志ID标记一条唯一的日志,可以使用 IDEA log support2 插件,主动生成一个LOGID
  2. 可以在每条日志信息记录具体的位置

记录任何可能的情况

不能认为某一情况一定不会发生,因为程序在高并发下和网络本省的不确定性,它还是可能发生,如果这个地方属于关键代码,应该就为error级别的错误。如果确认不可能发生,那就使用断言确保他不会发生

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值