美文网首页
java中的日志框架

java中的日志框架

作者: suxin1932 | 来源:发表于2019-10-08 14:51 被阅读0次

    各日志框架配置原则: 先看官网 --> 再看源代码 --> 最后中文博客

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

    参考资料

    相关文章

      网友评论

          本文标题:java中的日志框架

          本文链接:https://www.haomeiwen.com/subject/ibpbpctx.html