前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

1、日志规范

1.1、背景

用户行为留痕、调用链追踪,大数据分析,线上问题定位和解决都离不开合理的日志规范,所有应用按同样的规范打印日志可为后续的工作带来极大的便利性。

1.2、日志框架选择

Slf4j 是为 Java 提供的简单日志门面。它允许用户以自己的喜好,在工程中通过 Slf4j 接入不同的日志系统。

现在常用的是logback和log4j2,其中logback是slf4j的原生实现框架,与log4j相比性能更加出众,

spring boot默认是支持logback的;log4j2出生更晚,已经不仅仅是log4j的升级,它还参考了logback的设计,并且据说在异步方面性能更加出众。如果需要用log4j2,需要在相关的spring包里剔除log相关的依赖。

<!-- Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <!-- 如果在使用自带tomcat请注释下面,如果使用第三方tomcat不要注释下面 -->
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
        <exclusion>
            <artifactId>logback-classic</artifactId>
            <groupId>ch.qos.logback</groupId>
        </exclusion>
    </exclusions>
</dependency>

1.3、打印时机

1、 系统完成初始化,比如加载必要的配置信息完成时

2、 接口的入口和出口,入口需要打印参数信息

3、 业务流程中不应该出现的地方,暗示数据或逻辑可能有错误的地方

4、 数据库存储过程中出现异常,比如唯一索引冲突,列格式不正确,事务需要回滚等;

5、 需要保留痕迹的地方,比如用户行为留痕,为合规或审计留痕等

6、 重要的业务流程状态变化,以及对应的分支处

1.3.1、打印日志案例


try {
    int i = 1/0;
}catch (Exception e){
    //如下证券
    log.info("程序出现了异常", e);
    log.info("---------------------------");
}

1.4、日志级别

我们使用常用的日志级别:error、info、warn、debug,其他级别不建议使用。

1、 error级别应该在业务逻辑出现异常、数据持久化出现异常、数据不匹配等场景下使用打印该日志意味着系统出现潜在问题,需要报警,并且需要相关人员马上介入解决。error日志需要包含问题出现时必要的信息,比如用户信息,调用栈信息等。error级别的日志文件可以独立,以便报警处理

2、 info级别用于用户行为留痕、调用链跟踪以及问题定位方面,一般在核心或者重要的方法入口需要打印,还有一些重要的代码分支也需要打印

3、 warn级别在产生不符合预期的结果,但业务并未受损的情况下可以使用,warn级别的日志可不产生短信报警,可与公司及时消息打通报警或者邮件报警,解决的时效性要求低,但需要关注。

4、 debug是在测试和本地环境使用的。为了方便定位问题,我们可以在非线上环境使用debug这个日志级别,这样可以减少线上不必要的日志。

1.5、注意事项

1、 禁止直接使用日志系统的API(比如log4j日志系统),应该使用门面模式提供的接口可以安装lombok插件,类上使用@Slf4j注解,然后代码中直接使用log.info(“message:{}”, msg);

2、 异常日志的打印不能丢失调用栈信息,可使用:log.error(“Exception message:{}”,msg, ex);

3、 日志打印使用debug时应该注意,生产环境是不打印出来的,考虑效率应该加入判断:

 if(log.isDebugEnabled()){
     log.debug("message: {}", object);
 }

4、 测试环境和线上环境禁止使用禁用 System.out.println 和 System.err.println;

5、 日志打印的上下文中如果有用户ID、业务ID或者流水号的,一定要打印出来,方便追踪。

2、sfl4j和log4j和lobback有什么区别

我在多点的时候用的是logback,并且将日志输出到额graylog中,小米的时候使用的是log4j2

slf4j 是打日志的。可以使用各种日志系统存储。Log4jlogback就是那个日志存储系统(Log4j它自带打日志,因为自己本身就是一个日志系统。所以不能够切换日志系统)。但是slf4j 是可以随时切换到任何日志系统,所以一般我们打日志都用SLF4J进行打日志吧!!!

SLF4J:即简单日志门面(Simple Logging Facade for Java),不是具体的日志解决方案,它只服务于各种各样的日志系统。SLF4J是一个用于日志系统的简单Facade,允许最终用户在部署其应用时使用其所希望的日志系统(Log4j logback)。 在使用SLF4J的时候,不需要在代码中或配置文件中指定你打算使用那个具体的日志系统,SLF4J提供了统一的记录日志的接口,只要按照其提供的方法记录即可,最终日志的格式、记录级别、输出方式等通过具体日志系统的配置来实现,因此可以在应用中灵活切换日志系统。

log4j:日志系统

logbacklogbacklog4j非常相似,Logback的内核重写了,在一些关键执行路径上性能提升10倍以上。而且logback不仅性能提升了,初始化内存加载也更小。但是说白了也是个日志系统

3、获取日志错误的行号,方法,报错信息

不要使用e.printStackTrace()

1、printStackTrace()打印出的堆栈日志跟业务代码日志是交错混合在一起的,排查异常日志不太方便。

2、e.printStackTrace()语句产生的字符串记录的是堆栈信息,如果信息太长太多,字符串常量池所在的内存块没有空间了,即内存满了,那么,用户的请求就卡住啦~

 StackTraceElement s= e.getStackTrace()[0];

记录报错的文件:s.getFileName()

记录报错的方法:s.getMethodName()

记录报错的行号: s.getLineNumber()

记录报错的信息(不全面)“ e.getMessage()

互利报错的类名字: e.getClassName()

打印详细的堆栈信息: logger.error("错误堆栈", e);

3.1、 StackTraceElement s= e.getStackTrace()[0];


public static void main(String[] args) {

    try {
        int i =1/0 ;
    }catch (Exception e){
        log(e,ExceptionLogUtils.class );
    }


}


public static void log(Throwable e,Class c){
    Logger logger = LoggerFactory.getLogger(c);

    StackTraceElement s= e.getStackTrace()[0];//数组长度为 1
    logger.error("\n\n-----------------"+
                 "\n报错文件名:"+s.getFileName()+
                 "\n报错的类:"+s.getClassName()+
                 "\n报错方法::"+s.getMethodName()+
                 "\n报错的行:"+ s.getLineNumber()+
                 "\n报错的message:"+ e.getMessage()+
                 "\n错误堆栈:\n"+getStackTrace(e)+
                 "\n------------------\n\n");
}



//获取堆栈信息
public static String getStackTrace(Throwable throwable){
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    try
    {
        throwable.printStackTrace(pw);
        return sw.toString();
    } finally
    {
        pw.close();
    }
}





11:40:53.732 [main] ERROR com.duodian.youhui.admin.utils.ExceptionLogUtils - 

-----------------
报错文件名:ExceptionLogUtils.java
报错的类com.duodian.youhui.admin.utils.ExceptionLogUtils
报错方法::main
报错的行68
报错的message/ by zero
错误堆栈
java.lang.ArithmeticException: / by zero
	at com.duodian.youhui.admin.utils.ExceptionLogUtils.main(ExceptionLogUtils.java:68)

------------------

3.2、 Thread.currentThread().getStackTrace()

public static void main(String[] args) {
    logInfo("HealerJean",ExceptionLogUtils.class);
}


public static void logInfo(String msg,Class c){
    Logger logger = LoggerFactory.getLogger(c);
    String location="";
    StackTraceElement[] stacks = Thread.currentThread().getStackTrace();
    System.out.println(stacks.length); //长度为3
    for(StackTraceElement stackTraceElement:stacks){
        logger.info("\n\n**************"+
                    "\n打印文件名:"+stackTraceElement.getFileName() +
                    "\n打印类名:"+ stackTraceElement.getClassName() +
                    "\n方法名:" +  stackTraceElement.getMethodName() +
                    "\n行号:"  +  stackTraceElement.getLineNumber() +
                    "\n打印内容:"+msg+
                    "\n**************\n\n");
        System.out.println(location);
    }

}





11:44:47.685 [main] INFO com.duodian.youhui.admin.utils.ExceptionLogUtils - 

**************
打印文件名Thread.java
打印类名java.lang.Thread
方法名getStackTrace
行号1559
打印内容:HealerJean
**************



11:44:47.689 [main] INFO com.duodian.youhui.admin.utils.ExceptionLogUtils - 

**************
打印文件名ExceptionLogUtils.java
打印类名com.duodian.youhui.admin.utils.ExceptionLogUtils
方法名logInfo
行号31
打印内容:HealerJean
**************



11:44:47.689 [main] INFO com.duodian.youhui.admin.utils.ExceptionLogUtils - 

**************
打印文件名ExceptionLogUtils.java
打印类名com.duodian.youhui.admin.utils.ExceptionLogUtils
方法名main
行号49
打印内容:HealerJean
**************

4、Throwable、Exception、Error

Throwable是java.lang包中一个专门用来处理异常的类。它有两个子类,即Error 和Exception,它们分别用来处理两组异常。

4.1、Error

**用来处理程序运行环境方面的异常,Error无法预期的错误因此,这是不可捕捉的,无法采取任何恢复的操作,一般只能显示错误的信息 **

比如,虚拟机错误、装载错误和连接错误,这类异常主要是和硬件有关的,而不是由程序本身抛出的。

比如 OutOfMemoryError,试多少次很大概率出错的。

4.2、Exception

java提供了两类主要的异常:运行时异常runtime exception和一般异常checked exception。

是在逻辑上又科分成检查异常和非检查异常

4.2.1、正常分类

4.2.1.1、运行时异常

Java程序运行时常常遇到的各种异常的处理,其中包括隐式异常。比如,程序中除数为0引起的错误、数组下标越界错误等,这类异常也称为运行时异常,,因为它们虽然是由程序本身引起的异常,但不是程序主动抛出的,而是在程序运行中产生的。

运行时异常我们可以不处理。这样的异常由虚拟机接管。出现运行时异常后,系统会把异常一直往上层抛,一直遇到处理代码。如果不对运行时异常进行处理,那么出现运行时异常之后,要么是线程中止,要么是主程序终

4.2.1.2、一般异常

这些异常也称为显式异常。它们都是在程序中用语句抛出、并且也是用语句进行捕获的,比如,文件没找到引起的异常、类没找到引起的异常等。

JAVA要求程序员对其进行catch。所以,面对这种异常不管我们是否愿意,只能 catch捕获,要么用throws字句声明抛出,交给它的父类处理,否则编译不会通过。

4.2.1、逻辑分类:

逻辑分类:checked检查异常和unchecked非检查异常。

checked Exception就是在写代码的时候,IDE(比如Eclipse)会要求你写try catch的那种Exception,比如IOException。这种Exception是Java的设计者要求你的程序去处理的。这种异常一般不会影响程序的主体,容易手动诊断修复,所以Java要求你在catch下面写出处理的代码,以保证程序遇到此类exception之后还可以正常运行

unchecked这一类就是你在代码处理了checked exception之后,你在运行时候依然会遇到的exception,所以又叫做RunTimeException,比如NullPointerException, IndexOutOfBoundsException。此类exception相较于前面那种更容易影响程序运行,从设计者角度不提倡从程序中catch出来并处理,当然你也可以这么做。


/**
* 将CheckedException转换为UncheckedException
*/
public static RuntimeException toUncheckedException(Exception e) {
    if (e instanceof RuntimeException) {
        return (RuntimeException) e;
    } else {
        return new RuntimeException(e);
    }
}

5、Logback

5.1、logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--学习 https://blog.csdn.net/ZYC88888/article/details/85060315-->

    <!--
     格式化输出:%d表示日期,
     %thread表示线程名,
     %-5level:级别从左显示5个字符宽度,
     %logger{50} 表示 Logger 名字最长36个字符,
     %msg:日志消息,
     %M : 日志输出所在方法名
     %L : 日志输出所在行数
     %n是换行符 -->
    -->
    <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level -[%-32X{REQ_UID}] - %msg  -%logger{50}.%M[%L]%n "/>



    <property name="LOG_PATH" value="/Users/healerjean/Desktop/logs"/>
    <property name="FILE_PATH_INFO"  value="${LOG_PATH}/hlj-logback.log"/>
    <property name="FILE_PATH_ERROR" value="${LOG_PATH}/hlj-logback-error.log"/>

    <!--控制台-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="com.hlj.proj.controller.config.LogbackJsonFilter"/>
        <encoder charset="UTF-8"  >
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>


    <appender name="FILE-INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--日志文件输出的文件名 -->
        <File>${FILE_PATH_INFO}</File>
        <!--滚动日志 基于时间和文件大小-->
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 滚动日志文件保存格式 i是超出文件大小MaxFileSize 讲历史日志后缀名从0开始起步,
            如果超过了最大的totalSizeCap,就会全部删除,重新开始-->
            <FileNamePattern>${FILE_PATH_INFO}.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
            <MaxFileSize>1MB</MaxFileSize>
            <totalSizeCap>5GB</totalSizeCap>
            <!--日志最大的历史 10天 -->
            <MaxHistory>10</MaxHistory>
        </rollingPolicy>
        <!-- 按临界值过滤日志:低于INFO以下级别被抛弃 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>


    <appender name="FILE-ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${FILE_PATH_ERROR}</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <FileNamePattern>${FILE_PATH_ERROR}.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
            <MaxFileSize>60MB</MaxFileSize>
            <totalSizeCap>5GB</totalSizeCap>
            <MaxHistory>10</MaxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <encoder>
            <pattern>${LOG_PATTERN}</pattern>
        </encoder>
    </appender>



    <!--以配置文件application.properties 中为主,如果配置文件中不存在以它为主-->
    <root level="info">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="FILE-ERROR"/>
        <appender-ref ref="FILE-INFO"/>
    </root>
</configuration>





5.2、Logback日志到数据库

5.2.1、创建数据库表


DROP TABLE IF EXISTS logging_event_property;
DROP TABLE IF EXISTS logging_event_exception;
DROP TABLE IF EXISTS logging_event;


CREATE TABLE logging_event
(
  timestmp          BIGINT       NOT NULL,
  formatted_message TEXT         NOT NULL,
  logger_name       VARCHAR(254) NOT NULL,
  level_string      VARCHAR(254) NOT NULL,
  thread_name       VARCHAR(254),
  reference_flag    SMALLINT,
  arg0              VARCHAR(254),
  arg1              VARCHAR(254),
  arg2              VARCHAR(254),
  arg3              VARCHAR(254),
  caller_filename   VARCHAR(254) NOT NULL,
  caller_class      VARCHAR(254) NOT NULL,
  caller_method     VARCHAR(254) NOT NULL,
  caller_line       CHAR(4)      NOT NULL,
  event_id          BIGINT       NOT NULL AUTO_INCREMENT PRIMARY KEY
)


CREATE TABLE logging_event_property
(
event_id          BIGINT NOT NULL,
mapped_key        VARCHAR(150) NOT NULL,
mapped_value      TEXT,
PRIMARY KEY(event_id, mapped_key),
FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
);

CREATE TABLE logging_event_exception
(
event_id         BIGINT NOT NULL,
i                SMALLINT NOT NULL,
trace_line       VARCHAR(254) NOT NULL,
PRIMARY KEY(event_id, i),
FOREIGN KEY (event_id) REFERENCES logging_event(event_id)
);



5.2.3、日志配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>


  
 <!--日志异步到数据库 -->
    <appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
        <!--日志异步到数据库-->
        <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
            <driverClass>com.mysql.jdbc.Driver</driverClass>
            <url>jdbc:mysql://localhost:3306/healerjean?useUnicode=true&amp;allowMultiQueries=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false</url>
            <user>healerjean</user>
            <password>healerjean</password>
        </connectionSource>
    </appender>



    <root level="info">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="DB"/>
    </root>
</configuration>



5.2.3、查看日志

SELECT * from logging_event;

WX20180910-123004

5.3、LogBack打印Json数据

<!--控制台-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="com.hlj.proj.controller.config.LogbackJsonFilter"/>
    <encoder charset="UTF-8"  >
        <pattern>${LOG_PATTERN}</pattern>
    </encoder>
</appender>
package com.hlj.proj.controller.config;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;
import com.hlj.proj.controller.utils.JsonUtils;



public class LogbackJsonFilter extends Filter<ILoggingEvent> {
    @Override
    public FilterReply decide(ILoggingEvent event) {
        if (event.getLoggerName().startsWith("com.hlj")) {
            Object[] params = event.getArgumentArray();
            for (int index = 0; index < params.length; index++) {
                Object param = params[index];
                // class.isPrimitive() 8种基本类型的时候为 true,其他为false
                if (!param.getClass().isPrimitive()) {
                    params[index] = JsonUtils.toJsonString(param);
                }
            }
        }
        return FilterReply.ACCEPT;
    }
}

6、Log4j

6.1、log4j.properties

## 必填内容,info/all/.., stdout 为必填,后面的根据log4j.appender.内容进行填写,如果下面有内容则,这里必须加上,
#  level 是日志记录的优先级,分为OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者自定义的级别。
log4j.rootLogger=info, stdout, log, errorlog,proj



#%d: 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921
#%p: 输出日志信息优先级,即DEBUG,INFO,WARN,ERROR,FATAL,
#%t: 输出产生该日志事件的线程名
#%C: 输出日志信息所属的类目,通常就是所在类的全名
#%M: 输出代码中指定的消息,产生的日志具体信息
#%F: 输出日志消息产生时所在的文件名称
#%L: 输出代码中的行号
#%l: 输出日志事件的发生位置,相当于%C.%M(%F:%L)的组合,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)
#%r: 输出自应用启动到输出该log信息耗费的毫秒数
#%x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中。
#%%: 输出一个”%”字符
#%n: 输出一个回车换行符,Windows平台为”\r\n”,Unix平台为”\n”输出日志信息换行
#%hostName : 本地机器名
#%hostAddress : 本地ip地址-->
#可以在%与模式字符之间加上修饰符来控制其最小宽度、最大宽度、和文本的对齐方式。如:
#1) c:指定输出category的名称,最小的宽度是20,如果category的名称小于20的话,默认的情况下右对齐。
#2)%-20c:指定输出category的名称,最小的宽度是20,如果category的名称小于20的话,”-”号指定左对齐。
#3)%.30c:指定输出category的名称,最大的宽度是30,如果category的名称大于30的话,就会将左边多出的字符截掉,但小于30的话也不会有空格。
#4) .30c:如果category的名称小于20就补空格,并且右对齐,如果其名称长于30字符,就从左边交远销出的字符截掉


# RollingFileAppender按log文件最大长度限度生成新文件
# DailyRollingFileAppender按日期生成新文件,不能根据大小清除历史日志,但是我们可以自定义来实现

# %d{yyyy-MM-dd HH:mm:ss,SSS}日期    %p級別 %t当前线程名称   %m日志信息   [%C.%M]类名加方法    %L行数 %n换行
# 举例 # 2019-07-13 09:05:14,674  [INFO]-[http-nio-8888-exec-1] info日志==================  com.hlj.proj.controler.Log4jController.log4j][25]



# 控制台输出
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS}  [%p]-[%t] %m  %C.%M][%L] %n




## 根据日期生成配置文件当前log.log 如果时间超过了设置的格式的时间DatePattern 则会在后面加上    log.log.2019-07-12.log
#  解释:也就是说log文件会暂存每天的日志,到第二天时会再加上yyyy-MM,产生当天的完整日志文件
### Log info
log4j.appender.log = org.apache.log4j.DailyRollingFileAppender
log4j.appender.log.File = /Users/healerjean/Desktop/logs/hlj-log4j.log
log4j.appender.log.Append = true
log4j.appender.log.Threshold = INFO
#超过日期则讲历史日志加上后缀日期用于区分
log4j.appender.log.DatePattern='.'yyyy-MM-dd'.log'
log4j.appender.log.layout = org.apache.log4j.PatternLayout
log4j.appender.log.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS}  [%p]-[%t] %m  %C.%M][%L] %n





## 5  按照文件大小进行日志切分 文件历史日志依次 error.log.1 error.log.2
log4j.appender.errorlog=org.apache.log4j.RollingFileAppender
log4j.appender.errorlog.File=/Users/healerjean/Desktop/logs/error.log
log4j.appender.errorlog.Append=true
log4j.appender.errorlog.Threshold=error
#设置日志文件的大小
log4j.appender.errorlog.MaxFileSize=2000KB
#保存200个备份文件
log4j.appender.errorlog.MaxBackupIndex=200
log4j.appender.errorlog.layout=org.apache.log4j.PatternLayout
log4j.appender.errorlog.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS}   [%p]-[%t] %m  %C.%M][%L %n



log4j.appender.proj=com.hlj.proj.utils.RoolingAndDateFileAppender
log4j.appender.proj.file=/Users/healerjean/Desktop/logs/logRecoed.log
log4j.appender.proj.Append=true
log4j.appender.proj.DatePattern='.'yyyy-MM-dd'.log'
log4j.appender.proj.Threshold=error
#设置日志文件的大小
log4j.appender.proj.MaxFileSize=5KB
#最大保留多少个文件,超过之后会进行重新命名,所以尽量不要超过
log4j.appender.proj.maxIndex=10
#只保留多长时间的
log4j.appender.proj.expirDays=1
log4j.appender.proj.layout=org.apache.log4j.PatternLayout
log4j.appender.proj.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS}  [%p]-[%t] %m  %C.%M][%L] %n




7、Log4j2

log4j一直存在两个问题,一是打日志影响到系统性能效率,二是有多线程的时候,日志会比较乱

log4j2是log4j 1.x 的升级版,参考了logback的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升,主要有:

异常处理,在logback中,Appender中的异常不会被应用感知到,但是在log4j2中,提供了一些异常处理机制。

性能提升, log4j2相较于log4j 1和logback都具有很明显的性能提升,后面会有官方测试的数据。

自动重载配置,参考了logback的设计,当然会提供自动刷新参数配置,最实用的就是我们在生产上可以动态的修改日志的级别而不需要重启应用——那对监控来说,是非常敏感的。

无垃圾机制,log4j2在大部分情况下,都可以使用其设计的一套无垃圾机制,避免频繁的日志收集导致的jvm gc。

7.1、log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>


<!--status  Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,
 你会看到log4j2内部各种详细输出。可以设置成OFF(关闭)或Error(只输出错误信息)-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration status="error" monitorInterval="30">
    <!--    %d{yyyy-MM-dd HH:mm:ss, SSS} : 日志生产时间-->
    <!--    %p : 日志输出格式-->
    <!--    %thread表示线程名,-->
    <!--    %c : logger的名称-->
    <!--    %m : 日志内容,即 logger.info("message")-->
    <!--    %n : 换行符-->
    <!--    %C : Java类名-->
    <!--    %L : 日志输出所在行数-->
    <!--    %M : 日志输出所在方法名-->
    <!--    hostName : 本地机器名-->
    <!--    hostAddress : 本地ip地-->

    <!-- 日志文件目录和压缩文件目录配置 -->
    <Properties>
        <Property name="level">info</Property>
        <Property name="LOG_PATTERN">
            %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level -[%-32X{REQ_UID}]- %msg%xEx %logger{36}.%M[%L]%n
        </Property>

        <Property name="logDri">/Users/healerjean/Desktop/logs</Property>
        <Property name="logFileName">hlj-client</Property>

        <Property name="infoLogDri">${logDri}/info</Property>
        <Property name="infoLogGz">${infoLogDri}/gz</Property>
        <Property name="infoLogFileName">${logFileName}.log</Property>

        <Property name="errorLogDri">${logDri}/error</Property>
        <Property name="errorLogGz">${errorLogDri}/gz</Property>
        <Property name="errorLogFileName">${logFileName}.error</Property>

    </Properties>

    <appenders>
        <console name="Console" target="SYSTEM_OUT">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="${level}" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
            <!--输出日志的格式-->
            <PatternLayout pattern="${LOG_PATTERN}"/>
        </console>


        <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingRandomAccessFile name="infoFile" fileName="${infoLogDri}/${infoLogFileName}"
                                 filePattern="${infoLogGz}/$${date:yyyy-MM}/%d{yyyy-MM-dd}-%i.${infoLogFileName}.gz">
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <!-- 基于指定文件大小的滚动策略,size属性用来定义每个日志文件的大小 -->
                <SizeBasedTriggeringPolicy size="500 MB"/>
                <!-- 基于时间的滚动策略,interval属性用来指定多久滚动一次,默认是1 hour -->
                <TimeBasedTriggeringPolicy interval="6" modulate="true"/>
            </Policies>
            <Filters>
                <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)
                onMatch属性设置为DENY,过滤掉高等级的日志;onMismatch设置为NEUTRAL,把低等级的日志放行,
                -->
                <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
            <!-- 指定每天(文件夹是以天的,看上面的)的最大压缩包个数,默认7个,超过了会覆盖之前的(用来指定同一个文件夹下最多有几个日志文件时开始删除最旧的,创建新的(通过max属性)) -->
            <DefaultRolloverStrategy max="2000"/>
        </RollingRandomAccessFile>


        <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingRandomAccessFile name="errorFile" fileName="${errorLogDri}/${errorLogFileName}"
                                 filePattern="${errorLogGz}/$${date:yyyy-MM}/%d{yyyy-MM-dd}-%i.${errorLogFileName}.gz">
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <!-- 基于指定文件大小的滚动策略,size属性用来定义每个日志文件的大小 -->
                <SizeBasedTriggeringPolicy size="500 MB"/>
                <!-- 基于时间的滚动策略,interval属性用来指定多久滚动一次,默认是1 hour -->
                <TimeBasedTriggeringPolicy interval="6" modulate="true"/>
            </Policies>
            <Filters>
                <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)
                onMatch属性设置为DENY,过滤掉高等级的日志;onMismatch设置为NEUTRAL,把低等级的日志放行,
                -->
                <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
            <!-- 指定每天(文件夹是以天的,看上面的)的最大压缩包个数,默认7个,超过了会覆盖之前的(用来指定同一个文件夹下最多有几个日志文件时开始删除最旧的,创建新的(通过max属性)) -->
            <DefaultRolloverStrategy max="2000"/>
        </RollingRandomAccessFile>


    </appenders>


    <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>
        <!-- AsyncRoot - 异步记录日志 - 需要LMAX Disruptor的支持 -->
        <!-- additivity如果设置为true将会输出两次日志,意思和log4j里面意思是否追加 -->
        <AsyncRoot level="${level}" additivity="false" includeLocation="true">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="infoFile"/>
            <AppenderRef ref="errorFile"/>
        </AsyncRoot>
    </loggers>


</configuration>

7.1.1、日志打印位置

main方法和服务器日志都在一起

image-20200612153846792

image-20200612153853073

image-20200612153900308

image-20200612153907611

8、日志唯一标识追踪

1、Controll入参和出参打印

2、唯一标识

8.1、日志格式

8.1.1、log4j

%d{yyyy-MM-dd HH:mm:ss} %-5level -[%-32X{REQ_UID}]- %msg%xEx %logger{36}.%M[%L]%n

8.1.1、logback

%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level -[%-32X{REQ_UID}] - %msg  -%logger{50}.%M[%L]%n

8.2、过滤器Log4j2ReqUidFilter

package com.healerjean.proj.config.filter;


import org.slf4j.MDC;

import javax.servlet.*;
import java.io.IOException;
import java.util.UUID;

/**
 * @author HealerJean
 * @ClassName Log4j2Filter
 * @date 2020/6/15  20:12.
 * @Description
 */
public class Log4j2ReqUidFilter implements Filter {

    private static final String REQ_UID = "REQ_UID";
    private FilterConfig filterConfig;


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        MDC.put(REQ_UID, UUID.randomUUID().toString().replace("-", ""));
        filterChain.doFilter(servletRequest, servletResponse);
        MDC.remove(REQ_UID);
    }

    @Override
    public void destroy() {
        this.filterConfig = null;
    }



}



@Bean
public FilterRegistrationBean log4j2Fiter() {
    FilterRegistrationBean fitler = new FilterRegistrationBean();
    fitler.setFilter(new Log4j2Filter());
    fitler.addUrlPatterns("/*");
    fitler.setName("log4j2Fiter");
    fitler.setDispatcherTypes(DispatcherType.REQUEST);
    return fitler;
}

9、日志Controller出参入参打印

9.1、pom依赖

<!-- aop 切面 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

9.2、自定义注解标识方法名字

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InterfaceName {

    String value() default "";
}
@InterfaceName("demo控制器--------demo实体")
@ApiOperation(value = "demo实体",
              notes = "demo实体",
              consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
              produces = MediaType.APPLICATION_JSON_UTF8_VALUE,
              response = DemoDTO.class)
@GetMapping(value = "demo/get", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public ResponseBean get(DemoDTO demoDTO) {
    String validate = ValidateUtils.validate(demoDTO, ValidateGroup.HealerJean.class);
    if (!validate.equals(CommonConstants.COMMON_SUCCESS)) {
        throw new BusinessException(ResponseEnum.参数错误, validate);
    }
    return ResponseBean.buildSuccess(demoEntityService.getMmethod(demoDTO));
}

9.3、AOP拦截

package com.healerjean.proj.config.aspect;

import com.healerjean.proj.annotation.InterfaceName;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
@Slf4j
@Order(1)
public class ControllerLogAspect {



    @Around("execution(* com.healerjean.proj.controller.*Controller.*(..))")
    public Object handleControllerLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Signature signature = proceedingJoinPoint.getSignature();
        String className = signature.getDeclaringTypeName();
        String methodName = signature.getName();
        Object[] args = proceedingJoinPoint.getArgs();

        String value = "";
        Method method = ((MethodSignature) signature).getMethod();
        if (method.isAnnotationPresent(InterfaceName.class)) {
            value = "请求接口:【" + method.getAnnotation(InterfaceName.class).value() + "】,";
        }

        long start = System.currentTimeMillis();
        try {
            log.info("请求开始:{}类名:【{}】,方法名:【{}】, 参数:【{}】", value, className, methodName, args);
            Object result = proceedingJoinPoint.proceed();
            long timeCost = System.currentTimeMillis() - start;
            log.info("请求结束:{}类名:【{}】, 方法名:【{}】, 参数:【{}】, 返回值:{}, 耗时:{}ms。", value, className, methodName, args, result, timeCost);
            return result;
        } catch (Exception e) {
            long timeCost = System.currentTimeMillis() - start;
            log.info("请求出错:{}类名:【{}】,方法名:【{}】, 参数:【{}】, 耗时:【{}】ms。", value, className, methodName, args, timeCost);
            throw e;
        }
    }
}

10、dubbo日志追踪

10.1、服务提供者

10.1.1、aop切面pom依赖

<!-- aop 切面 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

10.1.2、ServiceLogAspect接口出入参日志打印

package com.healerjean.proj.config.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;


@Aspect
@Component
@Slf4j
@Order(1)
public class ServiceLogAspect {

    @Around("execution(* com.healerjean.proj.service.*Service.*(..))")
    public Object handleControllerLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Signature signature = proceedingJoinPoint.getSignature();
        String className = signature.getDeclaringTypeName();
        String methodName = signature.getName();
        Object[] args = proceedingJoinPoint.getArgs();
        long start = System.currentTimeMillis();
        try {
            log.info("请求开始:类名:【{}】,方法名:【{}】, 参数:【{}】", className, methodName, args);
            Object result = proceedingJoinPoint.proceed();
            long timeCost = System.currentTimeMillis() - start;
            log.info("请求结束:类名:【{}】, 方法名:【{}】, 参数:【{}】, 返回值:{}, 耗时:{}ms。", className, methodName, args, result, timeCost);
            return result;
        } catch (Exception e) {
            long timeCost = System.currentTimeMillis() - start;
            log.info("请求出错:类名:【{}】,方法名:【{}】, 参数:【{}】, 耗时:【{}】ms。", className, methodName, args, timeCost);
            throw e;
        }
    }
}

10.1.3、ProviderRpcTraceFilterdubbo日志追踪过滤器

package com.healerjean.proj.config.dubbo;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.*;
import com.alibaba.dubbo.rpc.protocol.dubbo.filter.FutureFilter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;

import java.util.UUID;

@Activate(group = Constants.PROVIDER, order = 1)
public class ProviderRpcTraceFilter extends FutureFilter {


    private static final String DUBBO_REQ_UID = "REQ_UID";

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String reqUid = RpcContext.getContext().getAttachment(DUBBO_REQ_UID);
        if (StringUtils.isBlank(reqUid)) {
            //传递丢失
            reqUid = "CUSTOM:" + UUID.randomUUID().toString().replace("-", "");
        }
        MDC.put(DUBBO_REQ_UID, reqUid);
        RpcContext.getContext().setAttachment(DUBBO_REQ_UID, reqUid);
        try {
            return invoker.invoke(invocation);
        } finally {
            MDC.remove(DUBBO_REQ_UID);
        }
    }
}

10.1.4、配置dubbo过滤器

![image-20200616112740541]https://raw.githubusercontent.com/HealerJean/HealerJean.github.io/master/blogImages/image-20200616112740541.png)

com.alibaba.dubbo.rpc.Filter

ProviderRpcTraceFilter=com.healerjean.proj.config.dubbo.ProviderRpcTraceFilter

10.2、服务消费者

10.2.1、aop切面pom依赖

<!-- aop 切面 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

10.2.2、ControllerLogAspectcontroller出入参打印

package com.healerjean.proj.config.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;


@Aspect
@Component
@Slf4j
@Order(1)
public class ControllerLogAspect {

    @Around("execution(* com.healerjean.proj.controller.*Controller.*(..))")
    public Object handleControllerLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Signature signature = proceedingJoinPoint.getSignature();
        String className = signature.getDeclaringTypeName();
        String methodName = signature.getName();
        Object[] args = proceedingJoinPoint.getArgs();
        long start = System.currentTimeMillis();
        try {
            log.info("请求开始:类名:【{}】,方法名:【{}】, 参数:【{}】", className, methodName, args);
            Object result = proceedingJoinPoint.proceed();
            long timeCost = System.currentTimeMillis() - start;
            log.info("请求结束:类名:【{}】, 方法名:【{}】, 参数:【{}】, 返回值:{}, 耗时:{}ms。", className, methodName, args, result, timeCost);
            return result;
        } catch (Exception e) {
            long timeCost = System.currentTimeMillis() - start;
            log.info("请求出错:类名:【{}】,方法名:【{}】, 参数:【{}】, 耗时:【{}】ms。", className, methodName, args, timeCost);
            throw e;
        }
    }
}

10.2.3、日志追踪过滤器

package com.healerjean.proj.config.filter;


import org.slf4j.MDC;

import javax.servlet.*;
import java.io.IOException;
import java.util.UUID;

/**
 * @author HealerJean
 * @ClassName Log4j2Filter
 * @date 2020/6/15  20:12.
 * @Description
 */
public class Log4j2ReqUidFilter implements Filter {

    private static final String REQ_UID = "REQ_UID";
    private FilterConfig filterConfig;


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        this.filterConfig = filterConfig;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        MDC.put(REQ_UID, UUID.randomUUID().toString().replace("-", ""));
        filterChain.doFilter(servletRequest, servletResponse);
        MDC.remove(REQ_UID);
    }

    @Override
    public void destroy() {
        this.filterConfig = null;
    }



}



@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter {

    @Bean
    public FilterRegistrationBean log4j2ReqUidFilter() {
        FilterRegistrationBean fitler = new FilterRegistrationBean();
        fitler.setFilter(new Log4j2ReqUidFilter());
        fitler.addUrlPatterns("/*");
        fitler.setName("Log4j2ReqUidFilter");
        fitler.setDispatcherTypes(DispatcherType.REQUEST);
        return fitler;
    }
}

10.2.4、ConsumerRpcTraceFilterdubbo日志追踪过滤器

package com.healerjean.proj.config.dubbo;

import com.alibaba.dubbo.common.Constants;
import com.alibaba.dubbo.common.extension.Activate;
import com.alibaba.dubbo.rpc.*;
import com.alibaba.dubbo.rpc.protocol.dubbo.filter.FutureFilter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;

import java.util.UUID;

@Activate(group = Constants.CONSUMER, order = 1)
public class ConsumerRpcTraceFilter extends FutureFilter {


    private static final String DUBBO_REQ_UID = "REQ_UID";

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcContext.getContext().setAttachment(DUBBO_REQ_UID, MDC.get(DUBBO_REQ_UID));
        return invoker.invoke(invocation);
    }
}

10.2.5、配置dubbo过滤器

![image-20200616113137513]https://raw.githubusercontent.com/HealerJean/HealerJean.github.io/master/blogImages/image-20200616113137513.png)

com.alibaba.dubbo.rpc.Filter

ConsumerRpcTraceFilter=com.healerjean.proj.config.dubbo.ConsumerRpcTraceFilter

ContactAuthor