日志就像车辆保险,没人愿意为保险付钱,但是一旦出了问题谁都又想有保险可用
日志的作用和目的
日志文件
日志文件是用于记录系统操作事件的文件集合,可以分为事件日志和消息日志。具有处理历史数据、诊断问题的追踪以及理解系统的活动等重要作用。
在计算机中,日志文件是一个记录了发生在运行中的操作系统或者其他软件中的事件的文件,或者记录了在网络聊天软件的用户之间发送的消息。日志记录是指保存日志的行为。最简单的做法的将日志写入单个存放日志的文件。
为什么要打印日志
为什么要打印日志,或者什么时候打印日志这取决于打印的目的。不同的打印目的决定了日志输出的格式,输出的位置以及输出的频率
调试开发:目的是开发调试程序时使用,只应该出现在开发周期内,而不应该在线上系统输出
用户行为日志:记录用户操作行为,多用于大数据分析,如监控、风控、推荐等等
程序运行日志:记录程序运行时情况,特别是非预期的行为,异常情况,主要是开发维护使用
机器日志:主要是记录网络请求、系统 CPU、内存、IO 使用等情况,供运维或者监控使用
日志中应该包含什么
利用 4W1H 进行分析
When:打印日志的时间戳,此时的时间应该是日志记录的事情发生的时间,具体的时间可以帮助我们分析时间发生的时间点
Where:日志在哪里被记录,具体哪个模块,记录到哪个文件,哪个函数,哪一行代码
What:日志的主体是什么,简明扼要描述日志记录的事情
Who:事件生产者的唯一标识,以订单为例就是订单 id,当然也可以是某个动作的声明
How:日志的重要程度分级,一般以 ERROR > WARNNING > INFO > DEBUG > TRACE 来划分重要程度
Java 日志的前世今生
为什么要用日志框架
软件系统发展到现在已经非常复杂了,特别是在服务器端软件,涉及到的知识以及内容问题太多。在某些方面使用别人成熟的框架,就相当于让别人帮你完成一些基础工作,你只需要集中精力完成系统的业务逻辑设计。而且框架一般是成熟、稳健的,他可以帮助你处理很多细节的问题,比如日志的异步处理、动态控制等等问题。还有框架一般都是经过很多人使用,所以结构性、扩展性都非常好。
现有的日志框架
按照日志门面和日志实现划分的话现有的 Java 日志框架有以下几种
日志门面:JCL、Slf4j
日志实现:JUL、Logback、Log4j、Log4j2
为什么要有日志门面
当我们的系统变得更加复杂的时候,我们的日志就容易发生混乱。随着系统开发的进行,可能会更新不同的日志框架,造成当前系统中存在不同的日志依赖,让我们难以统一的管理和控制。就算我们强制要求了我们公司内开发的项目使用了相同的日志框架,但是系统中会引用其他类似 Spring 或者 Mybatis 等等的第三方框架,它们依赖于我们规定不同的日志框架,而且他们自身的日志系统就有着不一致性,依然会出现日志体系的混乱。
所以借鉴 JDBC 的思想,为日志系统也提供一套门面,那么我们就可以面向这些接口规范来开发,避免直接依赖具体的日志框架。这样我们的系统在日志中就存在了日志的门面和日志的实现。
日志门面的日志实现的关系
Log4j
Apache Log4j 是一种基于 Java 的日志记录工具,它是 Apache 软件基金会的一个项目。在 jdk1.3 之前,还没有现成的日志框架,Java 工程师只能使用原始的 System.out.println (), System.err.println () 或者 e.printStackTrace ()。通过把 debug 日志写到 StdOut 流,错误日志写到 ErrOut 流,以此记录应用程序的运行状态。这种原始的日志记录方式缺陷明显,不仅无法实现定制化,而且日志的输出粒度不够细。鉴于此,1999 年,大牛 Ceki Gülcü 创建了 Log4j 项目,并几乎成为了 Java 日志框架的实际标准。
JUL
Log4j 作为 Apache 基金会的一员,Apache 希望将 Log4j 引入 jdk,不过被 sun 公司拒绝了。随后,sun 模仿 Log4j,在 jdk1.4 中引入了 JUL(java.util.logging)。
JCL
为了解耦日志接口与实现,2002 年 Apache 推出了 JCL (Jakarta Commons Logging),也就是 Commons Logging。Commons Logging 定义了一套日志接口,具体实现则由 Log4j 或 JUL 来完成。Commons Logging 基于动态绑定来实现日志的记录,在使用时只需要用它定义的接口编码即可,程序运行时会使用 ClassLoader 寻找和载入底层的日志库,因此可以自由选择由 log4j 或 JUL 来实现日志功能。
SlF4j 和 Logback
大牛 Ceki Gülcü 与 Apache 基金会关于 Commons-Logging 制定的标准存在分歧,后来,Ceki Gülcü 离开 Apache 并先后创建了 Slf4j 和 Logback 两个项目。Slf4j 是一个日志门面,只提供接口,可以支持 Logback、JUL、Log4j 等日志实现,Logback 提供具体的实现,它相较于 log4j 有更快的执行速度和更完善的功能。
Log4j2
为了维护在 Java 日志江湖的地位,防止 JCL、Log4j 被 Slf4j、Logback 组合取代 ,2014 年 Apache 推出了 Log4j 2。Log4j 2 与 Log4j 不兼容,经过大量深度优化,其性能显著提升。
各个日志框架原理简介及介绍
Log4j
Log4j 是 Apache 下的一款开源的日志框架,通过在项目中使用 Log4j,我们可以控制日志信息输出到控制台、文件、甚至是数据库中。我们可以控制每一条日志的输出格式,通过定义日志的输出级别,可以更加灵活方便的控制日志的输出过程。
Log4j 的官方网站:http://logging.apache.org/log4j/1.2/
如果要在项目中使用 Log4j 的话需要引入相应的 Jar 包
Log4j 主要是由 Loggers、Appenders、和 Layout 组成
Loggers
Loggers 主要负责处理日志记录,Loggers 的命名有继承的机制,例如名称为 com.test.log 的 logger 会继承名称为 com.test 的 logger。
Log4j 中有一个特殊的 logger 叫作 “root”,他是所有 logger 的根,也就是意味着其他所有的 logger 都会直接或者间接的继承自 root。
Appenders
Appender 用来指定日志输出到哪个地方,可以同时指定多个日志的输出目的地。Log4j 的输出目的地有以下集中。
输出端类型
作用
ConsoleAppender
将日志输出到控制台
FileAppender
将日志输出到文件
DailyRollingFileAppender
将日志输出到一个日志文件,并且每天输出到一个新的文件
RollingFileAppender
将日志信息输出到日志文件,并且按照指定文件的尺寸,当文件大小达到指定尺寸时,会自动将文件改名,同时生成一个新的文件
JDBCAppender
把日志信息保存到数据库中
Layouts
Layouts 用于控制日志输出内容的格式,让我们可以使用各种需要的格式输出日志。Log4j 常用的 Layouts:
格式化器类型
作用
HTMLLayout
格式化日志输出为 HTML 表格形式
SimpleLayout
简单的日志输出格式化
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字符,就从左边交远销出的字符截掉
JUL
JUL 全称是 Java util Logging,是 Java 原生的日志框架,使用时不需要另外引用第三方类库,相对其他日志框架使用方便,学习简单,能够在小型应用中灵活使用。
JUL 的架构
Logger:被称为记录器,应用程序通过获取 Logger 对象,调用其 API 来发布日志信息。Logger 通常是应用程序访问日志系统的入口程序
Handler(和 Log4j 的 Appenders 类似):每个 Logger 都会被关联一组 Handlers,Logger 会将日志交给关联的 Handlers 处理。此 Handler 是一个抽象,其具体的实现决定了日志记录的位置可以是控制台、文件、数据库等等
Layouts:也被称为 Formatters,它负责对日志进行格式化的处理,Layouts 决定了数据在一条日志记录中的最终形式
Filters:过滤器,根据需要定制哪些信息会被记录
总结一下就是用户使用 Logger 来进行日志的记录,Logger 持有若干个 Handler,日志的输出操作是由 Handler 来完成的,在 Handler 输出之前会通过自定义的 Filter 过滤规则过滤掉不需要输出的信息,最终由 Handler 决定使用什么样的 Layout 将日志格式化处理并决定输出到什么地方去。
接下来就写一个简单的入门案例看一下 JUL 是如何进行日志处理的
JUL 日志处理无需引用任何日志框架,是 Java 自带的功能
// 1.获取日志记录器对象
Logger logger = Logger.getLogger("com.macaque.JulLogTest");
// 关闭系统默认配置
logger.setUseParentHandlers(false);
// 自定义配置日志级别
// 创建ConsolHhandler 控制台输出
ConsoleHandler consoleHandler = new ConsoleHandler();
// 创建简单格式转换对象
SimpleFormatter simpleFormatter = new SimpleFormatter();
// 进行关联
consoleHandler.setFormatter(simpleFormatter);
logger.addHandler(consoleHandler);
// 配置日志具体级别
logger.setLevel(Level.ALL);
consoleHandler.setLevel(Level.ALL);
logger.severe("severe");
logger.warning("waring");
logger.info("info");
logger.config("config");
logger.fine("fine");
logger.finer("finer");
logger.finest("finest");
JCL
全称为 Jakarta Commons Logging,是由 Apache 提供的一个通用的日志 API。
它的目标是 “为所有的 Java 日志实现” 提供一个统一的接口,它自身也提供一个日志实现,但是功能非常弱(SimpleLog)。所以一般不单独使用 JCL。他允许开发人员使用不同的日志实现工具:Log4j、JDK 自带的日志(JUL)
JCL 有两个基本的抽象类:Log 和 LogFactory
如何使用
如果要在项目中使用 JCL 则要引入相应的 jar 包
这只是引入了相应的日志门面,具体的日志实现还需要自己引入。
原理介绍
在使用 JCL 打印日志的时候是通过调用其 LogFactory 动态加载 Log 的实现类
Log log = LogFactory.getLog(xxxx.class);
然后在初始化的时候通过遍历数组进行查找有没有符合的实现类,遍历的数组初始化是
/**
* The names of classes that will be tried (in order) as logging
* adapters. Each class is expected to implement the Log interface,
* and to throw NoClassDefFound or ExceptionInInitializerError when
* loaded if the underlying logging library is not available. Any
* other error indicates that the underlying logging library is available
* but broken/unusable for some reason.
*/
private static final String[] classesToDiscover = {
"org.apache.commons.logging.impl.Log4JLogger",
"org.apache.commons.logging.impl.Jdk14Logger",
"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
"org.apache.commons.logging.impl.SimpleLog"
};
遍历这个数组的逻辑
for(int i=0; i result = createLogFromClass(classesToDiscover[i], logCategory, true); } SlF4j 简单日志门面(Simple Logging Facade For Java)SlF4j 主要是为了给 Java 日志访问提供一套标准、规范的 API 框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架。对于一般的 Java 项目而言,日志框架会选择 Slf4j-api 作为门面,配上具体的实现框架,中间使用桥接器进行桥接。 官方网站:http://www.slf4j.org/ Slf4j 是目前市面上最流行的日志门面,其主要提供两大功能: 日志框架的绑定 日志框架的桥接 日志的绑定 Slf4j 支持各种日志框架,而 Slf4j 只是作为一个日志门面的存在,定义一个日志的打印规范,那么就会有两种情况,针对这两种情况引入包的类别略有不同。 遵守 Slf4j 定义的规范:如果是遵守了 Slf4j 定义的日志规范的话,那么只需要引入两个包,一个是 Slf4j 的依赖,以及遵守了其规范的日志 jar 包实现即可 没遵守 Slf4j 定义的规范:如果未遵守 Slf4j 定义的日志规范,那么需要引入三个包,一个是 Slf4j 的依赖,一个是适配器的包,一个是未遵守 Slf4j 定义的日志规范的包. 这是官网上给出的一张图,描述的就是其绑定的过程。 日志绑定底层原理简介 在上面介绍的 JCL 的底层绑定原理我们了解到 JCL 是通过轮询的机制进行启动时检测绑定的日志实现,但是在 Slf4j 中不一样,我们可以从 LoggerFactory.getLogger 方法中进行入手查看,最终定位到 LoggerFactory 的 findPossibleStaticLoggerBinderPathSet 方法,具体如下。 private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class"; static Set Set try { ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader(); Enumeration if (loggerFactoryClassLoader == null) { paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH); } else { // 这一处是重点,通过类加载器找到所有org/slf4j/impl/StaticLoggerBinder.class的类 paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH); } while (paths.hasMoreElements()) { URL path = paths.nextElement(); staticLoggerBinderPathSet.add(path); } } catch (IOException ioe) { Util.report("Error getting resources from path", ioe); } return staticLoggerBinderPathSet; } 所以其加载过程简单如下 Slf4j 通过 LoggerFactory 加载日志的具体实现 LoggerFactory 在初始化的过程中,会通过 performInitialization () 方法绑定具体的日志实现 在绑定具体实现的时候,通过类加载器,加载 org/slf4j/impl/StaticLoggerBinder.class 类 所以,只要是一个日志实现框架,在 org.slf4j.impl 包中提供一个自己的 StaticLoggerBinder 类,在其中提供具体日志实现的 LoggerFactory 就可以被 Slf4j 进行加载管理了 日志框架的桥接 在一些老项目中有可能一开始使用的不是 Slf4j 框架,如果在这时想要进行日志的升级,那么 Slf4j 也提供了这样的功能,提供了相应的桥接器进行对原有日志框架的替换,下图是官网所表示的如何进行的日志桥接。其实简单来说就是将原有的日志重定向到 Slf4j 然后交由 Slf4j 进行管理。 有可能看图不好理解桥接的意思,我们直接使用例子来演示一下 Slf4j 是如何替换原有的日志框架的。 首先我们建立一个项目首先使用 Log4j 进行打印日志,引入 Log4j 的 jar 包 然后简单加入 Log4j 的配置进行打印日志 @Test public void testLog4jToSlf4j(){ Logger logger = Logger.getLogger(TestSlf4jBridge.class); logger.info("testLog4jToSlf4j"); } 控制台的输出如下,因为没有做日志格式的处理,所以只是简单输出了字符串。 接下来我们要在不改动一点代码的情况,只是加入和移除一些依赖包就可以完成日志框架的升级,我们这里假设要升级为 Logback,按照以下步骤进行即可。 移除原有的日志框架(这里就是 Log4j 的日志框架) 移除了原有日志框架,代码肯定报错了,所以再添加 Log4j 的日志桥接器 加入 Slf4j-api 的依赖 再加入 Logback 的日志实现依赖 完成这四步以后,日志框架就完成了升级,接下来我们看一下效果,这里在 Logback 的日志输出中加入了格式的处理。能看到日志已经是由 Logback 打印出来了。 Logback Logback 是由 Log4j 的创始人设计的另一款开源日志组件,性能比 Log4j 性能要好,官方网站:https://logback.qos.ch/index.html Logback 主要分为三个模块 logback-core:其他两个模块的基础模块 logback-classic:它是 Log4j 的一个改良版本,同时它完整实现了 Slf4j 的 API logback-access:访问模块与 Servlet 容器集成,通过 Http 来访问日志的功能 后续的日志都是通过 Slf4j 日志门面搭建日志系统,所以在代码是没有什么区别的,主要是通过改变配置文件和 pom 依赖。 pom 依赖 基本配置 logback 会依次读取以下类型配置文件: logback.groovy logabck-test.xml logback.xml class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> FileAppender 配置 class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> HH:mm:ss}%c%M%L%thread%m RollingFileAppender 配置 class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> class="ch.qos.logback.core.rolling.RollingFileAppender"> class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> log%i.gz Filter 和异步日志配置 class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> class="ch.qos.logback.core.rolling.RollingFileAppender"> class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> log%i.gz Log4j 转向 Logback 官方提供了 Log4j.properties 转换成 logback.xml 文件配置的工具:http://logback.qos.ch/translator/ Log4j2 Apache Log4j2 是对 Log4j 的升级版,参考了 logback 的一些优秀设计,并且修复了一些问题带来了一些重大的提升,主要有: 异常处理:在 logback 中 Appender 中的异常不会被应用感知到,但是在 log4j2 中提供了一些异常的处理机制 性能提升,log4j2 相较于 log4j 和 logback 都具有很明显的性能提升,后面会有官方的测试数据 自动重载配置,参考了 logback 的配置,当然会提供自动刷新参数配置,最实用的就是在我们生产环境中动态的修改日志的级别而不需要重启应用 无垃圾机制,log4j 在大部分情况下,都可以使用其设计的一套无垃圾机制,避免频繁的日志收集导致的 jvm gc 官方网站:https://logging.apache.org/log4j/2.x/ 如何使用 Log4j2 目前市面上最主流的日志门面是 Slf4j,虽然本身 Log4j2 也是日志门面,因为它的日志实现功能非常强大,性能优越。所以大家一般还是将 Log4j2 看作是日志额实现,Slf4j+Log4j2 应该是未来的大势所趋。 添加依赖(配合 Slf4j 进行使用) Log4j2 的配置 Log4j2 的配合 Logback 的配置特别一样 filePattern="/logs/$${date:yyyy-MM-dd}/myrollog-%d{yyyy-MM-dd-HH-mm}-%i.log"> 异步日志 Log4j2 最大的特点就是异步日志,其性能的提升也是从异步日志中受益的。Log4j2 提供了两种异步日志的实现,一种是 AsyncAppender,一个是通过 AsyncLogger,分别对应前面我们说的 Apperder 组件和 Logger 组件。 如果要使用异步日志还需要额外引入一个 Jar 包 官网目前不建议使用 AsyncAppender 的模式,所以这里就不介绍了,着重介绍一下关于 AsyncLogger 的日志。其中 AsyncLogger 有两种选择:全局异步和混合异步。 全局异步就是所有的日志都是异步的记录,在配置文件上不需要任何改动,只需要加一个全局的 system 配置即可:-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector 混合异步就是,你可以在应用中同时使用同步日志和异步日志,这使得日志的配置方式更加灵活 includeLocation="false" additivity="false"> 如上的配置:com.macaque 日志是异步的,root 日志是同步的 使用异步日志需要注意两个问题 如果使用异步日志,AsyncApperder、AsyncLogger 和全局日志,不要同时出现。行呢个会和 AsyncApperder 一致,降至最低。 设置 includeLocation=false,打印位置信息会急剧降低异步日志的性能,比同步日志还要慢 Log4j2 的性能 Log4j2 最厉害的地方在于异步输出日志时的性能表现,Log4j2 再多线程的环境下吞吐量与 Log4j 和 Logback 比较官网提供的图。可以看到使用全局异步模式性能时最好的,其次是使用混合异步模式。 打印日志的最佳实践 坚持把简单的事情做好就是不简单,坚持把平凡的事情做好就是不平凡。所谓成功,就是在平凡中做出不平凡的坚持! 好的日志记录方式可以提供我们足够多定位问题的依据。日志记录大家都会认为很简单,但是如何通过日志可以高效定位问题并不是简单的事情。 怎么记日志更方便我们查问题 对外部的调用封装 程序中对外部系统与模块的依赖调用前后都记下日志,方便接口调试。出问题时也可以很快理清是哪块的问题。 boolean debugEnabled = logger.isDebugEnabled(); if (debugEnabled){ logger.debug("Calling external system : {}",requestParam); } try{ result = callRemoteSystem(requestParam); if (debugEnabled){ logger.debug("Called successfully result is :{}",result); } }catch (BusinessException e){ logger.warn("Failed at calling xxx system request:{}",requestParam,e); }catch (Exception e){ logger.error("Failed at calling xxx system Exception request:{}",requestParam,e); } 状态变化 程序中重要的状态信息变化应该记录下来,方便查问题时还原现场,推断程序运行过程。 系统入口与出口 这个粒度可以是重要的方法或者模块级别的,记录它的输入和输出,方便定位。 业务异常 任何业务异常都应该记下来并且将异常栈给输出出来。 很少出现的 else 情况 很少出现的 else 情况可能吞掉你的请求,或是赋予难以理解的最终结果 应该避免怎样的日志方式 混淆信息的 Log 日志应该是清晰准确的,比如当看到下面日志的时候,你知道是因为连接池取不到连接导致的问题吗? Connection connection = ConnectionFactory.getConnection(); if (connection == null) { LOG.warn("System initialized unsuccessfully"); } 不分级别的记录日志 无论是异常情况还是入参请求使用打印日志的级别都是 info 级别,没有区分级别。这样有两个不好的地方。 无法将打印日志在物理进行区分至不同文件 大量输出无效日志,不利于系统性能提升,也不利于快速定位错误点 遗漏关键信息 这里有可能包括两种情况 正常情况下未打印关键信息,比如下单流程的订单 ID 异常情况下未打印异常栈 动态拼接字符串 使用 String 字符串的拼接会使用 StringBuilder 的 append () 方式,有一定的性能损耗。使用占位符仅仅是替换动作,可以有效提升性能。 重复打印日志 避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivety=false 不加开关的日志输出 logger.debug("Called successfully result is :{}", JSONObject.toJSONString(result)); 打印的是 debug 日志,如果这时候将日志级别改为 info,虽然说不会输出 debug 的日志,但是参数会进行字符串拼接运算,也就是 JSON 序列化的方法会被调用。是会浪费方法调用的性能。 所有日志输入到一个文件中 不同级别的日志信息应该输出到不同的日志文件中。将信息进行区分,不仅能够有效的定位问题,也能够将现场保留的更久。 源代码 关于日志中的所有涉及到的源代码都在:https://github.com/modouxiansheng/Macaque/tree/master/macaque-log 中,大家可以自己下载下来修改配置文件自己理解一下。 参考文章 Springboot 整合 log4j2 日志全解 为什么阿里巴巴禁止工程师直接使用日志系统 (Log4j、Logback) 中的 API 动态调整日志级别 程序那些事:日志记录的作用和方法 进阶之路:Java 日志框架全画传(上) 你还在用 Logback?Log4j2 的异步性能已经无敌了,还不快试试