背景

从毕业到现在参加工作已经有两年了,在这两年期间,经历的几个大项目和一些小项目,每个项目都涉及到日志这个模块。存在的意义无非就是两个:

  1. 记录业务日志,方便后期排查问题
  2. 以日志上的形式记录业务数据,并将日志文件推送第三方

每次接手一个新项目,就会先问有没有配置日志,如果没有配置日志模块,那么排查问题就是一个很恼火的问题,不清楚请求参数是什么,走了哪个判断,调用第三方接口的参数和返回结果是什么,一切的问题都需要一步一步的调试且耗时耗力。自从意识到这个问题后,每次写代码都会加入日志,并将请求参数,返回结果,耗时的数据写入日志,方便后期排查问题。

目前接触比较多Java 开源框架是SpringBoot,默认的日志实现是LogBack,通过配置xml文件就可以输出日志。之前在配置LogBack时,总是无脑的将其他项目的配置复制过来,也不懂为什么要这样配置。通过这几天的学习,把自己对logback的日志记录如下。

基础知识

日志级别

日志输出级别从大高到低为:

  1. OFF
  2. FATAL
  3. ERROR
  4. WARN
  5. INFO
  6. DEBUG
  7. TRACE
  8. ALL

日常开发中常用的是:

  1. ERROR:用于输出影响系统正常运行的信息
  2. WARN:用于输出错误信息,但不影响系统正常运行
  3. INFO:用于记录正常业务日志信息,方便后期的问题排查
  4. DEBUG:用于开发环境下的信息输出

方法打印

推荐使用Slf4j记录日志,在LogBack不被允许的情况下可以方便的切换到其他日志系统。实例化logger对象:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class XXX {
    public static final Logger log = LoggerFactory.getLogger(XXX.class);
}

如果使用了lombok,则可以使用@Slf4j注解实例化logger对象

  • 输出ERROR日志
log.error("this is error log;time:{}", System.currentTimeMillis());
  • 输出WARN日志
log.warn("this is warn log;time:{}", System.currentTimeMillis());
  • 输出INFO日志
log.info("this is info log;time:{}", System.currentTimeMillis());
  • 输出DEBUG日志
        if (log.isDebugEnabled()) {
            log.debug("this is debug log;time:{}", System.currentTimeMillis());
        }

在开发中,使用参数化日志而不是字符串拼接,可以调高代码可读性并提高性能,在记录debug日志时,先判断目前所处环境是否支持debug输出,因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅 是替换动作,可以有效提升性能。(摘自:阿里巴巴Java开发手册嵩山版)

日志规约

image
[Java开发手册(泰山版)]
image
image
Java开发手册(泰山版)

配置LogBack

在SpringBoot框架中使用LogBack只需在resources目录下添加配置文件即可,可以项目启动日志设置为debug查看其初始化步骤:

23:42:43,896 |-INFO in ch.qos.logback.classic.LoggerContext[heron] - Could NOT find resource [logback-test.xml]
23:42:43,897 |-INFO in ch.qos.logback.classic.LoggerContext[heron] - Could NOT find resource [logback.groovy]
23:42:43,897 |-INFO in ch.qos.logback.classic.LoggerContext[heron] - Found resource [logback.xml] at [file:/E:/heron/target/classes/logback.xml]

这里使用logback.xml方式配置LogBack。

配置文件的组成

<configuration scan="true" scanPeriod="5 seconds">
    <property name="appName" value="heron"/>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="webiste.lhc.heron" level="debug" additivity="false">
        <appender-ref ref="rollingFileAppender"/>
    </logger>


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

<configuration>

整个XML文件由<configuration></configuration>标签包裹,属性含义:
  • scan: 扫描配置文件,查看配置文件是否有改动
  • scanPeriod:扫描时间间隔

<property>

用于定义变量,例如:
<!--定义项目名称-->
<property name="appName" value="heron"/>
<!--定义日志路径-->
<property name="logPath" value="/data/logs"/>

<appender>

输出日志的核心,定义了日志的数据格式、日志类型、日志文件大小、日志保存时间和轮转策略

属性含义:

  • name:定义logger所用应用的名称,名称随意
  • class:定义日志输出实现类,常用的输出包括:控制台,文件和消息中间件
<encoder>
决定通过哪种方式写日志
<pattern>
定义日志输出格式,例如:年月日时分秒和毫秒信息,线程名称,类名,日志级别

<logger>

用于设置一个包或一个类的日志打印级别

属性含义:

  • name:指定包名或类名
  • level:定义当前包或类应输出那个范围的日志
  • additivity:是否继承<root>标签的日志级别定义

在<logger>标签中应用已定义的<appender> ,可以指定一个类或一个包如何输出日志;<logger>可以包含零个或多个<appender-ref>

<root>

<root>也是一个<logger>,但是它是根logger,且只有一个属性,即:level

日志输出至控制台

在没有日志配置文件之前,SpringBoot框架就会将所有日志输出至控制台,这里是通过BasicConfigurator类来实现的

package ch.qos.logback.classic;

import ch.qos.logback.classic.layout.TTLLLayout;
import ch.qos.logback.classic.spi.Configurator;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.encoder.LayoutWrappingEncoder;
import ch.qos.logback.core.spi.ContextAwareBase;

/**
 * BasicConfigurator configures logback-classic by attaching a 
 * {@link ConsoleAppender} to the root logger. The console appender's layout 
 * is set to a {@link ch.qos.logback.classic.layout.TTLLLayout TTLLLayout}.
 * 
 * @author Ceki G&uuml;lc&uuml;
 */
public class BasicConfigurator extends ContextAwareBase implements Configurator {

    public BasicConfigurator() {
    }

    public void configure(LoggerContext lc) {
        addInfo("Setting up default configuration.");
        
        ConsoleAppender<ILoggingEvent> ca = new ConsoleAppender<ILoggingEvent>();
        ca.setContext(lc);
        ca.setName("console");
        LayoutWrappingEncoder<ILoggingEvent> encoder = new LayoutWrappingEncoder<ILoggingEvent>();
        encoder.setContext(lc);
        
 
        // same as 
        // PatternLayout layout = new PatternLayout();
        // layout.setPattern("%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n");
        TTLLLayout layout = new TTLLLayout();
 
        layout.setContext(lc);
        layout.start();
        encoder.setLayout(layout);
        
        ca.setEncoder(encoder);
        ca.start();
        
        Logger rootLogger = lc.getLogger(Logger.ROOT_LOGGER_NAME);
        rootLogger.addAppender(ca);
    }
}

查看TTLLLayout中的doLayout()方法完成日志格式的拼接。

可以在配置文件中自定义控制台日志的输出格式,并在<root>标签中引用即可

<configuration scan="true" scanPeriod="5 seconds">
    <!--控制台日志-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--默认配置-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n</pattern>
            <!--只输出内容-->
            <pattern>%msg%n</pattern>
        </encoder>
    </appender>
    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

控制台日志的实现类是ch.qos.logback.core.ConsoleAppender,在服务器上使用nohup java -jar xxx.jar命令启动服务时,项目的所有日志会保存在当前目录下的nohup.out文件中,也可以使用nohup java -jar xxx.jar > start.log将日志输出至指定目录下的指定文件中。这样做的缺点是:项目的启动日志和业务日志保存在同一个文件中,没有单独的划分。

日志输出至文件

为了解决上面存在的问题,就需要将启动日志和业务日志分隔开来,并输出至两个不同的文件中,且启动日志在项目正常启动之后并不需要经常查看。所以可以使用LogBack提供的ch.qos.logback.core.FileAppender,将业务日志以指定的格式作为输出。

<configuration scan="true" scanPeriod="5 seconds">
    <property name="appName" value="heron"/>
    <property name="logPath" value="/tmp/logs"/>
    <!--控制台日志-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n</pattern>
        </encoder>
    </appender>
    <!--业务日志-->
    <appender name="file" class="ch.qos.logback.core.FileAppender">
        <file>${logPath}/${appName}.log</file>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n</pattern>
        </encoder>
    </appender>
    <!--配置FileAppender引用-->
    <logger name="webiste.lhc.heron" level="debug" additivity="false">
        <appender-ref ref="file"/>
    </logger>
    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

上面的配置中使用了两个变量appNamelogPath,使用${}的方式定义了业务日志要输出的路径和文件名称。

在使用nohup java -jar xxx.jar > start.log 2>&1 &命令启动服务时,就会出现两个文件:当前目录下的start.log和/tmp/logs/heron.log文件。在查看start.log文件没有明显的报错时,就可以查看heron.log文件中所存储的内容。若要将日志文件按月,按天,按小时存储,则可以使用<timestamp>做划分。

<timestamp key="bySecond" datePattern="yyyy-MM-dd"/>
<appender name="file" class="ch.qos.logback.core.FileAppender">
    <file>${logPath}/${bySecond}/${appName}.log</file>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n</pattern>
    </encoder>
</appender>

这里使用yyyy-MM-dd的方式按天存储日志,并在<file>标签以变量的方式应用接口,有了这个配置,业务日志的存储路径为/tmp/logs/2020-10-01/heron.log。每个文件夹下就存在一个heron.log文件。由此,又出现了一个问题。若该系统用户访问量少,日志文件体积小,只生成一个log是说的过去的。但在一个大量用户访问的系统中,每天都会产生大量的业务日志且日志文件的体积也比较大。有些项目的日志需要在服务器保存特定的时间以备相关部门的检查。

日志轮转

为了解决上述问题,logback提供了ch.qos.logback.core.rolling.RollingFileAppender。规定日志以某种方式轮转,解决了单个日志文件体积过大,且需要手动删除早期日志的问题。

ch.qos.logback.core.rolling.RollingFileAppender存在两个重要的子组件:

  • RollingPolicy:负责日志轮转的功能(怎么做)
  • TriggeringPolicy:负责日志轮转的时机(何时做)

RollingPolicy

上图为RollingPolicy接口的实现类,每个实现类完成不同的功能。

  • TimeBasedRollingPolicy:基于时间的轮转策略。即负责怎么做,又决定何时做。以时间维度轮转。
  • SizeAndTimeBasedRollingPolicy: 是TimeBasedRollingPolicy子类,基于文件大小和时间的轮转策略,即可以显示单个日志文件的大小,也可以限制总文件的大小
  • FixedWindowRollingPolicy:根据固定窗口算法重命名文件

TriggeringPolicy

触发策略可以分为两类:

  • 基于时间的触发(常用)
  • 基于文件尺寸的触发

TimeBasedRollingPolicy即负责轮转的行为,又负责触发轮转

ConsoleAppender、FileAppender和RollingFileAppender三者关系

日志输出格式

以默认日志输出格式为例,每个参数的意义

%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n
  • %d{yyyy-MM-dd HH:mm:ss}:格式化输出当前时间,pattern语法与SimpleDateFormat相同。
  • [%-5level]:输出日志记录事件的级别
  • %logger{36}:logger名称
  • [%file:%line]:输出类名称和行号
  • %m:内容
  • %n:换行

更多配置信息,点击查看

logback配置模板

几个拿来即用的配置模板,方便你我他
  • 启动日志和业务日志分离,业务日志基于文件尺寸和时间轮转,单个文件最大10M,保留15天,日志总计5GB
<configuration scan="true" scanPeriod="5 seconds">
    <contextName>heron</contextName>
    <property name="appName" value="项目名称"/>
    <property name="logPath" value="日志保存路径"/>
    <!--控制台日志-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{16} [%file:%line] - %msg%n</pattern>
        </encoder>
    </appender>
    <!--基于时间和文件尺寸的轮转-->
    <appender name="rollingFileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logPath}/${appName}/${appName}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${logPath}/${appName}/%d{yyyy-MM-dd}/${appName}-%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>15</maxHistory>
            <totalSizeCap>5GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
    <logger name="项目名称" level="debug" additivity="false">
        <appender-ref ref="rollingFileAppender"/>
    </logger>
    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>
  • 分别输出error info debug日志到不同的文件,并以时间和文件尺寸轮转
<configuration scan="true" scanPeriod="5 seconds">
    <contextName>heron</contextName>
    <property name="appName" value="app"/>
    <property name="logPath" value="/mntlogs"/>

    <!--控制台日志-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%-5level] %logger{36} [%file:%line] - %msg%n</pattern>
        </encoder>
    </appender>

    <!--error级别-->
    <appender name="errorRolling" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logPath}/${appName}/error.log</file>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${logPath}/${appName}/%d{yyyy-MM-dd}/${appName}-error-%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>15</maxHistory>
            <totalSizeCap>5GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{15} - %msg%n</pattern>
        </encoder>
    </appender>


    <!--info级别-->
    <appender name="infoRolling" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logPath}/${appName}/info.log</file>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${logPath}/${appName}/%d{yyyy-MM-dd}/${appName}-info-%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>15</maxHistory>
            <totalSizeCap>5GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{15} - %msg%n</pattern>
        </encoder>
    </appender>


    <!--debug级别-->
    <appender name="debugRolling" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logPath}/${appName}/debug.log</file>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${logPath}/${appName}/%d{yyyy-MM-dd}/${appName}-debug-%i.log</fileNamePattern>
            <maxFileSize>10MB</maxFileSize>
            <maxHistory>15</maxHistory>
            <totalSizeCap>5GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{15} - %msg%n</pattern>
        </encoder>
    </appender>


    <logger name="项目类名" level="debug" additivity="false">
        <appender-ref ref="errorRolling"/>
        <appender-ref ref="infoRolling"/>
        <appender-ref ref="debugRolling"/>
    </logger>

    <!--spring data mongo输出查询语句-->
    <logger name="org.springframework.data.mongodb.core" level="debug" additivity="false">
        <appender-ref ref="debugRolling"/>
    </logger>


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

原文地址:https://www.haicheng.website/...


MakeFunny
16 声望4 粉丝

记录我的代码历程