日志框架

  • 日志门面
    JCL、slf4j
  • 日志实现
    JUL、logback、log4j、log4j2
  • Spring Boot 采用 SLF4J作为日志门面(接口),logback作为日志的具体实现

SLF4J 的使用

  • 对于一般的Java项目而言,日志框架 会选择slf4j-api作为门面,配上具体的实现框架(log4j、logback等),中间使用桥接器完成桥接
  • SLF4J的优点:
    • 使用SLF4J框架,可以在部署时迁移到所需的日志记录框架。
    • SLF4J提供了对所有流行的日志框架的绑定,例如log4j,JUL,Simple logging和NOP。因此可以在部署时切换到任何这些流行的框架。
    • 无论使用哪种绑定,SLF4J都支持参数化日志记录消息。由于SLF4J将应用程序和日志记录框架分离, 因此可以轻松编写独立于日志记录框架的应用程序。而无需担心用于编写应用程序的日志记录框架。
    • SLF4J提供了一个简单的Java工具,称为迁移器。使用此工具,可以迁移现有项目,这些项目使用日志框架(如Jakarta Commons Logging(JCL)或log4j或Java.util.logging(JUL))到SLF4J。

绑定

  • 绑定流程:
    1. 添加slf4j-api的依赖
    2. 使用slf4j的API在项目中进行统一的日志记录
    3. 绑定具体的日志实现框架
      1. 绑定已经实现了slf4j的日志框架,直接添加对应依赖
      2. 绑定没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
    4. slf4j有且仅有一个日志实现框架的绑定(如果出现多个默认使用第一个依赖日志实现)

分布式日志技术 - 图1

桥接

  • 如果某些组件依赖了SLF4J以外的日志API,则可以通过桥接将这些组件日志切换到SLF4J
  • SLF4J附带的桥接模块将对log4j,JCL和 java.util.logging API的调用重定向
  • 实现方式
    1. 先去除之前老的日志框架的依赖
    2. 添加SLF4J提供的桥接组件
    3. 为项目添加SLF4J的具体实现

SLF4J 原理

  1. SLF4J通过LoggerFactory加载日志具体的实现对象。
  2. LoggerFactory在初始化的过程中,会通过performInitialization()方法绑定具体的日志实现。
  3. 在绑定具体实现的时候,通过类加载器,加载org/slf4j/impl/StaticLoggerBinder.class
  4. 所以,只要是一个日志实现框架,在org.slf4j.impl包中提供一个自己的StaticLoggerBinder类,在其中提供具体日志实现的LoggerFactory就可以被SLF4J所加载

Logback 的使用

  • Logback主要分为三个模块:
    logback-core:其它两个模块的基础模块
    logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API
    logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能

Logback 配置

  • logback会依次读取以下类型配置文件:
    logback.groovy
    logback-test.xml
    logback.xml 如果均不存在会采用默认配置
  • logback组件之间的关系
    Logger:日志的记录器,把它关联到应用的对应的context上后,主要用于存放日志对象,也可以定义日志类型、级别。
    Appender:用于指定日志输出的目的地,目的地可以是控制台、文件、数据库等等。
    Layout:负责把事件转换成字符串,格式化的日志信息的输出。在logback中Layout对象被封装在encoder中。
  • logback.xml配置文件
    • 配置基础appender
    • 配置FileAppender输出日志到文件中
    • RollingFileAppender配置,根据日期或文件大小拆分日志
    • Filter日志过滤;异步日志
    • 自定义logger,指定特定包下的内容实用自定义的logger进行记录
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration>
  3. <!--
  4. 配置集中管理属性
  5. 我们可以直接改属性的 value 值
  6. 格式:${name}
  7. -->
  8. <!--
  9. 日志输出格式:
  10. %-5level
  11. %d{yyyy-MM-dd HH:mm:ss.SSS}日期
  12. %c类的完整名称
  13. %M为method
  14. %L为行号
  15. %thread线程名称
  16. %m或者%msg为信息
  17. %n换行
  18. -->
  19. <property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} %c %M %L [%thread] %m%n"></property>
  20. <!--定义日志文件保存路径属性-->
  21. <property name="log_dir" value="/logs"></property>
  22. <!--控制台日志输出的 appender-->
  23. <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
  24. <!--控制输出流对象 默认 System.out 改为 System.err-->
  25. <target>System.err</target>
  26. <!--日志消息格式配置-->
  27. <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
  28. <pattern>${pattern}</pattern>
  29. </encoder>
  30. </appender>
  31. <!--日志文件输出的 appender-->
  32. <appender name="file" class="ch.qos.logback.core.FileAppender">
  33. <!--日志文件保存路径-->
  34. <file>${log_dir}/logback.log</file>
  35. <!--日志消息格式配置-->
  36. <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
  37. <pattern>${pattern}</pattern>
  38. </encoder>
  39. </appender>
  40. <!--html 格式日志文件输出 appender-->
  41. <appender name="htmlFile" class="ch.qos.logback.core.FileAppender">
  42. <!--日志文件保存路径-->
  43. <file>${log_dir}/logback.html</file>
  44. <!--html 消息格式配置-->
  45. <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
  46. <layout class="ch.qos.logback.classic.html.HTMLLayout">
  47. <pattern>%-5level%d{yyyy-MM-dd HH:mm:ss.SSS}%c%M%L%thread%m</pattern>
  48. </layout>
  49. </encoder>
  50. </appender>
  51. <!--日志拆分和归档压缩的 appender 对象-->
  52. <appender name="rollFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
  53. <!--日志文件保存路径-->
  54. <file>${log_dir}/roll_logback.log</file>
  55. <!--日志消息格式配置-->
  56. <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
  57. <pattern>${pattern}</pattern>
  58. </encoder>
  59. <!--指定拆分规则-->
  60. <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
  61. <!--按照时间和压缩格式声明拆分的文件名-->
  62. <fileNamePattern>${log_dir}/rolling.%d{yyyy-MM-dd}.log%i.gz</fileNamePattern>
  63. <!--按照文件大小拆分-->
  64. <maxFileSize>1MB</maxFileSize>
  65. </rollingPolicy>
  66. <!--日志级别过滤器-->
  67. <filter class="ch.qos.logback.classic.filter.LevelFilter">
  68. <!--日志过滤规则-->
  69. <level>ERROR</level>
  70. <onMatch>ACCEPT</onMatch>
  71. <onMismatch>DENY</onMismatch>
  72. </filter>
  73. </appender>
  74. <!--异步日志-->
  75. <appender name="async" class="ch.qos.logback.classic.AsyncAppender">
  76. <!--指定某个具体的 appender-->
  77. <appender-ref ref="rollFile"/>
  78. </appender>
  79. <!--root logger 配置-->
  80. <root level="ALL">
  81. <appender-ref ref="console"/>
  82. <appender-ref ref="async"/>
  83. </root>
  84. <!--自定义 looger 对象
  85. additivity="false" 自定义 logger 对象是否继承 rootLogger
  86. -->
  87. <logger name="com.itheima" level="info" additivity="false">
  88. <appender-ref ref="console"/>
  89. </logger>
  90. </configuration>

Logback access

  • logback-access模块与Servlet容器(如Tomcat和Jetty)集成,以提供HTTP访问日志功能。我们可以使用logback-access模块来替换tomcat的访问日志。
  • 步骤:

    1. 将logback-access.jar与logback-core.jar复制到$TOMCAT_HOME/lib/目录下
    2. 修改$TOMCAT_HOME/conf/server.xml中的Host元素中添加:

      <Valve className="ch.qos.logback.access.tomcat.LogbackValve" />
      
    3. logback默认会在$TOMCAT_HOME/conf下查找文件 logback-access.xml
      ```xml <?xml version=”1.0” encoding=”UTF-8”?>

    ${LOG_DIR}/access.log access.%d{yyyy-MM-dd}.log.zip combined

    ```

log4j2

  • 目前市面上最主流的日志门面就是SLF4J,虽然Log4j2也是日志门面,因为它的日志实现功能非常强大,性能优越。所以大家一般是将Log4j2看作是日志的实现,Slf4j + Log4j2应该是未来的大势所趋。
<!--使用slf4j作为日志的门面,使用log4j2来记录日志 --> 
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>

<!--为slf4j绑定日志实现 log4j2的适配器 --> 
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.10.0</version>
</dependency>

log4j2 配置

<?xml version="1.0" encoding="UTF-8"?>
<!--
    status="warn" 日志框架本身的输出日志级别
    monitorInterval="5" 自动加载配置文件的间隔时间,不低于 5 秒
-->
<Configuration status="debug" monitorInterval="5">

    <!--
        集中配置属性进行管理
        使用时通过:${name}
    -->
    <properties>
        <property name="LOG_HOME">/logs</property>
    </properties>

    <!--日志处理-->
    <Appenders>
        <!--控制台输出 appender-->
        <Console name="Console" target="SYSTEM_ERR">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n" />
        </Console>

        <!--日志文件输出 appender-->
        <File name="file" fileName="${LOG_HOME}/myfile.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
        </File>

        <!--<Async name="Async">-->
            <!--<AppenderRef ref="file"/>-->
        <!--</Async>-->

        <!--使用随机读写流的日志文件输出 appender,性能提高-->
        <RandomAccessFile name="accessFile" fileName="${LOG_HOME}/myAcclog.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
        </RandomAccessFile>

        <!--按照一定规则拆分的日志文件的 appender-->
        <RollingFile name="rollingFile" fileName="${LOG_HOME}/myrollog.log"
                     filePattern="/logs/$${date:yyyy-MM-dd}/myrollog-%d{yyyy-MM-dd-HH-mm}-%i.log">
            <!--日志级别过滤器-->
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
            <!--日志消息格式-->
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %msg%n" />
            <Policies>
                <!--在系统启动时,出发拆分规则,生产一个新的日志文件-->
                <OnStartupTriggeringPolicy />
                <!--按照文件大小拆分,10MB -->
                <SizeBasedTriggeringPolicy size="10 MB" />
                <!--按照时间节点拆分,规则根据filePattern定义的-->
                <TimeBasedTriggeringPolicy />
            </Policies>
            <!--在同一个目录下,文件的个数限定为 30 个,超过进行覆盖-->
            <DefaultRolloverStrategy max="30" />
        </RollingFile>
    </Appenders>

    <!--logger 定义-->
    <Loggers>
        <!--自定义异步 logger 对象
            includeLocation="false" 关闭日志记录的行号信息
            additivity="false" 不在继承 rootlogger 对象
        -->
        <AsyncLogger name="com.itheima" level="trace" includeLocation="false" additivity="false">
            <AppenderRef ref="Console"/>
        </AsyncLogger>

        <!--使用 rootLogger 配置 日志级别 level="trace"-->
        <Root level="trace">
            <!--指定日志使用的处理器-->
            <AppenderRef ref="Console" />
            <!--使用异步 appender-->
            <AppenderRef ref="Async" />
        </Root>
    </Loggers>
</Configuration>

异步日志

  • Log4j2提供了两种实现日志的方式,一个是通过AsyncAppender,一个是通过AsyncLogger,分别对应前面我们说的Appender组件和Logger组件。
  • 配置异步日志需要添加依赖
    <!--异步日志依赖--> 
    <dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.3.4</version>
    </dependency>
    
  • AsyncAppender

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="warn">
      <properties>
          <property name="LOG_HOME">D:/logs</property>
      </properties>
      <Appenders>
                  <!--文件日志-->
            <File name="file" fileName="${LOG_HOME}/myfile.log">
              <PatternLayout>
                  <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
              </PatternLayout>
          </File>
    
            <!--异步日志-->
            <Async name="Async">
              <AppenderRef ref="file"/>
          </Async>
      </Appenders>
      <Loggers>
          <Root level="error">
            <AppenderRef ref="Async"/>
          </Root>
      </Loggers>
    </Configuration>
    
  • AsyncLogger

    • AsyncLogger才是log4j2 的重头戏,也是官方推荐的异步方式。它可以使得调用Logger.log返回的更快。你可以有两种选择:全局异步和混合异步。
  • 全局异步:全局异步就是,所有的日志都异步的记录,在配置文件上不用做任何改动,只需要添加一个log4j2.component.properties 配置;

    log4jContextSelector=org.apache.loggin.log4j.core.async.AsyncLoggerContextSelector
    
  • 混合异步:在应用中同时使用同步日志和异步日志

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="WARN">
    <properties>
     <property name="LOG_HOME">D:/logs</property>
    </properties>
    <Appenders>
     <File name="file" fileName="${LOG_HOME}/myfile.log">
       <PatternLayout>
         <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
       </PatternLayout>
     </File>
     <Async name="Async">
       <AppenderRef ref="file"/>
     </Async>
    </Appenders>
    
    <Loggers>
     <!--异步logger-->
     <AsyncLogger name="com.itheima" level="trace" includeLocation="false" additivity="false">
       <AppenderRef ref="file"/>
     </AsyncLogger>
    
     <!--同步logger-->
     <Root level="info" includeLocation="true">
       <AppenderRef ref="file"/>
     </Root>
    </Loggers>
    </Configuration>
    
  • 使用异步日志的注意事项

    1. 如果使用异步日志,AsyncAppender、AsyncLogger和全局日志,不要同时出现。性能会和 AsyncAppender一致,降至最低。
    2. 设置includeLocation=false ,打印位置信息会急剧降低异步日志的性能,比同步日志还要 慢。

log4j2 的性能

分布式日志技术 - 图2

分布式日志技术 - 图3

Spring Boot 中的日志使用

分布式日志技术 - 图4

  • application.properties文件中配置日志信息
# 日志级别
logging.level.com.itheima=trace

# 在控制台输出的日志的格式 同logback
logging.pattern.console=%d{yyyy-MM-dd} [%thread] [%-5level] %logger{50} -%msg%n

# 指定文件中日志输出的格式
logging.file=D:/logs/springboot.log
logging.pattern.file=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n
  • 存在以下配置文件时,会自动读取配置文件中的内容

分布式日志技术 - 图5

  • 配置文件 hogback-spring.xml
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
  <!--dev 环境-->
  <springProfile name="dev">
    <pattern>${pattern}</pattern>
  </springProfile>

  <!--pro 环境-->
  <springProfile name="pro">
    <pattern>%d{yyyyMMdd:HH:mm:ss.SSS} [%thread] %-5level%msg%n
    </pattern>
  </springProfile>
</encoder>
  • 将日志切换为log4j2
    <dependency>
    <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
            <!--排除logback--> 
      <exclusion>
            <artifactId>spring-boot-starter-logging</artifactId>
            <groupId>org.springframework.boot</groupId>
      </exclusion>
    </exclusions>
    </dependency>

    <!-- 添加log4j2 --> 
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
  </dependency>

项目:pinda-log

项目实现

分布式日志技术 - 图6

pd-tools-log的开发步骤为:

1、定义日志操作事件类SysLogEvent
2、定义@SysLog注解,用于在Controller的方法上标注当前方法需要进行操作日志的保存处理
3、定义切面类SysLogAspect
4、在切面类SysLogAspect中定义切点,拦截Controller中添加@SysLog注解的方法
5、在切面类SysLogAspect中定义前置通知,在前置通知方法recordLog中收集操作日志相关信息封装为OptLogDTO对象并保存到ThreadLocal中
6、在切面类SysLogAspect中定义后置通知,在后置通知方法doAfterReturning中通过ThreadLocal 获取OptLogDTO并继续设置其他的操作信息到OptLogDTO
7、在切面类SysLogAspect的后置通知方法doAfterReturning中发布事件SysLogEvent
8、定义监听器SysLogListener,监听日志发布事件SysLogEvent
9、定义配置类LogAutoConfiguration,用于自动配置切面SysLogAspect对象
10、定义starter所需的META-INF/spring.factories文件,并配置自动配置类LogAutoConfiguration

三个重要 ID 的解析

private String tid; // terminal ID 请求终端ID --> 从请求头中的 user-agent获取

private String uid; // user ID 用户ID --> 读取并解析请求头中的jwt令牌信息

private String rid; // request ID 请求ID,一次请求的唯一标识 --> 增强方法中生成

项目总结

  • 使用 Spring Boot 中已经整合的 SLF4J + Logback 框架实现日志的收集,采用 AOP 的设计思想实现声明式日志服务调用
  • 在切面类中通过 Spring 提供的事件监听机制实现异步化日志存储