关于java:万字长文带你了解日志的前世今生

48次阅读

共计 21130 个字符,预计需要花费 53 分钟才能阅读完成。

日志就像车辆保险,没人违心为保险付钱,然而一旦出了问题谁都又想有保险可用

日志的作用和目标

日志文件

日志文件是用于记录零碎操作事件的文件汇合,能够分为事件日志和消息日志。具备解决历史数据、诊断问题的追踪以及了解零碎的流动等重要作用。

在计算机中,日志文件是一个记录了产生在运行中的操作系统或者其他软件中的事件的文件,或者记录了在网络聊天软件的用户之间发送的音讯。日志记录是指保留日志的行为。最简略的做法的将日志写入单个寄存日志的文件。

为什么要打印日志

为什么要打印日志,或者什么时候打印日志这取决于打印的目标。不同的打印目标决定了日志输入的格局,输入的地位以及输入的频率

  1. 调试开发:目标是开发调试程序时应用,只应该呈现在开发周期内,而不应该在线上零碎输入
  2. 用户行为日志:记录用户操作行为,多用于大数据分析,如监控、风控、举荐等等
  3. 程序运行日志:记录程序运行时状况,特地是非预期的行为,异常情况,次要是开发保护应用
  4. 机器日志:次要是记录网络申请、零碎 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 包

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

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 包

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

这只是引入了相应的日志门面,具体的日志实现还须要本人引入。

原理介绍

在应用 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<classesToDiscover.length && result == null; ++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 只是作为一个日志门面的存在,定义一个日志的打印标准,那么就会有两种状况,针对这两种状况引入包的类别略有不同。

  1. 恪守 Slf4j 定义的标准:如果是恪守了 Slf4j 定义的日志标准的话,那么只须要引入两个包,一个是 Slf4j 的依赖,以及恪守了其标准的日志 jar 包实现即可
  2. 没恪守 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<URL> findPossibleStaticLoggerBinderPathSet() {Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
    try {ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
        Enumeration<URL> paths;
        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;
}

所以其加载过程简略如下

  1. Slf4j 通过 LoggerFactory 加载日志的具体实现
  2. LoggerFactory 在初始化的过程中,会通过 performInitialization()办法绑定具体的日志实现
  3. 在绑定具体实现的时候,通过类加载器,加载 org/slf4j/impl/StaticLoggerBinder.class 类
  4. 所以,只有是一个日志实现框架,在 org.slf4j.impl 包中提供一个本人的 StaticLoggerBinder 类,在其中提供具体日志实现的 LoggerFactory 就能够被 Slf4j 进行加载治理了

日志框架的桥接

在一些老我的项目中有可能一开始应用的不是 Slf4j 框架,如果在这时想要进行日志的降级,那么 Slf4j 也提供了这样的性能,提供了相应的桥接器进行对原有日志框架的替换,下图是官网所示意的如何进行的日志桥接。其实简略来说就是将原有的日志 重定向 到 Slf4j 而后交由 Slf4j 进行治理。

有可能看图不好了解桥接的意思,咱们间接应用例子来演示一下 Slf4j 是如何替换原有的日志框架的。

首先咱们建设一个我的项目首先应用 Log4j 进行打印日志,引入 Log4j 的 jar 包

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

而后简略退出 Log4j 的配置进行打印日志

@Test
public void testLog4jToSlf4j(){Logger logger = Logger.getLogger(TestSlf4jBridge.class);
    logger.info("testLog4jToSlf4j");
}

控制台的输入如下,因为没有做日志格局的解决,所以只是简略输入了字符串。

接下来咱们要在不改变一点代码的状况,只是退出和移除一些依赖包就能够实现日志框架的降级,咱们这里假如要降级为 Logback,依照以下步骤进行即可。

  1. 移除原有的日志框架(这里就是 Log4j 的日志框架)
  2. 移除了原有日志框架,代码必定报错了,所以再增加 Log4j 的日志桥接器
  3. 退出 Slf4j-api 的依赖
  4. 再退出 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 依赖

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.26</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

根本配置

logback 会顺次读取以下类型配置文件:

  • logback.groovy
  • logabck-test.xml
  • logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--
日志输入格局:%-5level
%d{yyyy-MM-dd HH:mm:ss.SSS}日期
%c 类的残缺名称
%M 为 method
%L 为行号
%thread 线程名称
%m 或者 %msg 为信息
%n 换行
-->
<!-- 格式化输入:%d 示意日期,%thread 示意线程名,%-5level:级别从左显示 5 个字符宽度
%msg:日志音讯,%n 是换行符 -->
<property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread]
%-5level %msg%n"/>
<!--
Appender: 设置日志信息的去向, 罕用的有以下几个
ch.qos.logback.core.ConsoleAppender (控制台)
3. FileAppender 配置
ch.qos.logback.core.rolling.RollingFileAppender (文件大小达到指定尺
寸的时候产生一个新文件)
ch.qos.logback.core.FileAppender (文件)
-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!-- 输入流对象 默认 System.out 改为 System.err-->
<target>System.err</target>
<!-- 日志格局配置 -->
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!--
用来设置某一个包或者具体的某一个类的日志打印级别、以及指定 <appender>。<loger> 仅有一个 name 属性,一个可选的 level 和一个可选的 addtivity 属性
name:
用来指定受此 logger 束缚的某一个包或者具体的某一个类。level:
用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和
OFF,如果未设置此属性,那么以后 logger 将会继承下级的级别。additivity:
是否向下级 loger 传递打印信息。默认是 true。<logger> 能够蕴含零个或多个 <appender-ref> 元素,标识这个 appender 将会增加到这个
logger
-->
<!--
也是 <logger> 元素,然而它是根 logger。默认 debug
level: 用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL
和 OFF,<root> 能够蕴含零个或多个 <appender-ref> 元素,标识这个 appender 将会增加到这个
logger。-->
<root level="ALL">
<appender-ref ref="console"/>
</root>
</configuration>

FileAppender 配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 自定义属性 能够通过 ${name}进行援用 -->
<property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c %M
%L [%thread] %m %n"/>
      <!--
        日志输入格局:%d{pattern}日期
        %m 或者 %msg 为信息
        %M 为 method
        %L 为行号
        %c 类的残缺名称
        %thread 线程名称
        %n 换行
        %-5level
      -->
<!-- 日志文件寄存目录 -->
<property name="log_dir" value="d:/logs"></property>
<!-- 控制台输入 appender 对象 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!-- 输入流对象 默认 System.out 改为 System.err-->
<target>System.err</target>
<!-- 日志格局配置 -->
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!-- 日志文件输入 appender 对象 -->
<appender name="file" class="ch.qos.logback.core.FileAppender">
<!-- 日志格局配置 -->
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<!-- 日志输入门路 -->
<file>${log_dir}/logback.log</file>
</appender>
<!-- 生成 html 格局 appender 对象 -->
<appender name="htmlFile" class="ch.qos.logback.core.FileAppender">
<!-- 日志格局配置 -->
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.html.HTMLLayout">
<pattern>%level%d{yyyy-MM-dd
HH:mm:ss}%c%M%L%thread%m</pattern>
</layout>
</encoder>
<!-- 日志输入门路 -->
<file>${log_dir}/logback.html</file>
</appender>
<!--RootLogger 对象 -->
<root level="all">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
<appender-ref ref="htmlFile"/>
</root>
</configuration>

RollingFileAppender 配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 自定义属性 能够通过 ${name}进行援用 -->
<property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c %M
%L [%thread] %m %n"/>
      <!--
        日志输入格局:%d{pattern}日期
        %m 或者 %msg 为信息
        %M 为 method
        %L 为行号
        %c 类的残缺名称
        %thread 线程名称
        %n 换行
        %-5level
      -->
<!-- 日志文件寄存目录 -->
<property name="log_dir" value="d:/logs"></property>
<!-- 控制台输入 appender 对象 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
  <!-- 输入流对象 默认 System.out 改为 System.err-->
  <target>System.err</target>
  <!-- 日志格局配置 -->
  <encoder
  class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
  <pattern>${pattern}</pattern>
  </encoder>
</appender>
<!-- 日志文件拆分和归档的 appender 对象 -->
<appender name="rollFile"
class="ch.qos.logback.core.rolling.RollingFileAppender">
  <!-- 日志格局配置 -->
  <encoder
  class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
  <pattern>${pattern}</pattern>
  </encoder>
  <!-- 日志输入门路 -->
  <file>${log_dir}/roll_logback.log</file>
  <!-- 指定日志文件拆分和压缩规定 -->
  <rollingPolicy
    class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
    <!-- 通过指定压缩文件名称,来确定宰割文件形式 -->
    <fileNamePattern>${log_dir}/rolling.%d{yyyy-MMdd}.
    log%i.gz</fileNamePattern>
    <!-- 文件拆分大小 -->
    <maxFileSize>1MB</maxFileSize>
  </rollingPolicy>
</appender>
<!--RootLogger 对象 -->
<root level="all">
  <appender-ref ref="console"/>
  <appender-ref ref="rollFile"/>
</root>
</configuration>

Filter 和异步日志配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 自定义属性 能够通过 ${name}进行援用 -->
<property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c %M
%L [%thread] %m %n"/>
<!--
日志输入格局:%d{pattern}日期
%m 或者 %msg 为信息
%M 为 method
%L 为行号
%c 类的残缺名称
%thread 线程名称
%n 换行
%-5level
-->
<!-- 日志文件寄存目录 -->
<property name="log_dir" value="d:/logs/"></property>
<!-- 控制台输入 appender 对象 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!-- 输入流对象 默认 System.out 改为 System.err-->
<target>System.err</target>
<!-- 日志格局配置 -->
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!-- 日志文件拆分和归档的 appender 对象 -->
<appender name="rollFile"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志格局配置 -->
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<!-- 日志输入门路 -->
<file>${log_dir}roll_logback.log</file>
<!-- 指定日志文件拆分和压缩规定 -->
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 通过指定压缩文件名称,来确定宰割文件形式 -->
<fileNamePattern>${log_dir}rolling.%d{yyyy-MMdd}.
log%i.gz</fileNamePattern>
<!-- 文件拆分大小 -->
<maxFileSize>1MB</maxFileSize>
</rollingPolicy>
<!--filter 配置 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!-- 设置拦挡日志级别 -->
<level>error</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 异步日志 -->
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="rollFile"/>
</appender>
<!--RootLogger 对象 -->
<root level="all">
<appender-ref ref="console"/>
<appender-ref ref="async"/>
</root>
<!-- 自定义 logger additivity 示意是否从 rootLogger 继承配置 -->
<logger name="com.macaque" level="debug" additivity="false">
<appender-ref ref="console"/>
</logger>
</configuration>

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 进行应用)

<!-- 应用 slf4j 作为日志的门面, 应用 log4j2 来记录日志 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<!-- 为 slf4j 绑定日志实现 log4j2 的适配器 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.10.0</version>
</dependency>
<!-- Log4j2 门面 API-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.11.1</version>
</dependency>
<!-- Log4j2 日志实现 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.11.1</version>
</dependency>

Log4j2 的配置

Log4j2 的配合 Logback 的配置特地一样

<?xml version="1.0" encoding="UTF-8"?>
<!--
    status="warn" 日志框架自身的输入日志级别
    monitorInterval="5" 主动加载配置文件的间隔时间,不低于 5 秒
-->
<Configuration status="debug" monitorInterval="5">
    <!--
        集中配置属性进行治理
        应用时通过:${name}
    -->
    <properties>
        <property name="LOG_HOME">/logs</property>
    </properties>
    <!-- 日志解决 -->
    <Appenders>
        <!-- 控制台输入 appender-->
        <Console name="Console" target="SYSTEM_ERR">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n" />
        </Console>
        <!-- 日志文件输入 appender-->
        <File name="file" fileName="${LOG_HOME}/myfile.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
        </File>
        <!--<Async name="Async">-->
            <!--<AppenderRef ref="file"/>-->
        <!--</Async>-->
        <!-- 应用随机读写刘的日志文件输入 appender,性能进步 -->
        <RandomAccessFile name="accessFile" fileName="${LOG_HOME}/myAcclog.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
        </RandomAccessFile>
        <!-- 依照肯定规定拆分的日志文件的 appender-->
        <RollingFile name="rollingFile" fileName="${LOG_HOME}/myrollog.log"
                     filePattern="/logs/$${date:yyyy-MM-dd}/myrollog-%d{yyyy-MM-dd-HH-mm}-%i.log">
            <!-- 日志级别过滤器 -->
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
            <!-- 日志音讯格局 -->
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %msg%n" />
            <Policies>
                <!-- 在系统启动时,登程拆分规定,生产一个新的日志文件 -->
                <OnStartupTriggeringPolicy />
                <!-- 依照文件大小拆分,10MB -->
                <SizeBasedTriggeringPolicy size="10 MB" />
                <!-- 依照工夫节点拆分,规定依据 filePattern 定义的 -->
                <TimeBasedTriggeringPolicy />
            </Policies>
            <!-- 在同一个目录下,文件的个数限定为 30 个,超过进行笼罩 -->
            <DefaultRolloverStrategy max="30" />
        </RollingFile>
    </Appenders>
    <!--logger 定义 -->
    <Loggers>
        <!-- 自定义异步 logger 对象
            includeLocation="false" 敞开日志记录的行号信息
            additivity="false" 不在继承 rootlogger 对象
        -->
        <AsyncLogger name="com.macaque" level="trace" includeLocation="false" additivity="false">
            <AppenderRef ref="Console"/>
        </AsyncLogger>
        <!-- 应用 rootLogger 配置 日志级别 level="trace"-->
        <Root level="trace">
            <!-- 指定日志应用的处理器 -->
            <AppenderRef ref="Console" />
            <!-- 应用异步 appender-->
            <AppenderRef ref="Async" />
        </Root>
    </Loggers>
</Configuration>

异步日志

Log4j2 最大的特点就是异步日志,其性能的晋升也是从异步日志中受害的。Log4j2 提供了两种异步日志的实现,一种是 AsyncAppender,一个是通过 AsyncLogger,别离对应后面咱们说的 Apperder 组件和 Logger 组件。

如果要应用异步日志还须要额定引入一个 Jar 包

<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.4.2</version>
</dependency>

官网目前不倡议应用 AsyncAppender 的模式,所以这里就不介绍了,着重介绍一下对于 AsyncLogger 的日志。其中 AsyncLogger 有两种抉择:全局异步和混合异步。

  • 全局异步就是所有的日志都是异步的记录,在配置文件上不须要任何改变,只须要加一个全局的 system 配置即可:-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
  • 混合异步就是,你能够在利用中同时应用同步日志和异步日志,这使得日志的配置形式更加灵便
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <Appenders>
        <File name="file" fileName="${LOG_HOME}/myfile.log">
            <PatternLayout>
                <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
            </PatternLayout>
        </File>
        <Async name="Async">
            <AppenderRef ref="file"/>
        </Async>
    </Appenders>
    <Loggers>
        <AsyncLogger name="com.macaque" level="trace"
                     includeLocation="false" additivity="false">
            <AppenderRef ref="file"/>
        </AsyncLogger>
        <Root level="info" includeLocation="true">
            <AppenderRef ref="file"/>
        </Root>
    </Loggers>
</Configuration>

如上的配置:com.macaque 日志是异步的,root 日志是同步的

应用异步日志须要留神两个问题

  • 如果应用异步日志,AsyncApperder、AsyncLogger 和全局日志,不要同时呈现。行呢个会和 AsyncApperder 统一,降至最低。
  • 设置 includeLocation=false,打印地位信息会急剧升高异步日志的性能,比同步日志还要慢

Log4j2 的性能

Log4j2 最厉害的中央在于异步输入日志时的性能体现,Log4j2 再多线程的环境下吞吐量与 Log4j 和 Logback 比拟官网提供的图。能够看到应用全局异步模式性能时最好的,其次是应用混合异步模式。

打印日志的最佳实际

保持把简略的事件做好就是不简略,保持把平庸的事件做好就是不平庸。所谓胜利,就是在平庸中做出不平庸的保持!

好的日志记录形式能够提供咱们足够多定位问题的根据。日志记录大家都会认为很简略,然而如何通过日志能够高效定位问题并不是简略的事件。

怎么记日志更不便咱们查问题

  1. 对外部的调用封装

程序中对外部零碎与模块的依赖调用前后都记下日志,不便接口调试。出问题时也能够很快理清是哪块的问题。

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);
}
  1. 状态变动

程序中重要的状态信息变动应该记录下来,不便查问题时还原现场,推断程序运行过程。

  1. 零碎入口与进口

这个粒度能够是重要的办法或者模块级别的,记录它的输出和输入,不便定位。

  1. 业务异样

任何业务异样都应该记下来并且将异样栈给输入进去。

  1. 很少呈现的 else 状况

很少呈现的 else 状况可能吞掉你的申请,或是赋予难以了解的最终后果

应该防止怎么的日志形式

  1. 混同信息的 Log

日志应该是清晰精确的,比方当看到上面日志的时候,你晓得是因为连接池取不到连贯导致的问题吗?

  Connection connection = ConnectionFactory.getConnection();  
  if (connection == null) {LOG.warn("System initialized unsuccessfully");  
  }  
  1. 不分级别的记录日志

无论是异常情况还是入参申请应用打印日志的级别都是 info 级别,没有辨别级别。这样有两个不好的中央。

  • 无奈将打印日志在物理进行辨别至不同文件
  • 大量输入有效日志,不利于零碎性能晋升,也不利于疾速定位谬误点
  1. 脱漏要害信息

这里有可能包含两种状况

  • 失常状况下未打印要害信息,比方下单流程的订单 ID
  • 异常情况下未打印异样栈
  1. 动静拼接字符串

应用 String 字符串的拼接会应用 StringBuilder 的 append()形式,有肯定的性能损耗。应用占位符仅仅是替换动作,能够无效晋升性能。

  1. 反复打印日志

防止反复打印日志,节约磁盘空间,务必在日志配置文件中设置 additivety=false

  1. 不加开关的日志输入
logger.debug("Called successfully result is :{}", JSONObject.toJSONString(result));

打印的是 debug 日志,如果这时候将日志级别改为 info,尽管说不会输入 debug 的日志,然而参数会进行字符串拼接运算,也就是 JSON 序列化的办法会被调用。是会节约办法调用的性能。

  1. 所有日志输出到一个文件中

不同级别的日志信息应该输入到不同的日志文件中。将信息进行辨别,不仅可能无效的定位问题,也可能将现场保留的更久。

源代码

对于日志中的所有波及到的源代码都在:https://github.com/modouxiansheng/Macaque/tree/master/macaque-log 中,大家能够本人下载下来批改配置文件本人了解一下。

参考文章

  • Springboot 整合 log4j2 日志全解
  • 为什么阿里巴巴禁止工程师间接应用日志零碎 (Log4j、Logback) 中的 API
  • 动静调整日志级别
  • 程序那些事:日志记录的作用和办法
  • 进阶之路:Java 日志框架全画传(上)
  • 你还在用 Logback?Log4j2 的异步性能曾经无敌了,还不快试试

正文完
 0