文章

Java日志

java⽇志体系

一、体系概述

image-20241028222041294

1.1 ⽇志接⼝

  • JCL:Apache基⾦会所属的项⽬,是⼀套Java⽇志接⼝,之前叫Jakarta Commons Logging,后更名为Commons Logging,简称JCL
  • SLF4J:Simple Logging Facade for Java,缩写Slf4j,是⼀套简易Java⽇志⻔⾯,只提供相关接⼝,和其他⽇志⼯具之间需要桥接

1.2 ⽇志实现

  • JUL:JDK中的⽇志⼯具,也称为jdklog、jdk-logging,⾃Java1.4以来sun的官⽅提供。
  • Log4j:⾪属于Apache基⾦会的⼀套⽇志框架,现已不再维护
  • Log4j2:Log4j的升级版本,与Log4j变化很⼤,不兼容
  • Logback:⼀个具体的⽇志实现框架,和Slf4j是同⼀个作者,性能很好

二、日志配置

2.1 概述

1)⽇志级别:编码最经常⽤到的主流分级如下(由低到⾼):

  • trace:路径跟踪
  • debug:⼀般⽤于⽇常调式
  • info:打印重要信息
  • warn:给出警告
  • error:出现错误或问题

2)⽇志组件

  • appender:⽇志输出⽬的地,负责⽇志的输出 (输出到什么 地⽅)
  • logger:⽇志记录器,负责收集处理⽇志记录 (如何处理⽇志)
  • layout:⽇志格式化,负责对输出的⽇志格式化(以什么形式展现)

2.2 logback

http://logback.qos.ch/manual/index.html

1)配置⽂件:

  • Logback tries to find a file called logback-test.xml in the classpath.
  • If no such file is found, logback tries to find a file called logback.groovy in the classpath.
  • If no such file is found, it checks for the file logback.xml in the classpath..
  • If no such file is found, service-provider loading facility (introduced in JDK 1.6) is used to resolve the implementation of com.qos.logback.classic.spi.Configurator interface by looking up the file META-INF\services\ch.qos.logback.classic.spi.Configurator in the class path. Its contents should specify the fully qualified class name of the desired Configurator implementation.
  • If none of the above succeeds, logback configures itself automatically using the BasicConfigurator which will cause logging output to be directed to the console.

2)级别:

  • ⽇志打印级别 ALL > TRACE > FATAL > DEBUG > INFO > WARN > ERROR > OFF

3)处理器:

http://logback.qos.ch/manual/appenders.html

image-20241028223930529

4)格式化:

http://logback.qos.ch/manual/layouts.html

image-20241028223946308

\5) 代码实战:

  • Appender:Console,Rollingfile
  • Layout:Xml,Pattern,Html , ⾃定义
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="3 seconds" debug="false">
    <!-- lOGGER PATTERN 根据个⼈喜好选择匹配 -->
    <property name="logPattern" value="logback:[ %-5level] [%date{yyyy-MM-dd
HH:mm:ss.SSS}] %logger{96} [%line] [%thread]- %msg%n"></property>
    <!-- 动态⽇志级别 -->
    <jmxConfigurator/>
    <!-- 控制台的标准输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <charset>UTF-8</charset>
            <pattern>${logPattern}</pattern>
        </encoder>
    </appender>
    <!-- 滚动⽂件 -->
    <appender name="ROLLING_FILE"
              class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <file>./logback.log</file>
        <rollingPolicy
                class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>./logback.log.%d{yyyy-MM-dd}.zip</fileNamePattern>
            <!-- 最⼤保存时间 -->
            <maxHistory>2</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${logPattern}</pattern>
        </encoder>
    </appender>
    <!-- DB -->
    <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://172.17.0.203:3306/log?useSSL=false</url>
            <user>root</user>
            <password>root</password>
        </connectionSource>
    </appender>
​
    <!-- ASYNC_LOG -->
    <appender name="ASYNC_LOG" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丢失⽇志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的⽇志 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
        <queueSize>3</queueSize>
        <appender-ref ref="STDOUT"/>
    </appender>
​
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
            <!--<charset>UTF-8</charset>-->
            <!--<pattern>${logPattern}</pattern>-->
            <!--<layout class="com.itheima.logback.MySampleLayout" />-->
            <layout class="ch.qos.logback.classic.html.HTMLLayout">
                <pattern>%relative%thread%mdc%level%logger%msg</pattern>
            </layout>
            <!--<layout class="ch.qos.logback.classic.log4j.XMLLayout">-->
            <!--<locationInfo>false</locationInfo>-->
            <!--</layout>-->
        </encoder>
    </appender>
    
    <!-- ⽇志的记录级别 -->
    <!-- 在定义后引⽤APPENDER -->
    <root level="DEBUG">
        <!-- 控制台 -->
        <appender-ref ref="STDOUT"/>
        <!-- ROLLING_FILE -->
        <appender-ref ref="ROLLING_FILE"/>
        <!-- ASYNC_LOG -->
        <appender-ref ref="ASYNC_LOG"/>
    </root>
</configuration>

2.3 log4j2

1)配置⽂件:

  • Log4j will inspect the log4j.configurationFile system property and, if set, will attempt to
  • load the configuration using the ConfigurationFactory that matches the file extension.
  • If no system property is set the YAML ConfigurationFactory will look for log4j2-test.yaml
  • or log4j2-test.yml in the classpath.
  • If no such file is found the JSON ConfigurationFactory will look for log4j2-test.json or
  • log4j2-test.jsn in the classpath.
  • If no such file is found the XML ConfigurationFactory will look for log4j2-test.xml in the
  • classpath.
  • If a test file cannot be located the YAML ConfigurationFactory will look for log4j2.yaml or
  • log4j2.yml on the classpath.
  • If a YAML file cannot be located the JSON ConfigurationFactory will look for log4j2.json or
  • log4j2.jsn on the classpath.
  • If a JSON file cannot be located the XML ConfigurationFactory will try to locate log4j2.xml
  • on the classpath.
  • If no configuration file could be located the DefaultConfiguration will be used. This will
  • cause logging output to go to the console.

2)级别:

  • 从低到⾼为:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF

3)处理器:

http://logging.apache.org/log4j/2.x/manual/appenders.html

  • FileAppender   普通地输出到本地⽂件
  • KafkaAppender 输出到kafka队列
  • FlumeAppender  将⼏个不同源的⽇志汇集、集中到⼀处。
  • JMSQueueAppender,JMSTopicAppender  与JMS相关的⽇志输出
  • RewriteAppender   对⽇志事件进⾏掩码或注⼊信息
  • RollingFileAppender  对⽇志⽂件进⾏封存(详细)
  • RoutingAppender  在输出地之间进⾏筛选路由
  • SMTPAppender  将LogEvent发送到指定邮件列表
  • SocketAppender  将LogEvent以普通格式发送到远程主机
  • SyslogAppender  将LogEvent以RFC 5424格式发送到远程主机
  • AsynchAppender   将⼀个LogEvent异步地写⼊多个不同输出地
  • ConsoleAppender  将LogEvent输出到命令⾏
  • FailoverAppender  维护⼀个队列,系统将尝试向队列中的Appender依次输出LogEvent,直到有⼀个成功为⽌

4)格式化:

http://logging.apache.org/log4j/2.x/manual/layouts.html

image-20241028224352772

5)代码实战:

<?xml version="1.0" encoding="UTF-8"?>
<!--⽇志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE >
ALL -->
<!--status="WARN" :⽤于设置log4j2⾃身内部⽇志的信息输出级别,默认是OFF-->
<!--monitorInterval="30" :间隔秒数,⾃动检测配置⽂件的变更和重新配置本身-->
<configuration status="info" monitorInterval="30">
    <Properties>
        <!--⾃定义⼀些常量,之后使⽤${变量名}引⽤-->
        <Property name="pattern">log4j2:[%-5p]:%d{YYYY-MM-dd HH:mm:ss} [%t]
            %c{1}:%L - %msg%n</Property>
    </Properties>
    <!--appenders:定义输出内容,输出格式,输出⽅式,⽇志保存策略等,常⽤其下三种标签
   [console,File,RollingFile]-->
    <appenders>
        <!--console :控制台输出的配置-->
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="${pattern}"/>
        </Console>
        <!--File :同步输出⽇志到本地⽂件-->
        <!--append="false" :根据其下⽇志策略,每次清空⽂件重新输⼊⽇志,可⽤于测试-->
        <File name="File" fileName="./log4j2-file.log" append="false">
            <PatternLayout pattern="${pattern}"/>
        </File>
        <RollingFile name="RollingFile" fileName="./log4j2-rollingfile.log"
                     filePattern="./$${date:yyyy-MM}/log4j2-%d{yyyy-MM-dd}-
%i.log">
            <!--ThresholdFilter :⽇志输出过滤-->
            <!--level="info" :⽇志级别,onMatch="ACCEPT" :级别在info之上则接
           受,onMismatch="DENY" :级别在info之下则拒绝-->
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="${pattern}"/>
            <!-- Policies :⽇志滚动策略-->
            <Policies>
                <!-- TimeBasedTriggeringPolicy :时间滚动策略,
                默认0点产⽣新的⽂件,
                interval="6" : ⾃定义⽂件滚动时间间隔,每隔6⼩时产⽣新⽂件,
                modulate="true" : 产⽣⽂件是否以0点偏移时间,即6点,12点,18点,0点-->
                <TimeBasedTriggeringPolicy interval="6" modulate="true"/>
                <!-- SizeBasedTriggeringPolicy :⽂件⼤⼩滚动策略-->
                <SizeBasedTriggeringPolicy size="1 MB"/>
            </Policies>
            <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同⼀⽂件夹下7个⽂
           件,这⾥设置了20 -->
            <DefaultRolloverStrategy max="20"/>
        </RollingFile>
    </appenders>
    <!--然后定义logger,只有定义了logger并引⼊的appender,appender才会⽣效-->
    <loggers>
        <!--过滤掉spring和mybatis的⼀些⽆⽤的DEBUG信息-->
        <!--Logger节点⽤来单独指定⽇志的形式,name为包路径,⽐如要为org.springframework
       包下所有⽇志指定为INFO级别等。 -->
        <logger name="org.springframework" level="INFO"></logger>
        <logger name="org.mybatis" level="INFO"></logger>
        <!--AsyncLogger :异步⽇志,LOG4J有三种⽇志模式,全异步⽇志,混合模式,同步⽇志,性能
       从⾼到底,线程越多效率越⾼,也可以避免⽇志卡死线程情况发⽣-->
        <!--additivity="false" : additivity设置事件是否在root logger输出,为了避免重
       复输出,可以在Logger 标签下设置additivity为”false”-->
        <AsyncLogger name="AsyncLogger" level="trace" includeLocation="true"
                     additivity="true">
            <appender-ref ref="Console"/>
        </AsyncLogger>
        <logger name="Kafka" additivity="false" level="debug">
            <appender-ref ref="Kafka"/>
            <appender-ref ref="Console"/>
        </logger>
        <!-- Root节点⽤来指定项⽬的根⽇志,如果没有单独指定Logger,那么就会默认使⽤该Root
       ⽇志输出 -->
        <root level="info">
            <appender-ref ref="Console"/>
            <!--<appender-ref ref="File"/>-->
            <!--<appender-ref ref="RollingFile"/>-->
            <!--<appender-ref ref="Kafka"/>-->
        </root>
    </loggers>
</configuration>

2.4 slf4j

1)配置⽂件:

**  具体⽇志输出内容取决于⽣效的具体⽇志实现**

2)级别:

**  slf4j⽇志级别有五种:ERROR、WARN、INFO、DEBUG、TRACE,级别从⾼到底**

3)slf⽇志实现:

        <!--slf转其他⽇志-->
        <!--slf - jcl-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-jcl</artifactId>
            <version>1.7.30</version>
        </dependency>
        <!-- slf - log4j -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.30</version>
        </dependency>
        <!-- slf4j - log4j2 -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.13.0</version>
        </dependency>
        <!--slf - jul-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-jdk14</artifactId>
            <version>1.7.30</version>
        </dependency>
        <!--slf - simplelog-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.30</version>
        </dependency>
        <!--slf - logback-->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>

问题:多个桥接器的话,slf4j怎么处理呢?

**  使⽤在class path中较早出现的那个,如在maven中,会使⽤在pom.xml中定义较靠前的桥接器**

** ⼩知识:桥接器会传递依赖到对应的下游⽇志组件,⽐如slf4j-log4j12会附带log4j的jar包依赖(代码验证)**

4)其他⽇志转slf

        <!--其他⽇志转slf-->
        <!--jul - slf-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jul-to-slf4j</artifactId>
            <version>1.7.30</version>
        </dependency>
        <!--jcl - slf-->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.30</version>
        </dependency>
        <!--log4j - slf-->
        <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>log4j-over-slf4j</artifactId>
            <version>1.7.30</version>
        </dependency>
        <!--log4j2 - slf-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-to-slf4j</artifactId>
            <version>2.13.0</version>
        </dependency>

三、使用建议

3.1 ⻔⾯约束

**  使⽤⻔⾯,⽽不是具体实现**

image-20241028225138587

3.2 依赖约束

** ⽇志实现坐标应该设置为optional并使⽤runtime scope**

<dependency>
 <groupId>org.apache.logging.log4j</groupId>
 <artifactId>log4j-core</artifactId>
 <version>${log4j.version}</version>
 <scope>runtime</scope>
 <optional>true</optional>
</dependency>
<dependency>
 <groupId>org.apache.logging.log4j</groupId>
 <artifactId>log4j-slf4j-impl</artifactId>
 <version>${log4j.version}</version>
 <scope>runtime</scope>
 <optional>true</optional>
</dependency>

** 设为optional,依赖不会传递,这样如果你是个lib项⽬,然后别的项⽬使⽤了你这个lib,不会被引⼊不想要的Log Implementation 依赖;**

Scope设置为runtime,是为了防⽌开发⼈员在项⽬中直接使⽤Log Implementation中的类,强制约束开发⼈员使⽤Facade接⼝。

3.3 避免传递

** **尽量⽤exclusion排除依赖的第三⽅库中的⽇志坐标

实例:依赖jstorm会引⼊Logback和log4j-over-slf4j,如果你在⾃⼰的项⽬中使⽤Log4j或其他Log实现的话,就需要加上exclusion:

<dependency>
 <groupId>com.alibaba.jstorm</groupId>
 <artifactId>jstorm-core</artifactId>
 <version>2.1.1</version>
 <exclusions>
 <exclusion>
 <groupId>org.slf4j</groupId>
 <artifactId>log4j-over-slf4j</artifactId>
 </exclusion>
 <exclusion>
 <groupId>ch.qos.logback</groupId>
 <artifactId>logback-classic</artifactId>
 </exclusion>
 </exclusions>
</dependency>

3.4 注意写法

避免为不会输出的log买单

logger.debug("this is debug: " + message);
logger.debug("this is json msg: {}", toJson(message));

即使⽇志级别⾼于debug不会打印,依然会做字符串连接操作;

第⼆条虽然⽤了SLF4J/Log4j2 中的懒求值⽅式,但是toJson()这个函数却是总会被调⽤并且开销更⼤。

推荐的写法如下:

// SLF4J/LOG4J2
logger.debug("this is debug:{}", message);
// LOG4J2
logger.debug("this is json msg: {}", () -> toJson(message));
// SLF4J/LOG4J2
if (logger.isDebugEnabled()) {
 logger.debug("this is debug: " + message);
}

image-20241028225429217

3.5** 减少分**

** **输出的⽇志中尽量不要使⽤⾏号,函数名等信息

** 原因是,为了获取语句所在的函数名,或者⾏号,log库的实现都是获取当前的stacktrace,然后分**

**析取出这些信息,⽽**获取stacktrace的代价是很昂贵的。如果有很多的⽇志输出,就会占⽤⼤量的

CPU。在没有特殊需要的情况下,建议不要在⽇志中输出这些这些字段。

3.6 精简⾄上

** **log中尽量不要输出稀奇古怪的字符,这是个习惯和约束问题。有的同学习惯⽤这种语句:

logger.debug("=====================================:{}",message);

**  输出了⼤量⽆关字符,虽然⾃⼰⼀时痛快,但是如果所有⼈都这样做的话,那log输出就没法看了!**

正确的做法是⽇志只输出必要信息,如果要过滤,后期使⽤grep来筛选,只查⾃⼰关⼼的⽇志。

License:  CC BY 4.0