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 个字符,从开头剪除。

有些特殊符号不能直接打印,需要使用实体名称或者编号

& —— &amp; 或者 &#38;
< —— &lt;  或者 &#60;
> —— &gt;  或者 &#62;
“ —— &quot; 或者 &#34;
‘ —— &apos; 或者 &#39;

2.2 MDC机制

https://blog.csdn.net/xiaolyuh123/article/details/80560662

MDC之坑.png

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 官网配置)

logback 官网配置.png

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";
    }
}

参考资料

上一篇下一篇

猜你喜欢

热点阅读