SpringBoot入门到精通(十三)日志:别小看它,否则吃亏的是自己!学会你也可以设计架构

别小看他,当你面对的时候,就会知道,多么痛的领悟!

如何在 Spring Boot 中使用 Logback 记录详细的日志?

整合LogBack,Log4J...等,是不是很多方法!但需要注意,我讲的可能和你是一样的,但也是不一样的。

复制代码
常见日志级别:高 --- 低排列
TRACE:
描述:最详细的日志级别,通常用于开发和调试阶段,记录非常详细的执行信息。
示例:log.trace("Entering method: {这里的数据,后面的参数会自动填充}", methodName);
DEBUG:
描述:用于调试信息,记录程序的详细执行过程,但比 TRACE 级别略少。
示例:log.debug("Variable value: {}", variableValue);
INFO:
描述:记录普通的信息日志,通常用于记录应用程序的正常运行状态。
示例:log.info("User logged in: {}", userId);
WARN:
描述:警告信息,表示潜在的问题,但应用程序仍可以继续运行。
示例:log.warn("File not found: {}", fileName);
ERROR:
描述:错误信息,表示应用程序中发生了错误,可能会影响功能的正常运行。
示例:log.error("Database connection failed: {}", e.getMessage());  这里每个字母代码,都认真看,有没有疑问呢?特别,特别注意哦,后面告诉你
FATAL:
描述:严重错误,通常会导致应用程序崩溃或无法继续运行。
示例:log.fatal("Critical system failure: {}", e.getMessage());

实战检验真理!论日志的重要性。

在开发企业级应用时,日志记录是一项非常重要的功能。良好的日志记录可以帮助我们快速定位和解决问题。比如异常排查,接口交互!大多数认为,直接log.info.debug一下就可以了...

细节很重要:

通常,生产环境,日志级别要求是很严格的(设置INFO的举手),企业级开发,基本要求不允许太多日志,通常不推荐使用DEBUG级别的日志,因为这会产生大量的日志输出,不仅占用存储空间,还可能影响系统性能。。

SpringBoot与LogBack日志(合并)

1. 引入依赖

在 Spring Boot 项目中,Logback 是默认的日志框架。Spring Boot 会自动配置 Logback,因此你通常不需要手动添加 Logback 的依赖。但是,为了确保所有必要的依赖都已包含,你可以在 pom.xml 文件中明确指定这些依赖。

复制代码
<dependencies>
    <!-- Spring Boot Starter Web (或其他你需要的 Starter) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot Starter Logging (包含 SLF4J 和 Logback) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>

    <!-- 如果你已经包含了 spring-boot-starter-web,这个依赖是多余的,因为 spring-boot-starter-web 已经包含了 spring-boot-starter-logging -->
</dependencies>

2. 配置 Logback

在 src/main/resources 目录下创建或编辑 logback-spring.xml 文件(按照自动装配机制,文件名和位置,默认)

文件名:logback-spring.xml (基于自动装配)

位置:src/main/resources/ (基于自动装配)

复制代码
<configuration>
    <!-- 定义日志文件的存储路径 -->
    <!-- <property name="LOG_PATH" value="logs" /> 相对路径,当前项目所在目录下的logs,如项目在/home/tomcat/project那么日志在/home/tomcat/project/logs -->
    <!-- 自定义日志路径,可指定日志保存位置,logging.file.path指向Boot配置文件yml、properties文件配置 -->
    <property name="LOG_PATH" value="${logging.file.path}" />
    <!-- 自定义日志名称,可忽略 -->
    <property name="LOG_FILE_NAME" value="application" />

    <!-- 控制台日志输出:常配置使用在开发环境本地 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level \[%X{uuid}\] %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 文件日志输出:线上环境测试、UAT、PRE -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 【定义日志文件要求】 -->
        <file>${LOG_PATH}/${LOG_FILE_NAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 按天滚动
            ${LOG_PATH} 路径
            ${LOG_FILE_NAME} 名称
            %d{yyyy-MM-dd} 日期
            %i    序号,0开始
            -->
            <fileNamePattern>${LOG_PATH}/${LOG_FILE_NAME}-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- 单个文件最大500MB -->
            <maxFileSize>500MB</maxFileSize>
            <!-- 保留最近30天的日志文件 -->
            <maxHistory>30</maxHistory>
            <!-- 总日志文件大小不超过1GB -->
            <!-- <totalSizeCap>1GB</totalSizeCap> -->
        </rollingPolicy>
        <!-- 【定义日志文件中记录日志的内容格式】
            %d{yyyy-MM-dd HH:mm:ss.SSS}:
            %d:表示日期和时间。
            {yyyy-MM-dd HH:mm:ss.SSS}:日期和时间的格式化模式。
            yyyy:四位年份。
            MM:两位月份。
            dd:两位日期。
            HH:两位小时(24小时制)。
            mm:两位分钟。
            ss:两位秒。
            SSS:三位毫秒。
            [%thread]:
            %thread:表示当前线程的名称。
            []:用于包裹线程名称,使其更易读。
            %-5level:
            %level:表示日志级别(如 TRACE, DEBUG, INFO, WARN, ERROR)。
            -5:表示日志级别的最小宽度为5个字符,如果日志级别不足5个字符,则左对齐并用空格填充。
            %logger{36}:
            %logger:表示日志记录器的名称。
            {36}:表示日志记录器名称的最大长度为36个字符,如果名称超过36个字符,则截断。
            - %msg:
            %msg:表示日志消息的内容。
            -:用于分隔日志记录器名称和日志消息,使其更易读。
            %n:
            %n:表示换行符,用于在每条日志消息后换行。
        -->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level \[%X{uuid}\] %logger{36} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 异步日志输出
        作用
        提高性能:
        异步日志记录:AsyncAppender 将日志消息放入队列中,然后由单独的线程处理这些消息。这样,主线程不会因为日志记录操作而阻塞,从而提高应用程序的性能。
        减少 I/O 影响:日志记录通常涉及磁盘 I/O 操作,这些操作可能会比较慢。通过异步记录,可以将这些操作移到后台线程,减少对主线程的影响。
        资源管理:
        队列大小:通过设置 queueSize,可以控制内存的使用。较大的队列可以容纳更多的日志消息,但会占用更多的内存。
        丢弃策略:通过设置 discardingThreshold,可以在队列满时选择是否丢弃日志消息,以防止内存溢出。
    -->
    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="FILE" /> <!-- 这里有指向哦 -->
        <!-- 这定义异步日志记录器的队列大小。队列用于暂存日志消息,直到它们被处理 -->
        <queueSize>512</queueSize>
        <!-- 定义当队列满时是否丢弃日志消息。默认值是 0,表示不丢弃任何日志消息。大于 0 当队列中的消息数量超过这个阈值时,新产生的日志消息会被丢弃 -->
        <discardingThreshold>0</discardingThreshold> 
    </appender>
    
    <!-- 开发日志级别设置 springProfile的启用 与boot配置文件yml、properties中的 spring.profiles.active 指向相关联 -->
    <springProfile name="dev">
        <!-- 设置日志的根级别为 debug 并指定日志输出到控制台,同时异步输出到文件 -->
        <root level="debug">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="ASYNC" />
        </root>
        <!-- 指定一个其他日志记录器为 info 异步输出到文件
            additivity="false":当 com.yourcompany 包中的日志事件被记录时,这些日志事件只会被输出到 FILE 日志记录器,而不会被传递到根日志记录器。因此,这些日志事件不会出现在控制台中。
            additivity="true"(默认):如果 additivity 为 true,com.yourcompany 包中的日志事件不仅会被输出到 FILE 日志记录器,还会被传递到根日志记录器,从而也会出现在控制台中。
            使用场景
            避免重复日志:如果你希望某个特定的日志记录器的日志事件只输出到特定的记录器,而不希望这些日志事件在其他地方重复出现,可以设置 additivity="false"。
            精细控制日志输出:通过设置 additivity="false",可以更精细地控制日志的输出,确保日志信息的清晰和有序。
            注意:有root,其实就够了,其他的顶部定义的看你的要求,这是精细化的一种方式
        -->
        <logger name="包名" level="info" additivity="false">
            <appender-ref ref="ASYNC" />
        </logger>
    </springProfile>
    
    <!-- test、UAT日志级别设置 springProfile的启用 与boot配置文件yml、properties中的 spring.profiles.active 指向相关联 -->
    <springProfile name="test、uat">
        <!-- 设置日志的根级别为 debug 并指定日志异步输出到文件 -->
        <root level="debug">
            <appender-ref ref="ASYNC" />
        </root>
        <logger name="包名" level="info" additivity="false">
            <appender-ref ref="ASYNC" />
        </logger>
    </springProfile>

    <springProfile name="pre">
        <root level="info">
            <appender-ref ref="ASYNC" />
        </root>
        <!-- 第三方库的日志级别配置 -->
        <logger name="org.driud" level="debug" additivity="false">
            <appender-ref ref="ASYNC" />
        </logger>
    </springProfile>
</configuration>

3. 配置变化量

你可以在 Spring Boot 的配置文件中,使用环境变量来设置日志变量,Spring Boot 支持在 application.properties 或 application.yml 文件中引用环境变量:

复制代码
# application.properties
# 激活配置环境
spring.profiles.active=dev

# 配置日志文件生成后,放在哪里:文件位置,配置后都可以被引用
logging.file.path=/opt/tomcat/myapp/logs
# 配置根日志级别
# logging.level.root=info
# 配置三方日志级别,细化
# logging.level.包名=debug
# 如果日志配置文件位置、名称,自定义了怎么办?
# logging.config=classpath:custom-logback.xml

你还可以结合多环境配置文件,来设置不同的日志路径。例如,为开发环境、测试环境和预发布环境分别设置不同的日志路径。

application-dev.properties

application-uat.properties

application-pre.properties

application.properties 文件激活特定环境的配置文件即可

SpringBoot与LogBack日志(生产细节控)

问题一

无论是否集群服务,十个人并发也是并发,发起同一个功能或不通功能属于并发交互,日志记录器打印日志时也是有交叉打印。那么如何在百千万行的交叉日志记录中,找到属于某一个指定功能操作的记录呢!

A、B、C都在请求message服务,每个业务的处理时间肯定不一致,并发交叉日志打印时,如何排查耗时?

解决方案:每次交互,从日志的开始就进行标记,直到这个交互结束,只要保证,每个线程,每个交互,标记是唯一的,哪就可以了,即使日志交叉打印,因为保证了唯一标记不同,所以也会很好区分。

Logback 支持在日志输出中插入 MDC(Mapped Diagnostic Context)变量的占位符。在【定义日志文件中记录日志的内容格式】时,有一处标红,那里就是一个MDC取值的示例,MDC 是一个线程上下文相关的键值对存储,可以用来在日志中添加额外的信息,如请求的唯一标识符(UUID)

MDC(Mapped Diagnostic Context)

MDC 概述

MDC 是一个线程上下文相关的键值对存储,每个线程都有自己的 MDC 实例。MDC 是 Logback 和 Log4j 中提供的一个功能,主要用途是在日志记录中添加与当前线程相关的上下文信息,些信息可以是请求的唯一标识符、用户 ID、会话 ID 等,以便在日志中进行更细粒度的跟踪和调试。

主要功能

存储上下文信息:MDC 允许你在日志记录中存储和访问与当前线程相关的上下文信息。

日志格式化:在日志输出模式中使用 MDC 变量,可以在日志消息中插入这些上下文信息。

线程安全:MDC 是线程安全的,每个线程都有独立的 MDC 实例,不会互相干扰。

MDC的使用

1.在每个日志打印之前使用(不推荐,不利用统一管理,但凡有人忘,新人,那日志就没有了,重复,还太麻烦)

复制代码
public class MyController {
    public void handleRequest() {
        // 生成一个唯一的请求标识符
        String uuid = UUID.randomUUID().toString();
        
        // 将 UUID 设置到 MDC 中   在你使用log。。。打印日志之前放入
        MDC.put("uuid", uuid);
        
        log.info("请求,start:{}",json)
        // 处理请求
        // ...其他业务,等等,其他日志等等...
        log.info("应答,end:{}",json)
        
        // 请求处理完成后,清除 MDC 中的 UUID
        MDC.remove("uuid");
    }
}

控制台输出

复制代码
2024-11-01 14:30:00.123 [main] [f0e2c1a0-1b9d-4b7e-8c0a-123456789abc] INFO  MyController - 请求,start:{"name": "John", "age": 30}
------ 若业务中有其他日志,那么他们的uuid都是一样的,非常好定位  ------
2024-11-01 14:30:00.124 [main] [f0e2c1a0-1b9d-4b7e-8c0a-123456789abc] INFO  MyController - 应答,end:{"name": "John", "age": 30}

2.利用过滤器Filter

可以自定义实现Filter接口哈,这就比较老了,与时俱进吧,Boot中有了,就用吧。

**  OncePerRequestFilter 概述**

OncePerRequestFilter 的主要目的是确保即使在一个请求被多个过滤器链中的多个实例处理时,也只会执行一次过滤逻辑。这对于避免重复处理和潜在的性能问题非常重要。

OncePerRequestFilter 是 Spring Framework 提供的一个过滤器类,用于确保每个请求只被处理一次。它继承自 org.springframework.web.filter.OncePerRequestFilter 类,并且通常用于需要在每个 HTTP 请求上执行某些操作的场景,例如日志记录、性能监控、安全检查等。

主要特点

单次执行:确保每个请求只被处理一次,即使在过滤器链中有多个实例。

线程安全:适用于多线程环境,确保线程安全。

灵活性:可以通过重写 doFilterInternal 方法来自定义过滤逻辑。

使用场景

日志记录:在每个请求开始和结束时记录日志。

性能监控:记录每个请求的处理时间。

安全检查:在请求到达控制器之前进行身份验证和授权。

跨域处理:设置响应头以支持跨域请求。、

创建自定义过滤器

创建一个继承自 OncePerRequestFilter 的自定义过滤器,用于生成唯一请求标识符并将其设置到 MDC 中:

复制代码
@Component // 标记为Spring组件,SpringBoot自动装配机制,扫描到组件后,会自动将其加载。当然也可以选择手动将过滤器加入到服务(可自行查阅)
@Slf4j
public class RequestLoggingFilter extends OncePerRequestFilter {
    /**
     * 重写doFilterInternal过滤器方法,每次交互,过滤器都会拦截,并执行次方法
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 生成一个唯一的请求标识符 每个请求生成一个
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        // 将 UUID 设置到 MDC 中 每个线程一个
        MDC.put("uuid", uuid);
        // 记录请求开始日志
        log.info("请求开始,URL: {}", request.getRequestURL());

        // 继续执行过滤器链
        try {
            filterChain.doFilter(request, response);
        } catch (Exception e) {
            // 记录错误日志
            log.error("请求处理失败,URL: {}, 错误信息: {}", request.getRequestURL(), ExceptionUtils.getStackTrace(e));
            // 重新抛出异常,确保后续的统一异常处理 能够接收到异常,并统一处理异常信息
            throw e;
        } finally {
            // 记录请求结束日志
            log.info("请求结束,URL: {}", request.getRequestURL());
            // 请求处理完成后,清除 MDC 中的 UUID
            // MDC.remove("uuid"); // 清楚指定
            // 请求处理完成后,清除 MDC 中的 UUID
            MDC.clear(); // 清除所有(MDC每个请求、线程都有自己的MDC,建议使用清除所有,避免交互结束后的MDC上下文数据残留,以及潜在的内存泄漏或信息混淆)
        }
    }
}

备注:关于统一异常处理,请观看我的"统一异常处理" ,这些都是架构必须要会的。

优化日志第三步,AOP

日志搭建基本完善,但是日志输出呢?每个接口都需要手动log.....

可以利用Spring AOP 对控制器进行环绕通知。在每个控制器的请求处理业务之前,应答返回之前,加入日志记录:接口名,控制器名称,交互的请求报文,应答报文等。

复制代码
@Component
@Aspect
@Slf4j
public class ControllerAop {
    /**
     * 切入点表达式:筛选所有的控制层,以及控制层接口方法
     * 第一个"*" :任意返回值
     * 第二个".*" :如果少一个点,就是com.xxx下的任意包,多一个点,就是com.xxx下的任意包以及子包的controller包
     * 第三个".*" :controller包下任意类
     * 第四个"*" :任意方法
     * 第五个"(..)":任意参数
     */
    @Pointcut("execution(public * com.xxx..*.controller..*.*(..))")
    public void privilege() {
    }

    /**
     * 定义AOP通知方式:Around环绕,满足切片的控制器方法,将在这里先走一次关卡。
     * 
     * @param proceedingJoinPoint 是 AOP(面向切面编程)中的一个重要接口,主要用于实现环绕通知(@Around)
     * @return 被拦截方法执行结果
     * @throws Throwable
     */
    @Around("privilege()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        // ThreadLocalContext 自己定义的类 是一个用于在当前线程中存储和传递上下文信息的工具类。它利用了 ThreadLocal 机制,确保每个线程都有独立的变量副本,从而避免了多线程环境下的数据竞争问题。
        // ThreadLocalContext.set("aaa", "");
        // PageHelper.clearPage():清除分页信息。PageHelper 是一个常用的分页插件,这个方法用于在每次请求开始时清除之前的分页设置,确保新的请求不会受到之前分页设置的影响。

        long start = System.currentTimeMillis();
        // getSignature 获取被拦截方法的签名信息,包括方法名、类名等
        String className = proceedingJoinPoint.getSignature().getDeclaringTypeName();
        String methodName = proceedingJoinPoint.getSignature().getName();
        Object result = null;
        // 获取被拦截方法的参数数组
        Object[] args = proceedingJoinPoint.getArgs();
        for (Object object : args) {
            if(object instanceof RequestVo) {
                log.info("{}{}{}" , "【"+className + "." + methodName + "】" , "【前后端交互--请求】" ,  null != object ? JSONObject.toJsonString(object): object);
            }
        }
        // 继续执行被拦截的方法,并获取返回报文。
        result = proceedingJoinPoint.proceed();
        long end = System.currentTimeMillis();
        log.info("{}{}{}" , "【"+className + "." + methodName + "】" , "【前后端交互--响应--耗时:"+(end - start)+"】" ,  null != result ? JSONObject.toJsonString(result) : result);
        return result;
    }
}

ThreadLocal 是 Java 提供的一个线程局部变量存储机制,每个线程都有自己独立的 ThreadLocal 变量副本,互不干扰。它的主要作用是为每个线程提供独立的变量副本,从而实现线程间的隔离和数据的安全性。下面详细解释 ThreadLocal 的作用、意义和常见使用场景。

作用

线程隔离:

每个线程都有自己独立的 ThreadLocal 变量副本,确保不同线程之间不会相互影响。

适用于需要在线程内部保存状态且不希望其他线程访问这些状态的场景。

简化线程间数据传递:

避免在方法调用之间传递参数,减少方法签名的复杂性。

提高代码的可读性和可维护性。

使用场景

日志追踪:

在分布式系统中,为了追踪请求的整个调用链路,可以在 ThreadLocal 中存储一个唯一的请求标识符(如 traceId),并在日志中输出这个标识符,以便于问题定位和调试。

事务管理:

在事务处理中,可以使用 ThreadLocal 存储事务上下文信息,确保同一个线程中的多个方法调用共享同一个事务上下文。

例如,Spring 的事务管理器 TransactionSynchronizationManager 使用 ThreadLocal 来管理事务上下文。

用户会话信息:

在 Web 应用中,可以使用 ThreadLocal 存储用户的会话信息(如用户 ID、角色等),以便在多个方法调用中使用这些信息,而不需要每次都传递参数。

自定义类ThreadLocalContext ,这个不一定用到,看自己的任务需求,其实这个应该单独介绍的,后来想想还是算了,懒

复制代码
/**
 * 一个用于在当前线程中存储和传递上下文信息的工具类。它利用了 ThreadLocal 机制,确保每个线程都有独立的变量副本,从而避免了多线程环境下的数据竞争问题。.
 */
public class ThreadLocalContext {
    
    private static final ThreadLocal<Map<String, Object>> context = new ThreadLocal<Map<String, Object>>();
    
    /**
     * 清空线程上下文中缓存的变量.
     */
     public static void clear() {
       context.set(null);
     }

     /**
      * 在线程上下文中缓存变量.
      */
     public static void set(String key, Object value) {
       Map<String, Object> map = context.get();
       if (map == null) {
         map = new HashMap<>();
         context.set(map);
       }
       map.put(key, value);
     }

     /**
      * 从线程上下文中取出变量.
      */
     public static Object get(String key) {
       Map<String, Object> map = context.get();
       Object value = null;
       if (map != null) {
         value = map.get(key);
       }
       return value;
     }

     /**
      * 将变量移除.
      */
     public static void remove(String key) {
       Map<String, Object> map = context.get();
       if (map != null) {
         map.remove(key);
       }
     }
}

日志输出样式:

复制代码
2024-11-01 05:09:20.788 logback [http-nio-8080-exec-13] INFO  c.x.c.u.RequestLoggingFilter [ed80915251674756adf3d51c7c89bfdb] 请求开始,URL: {http://......}
2023-11-01 05:09:20,789 logback [http-nio-8080-exec-13] INFO  c.e.a.ControllerAop [ed80915251674756adf3d51c7c89bfdb] -【com.xx.controller.CtrollerTest.testLog】【前后端交互--请求】{"param1":"Hello","param2":123}
2023-11-01 05:09:20,790 logback [http-nio-8080-exec-13] INFO  c.e.a.ControllerAop [ed80915251674756adf3d51c7c89bfdb] -【com.xx.controller.CtrollerTest.testLog】【前后端交互--响应--耗时:1ms】{"param1":"Hello","param2":123}
2024-11-01 05:09:20.800 logback [http-nio-8080-exec-13] INFO  c.x.c.u.RequestLoggingFilter [ed80915251674756adf3d51c7c89bfdb] 请求结束,URL: {http://......}

问题二

日志log....必须要注意的事项。

当有异常处理,不想抛出异常,但需要显示提示,并日志追踪,想便于日志排查时,那么必须使用重载哦****。否则,可能你什么都看不到

复制代码
try {
    // 某个业务操作
} catch(Exception e) {
    // 打印异常信息,有用debug,有的用info,有的用error(标准),但,方法使用不对,是没有什么作用的,只有很简单的信息文字,很难排查
    log.debug("debug异常信息:" + e.getMessage());
    log.info("info异常信息:" + e.toString());
    log.error("error异常信息:" + e);
    /** 
     * 以上无论哪个,也许显示的都是这样:xxx异常信息: / by zero  
     * 然后就没了,具体那一行报错,哪个类报错,都不知道,
     * 甚至某些错误可能 getMessage 就是个空字符串,那么你将只看到:xxx异常信息:
     * 排查问题时,你将无从下手。
     */
    java.lang.ArithmeticException: / by zero
    at ExampleClass.someMethod(ExampleClass.java:14)
    at ExampleClass.main(ExampleClass.java:21)
    
    // 一定要用这个,方法重载,且重载参数是一个异常对象:默认情况下log不会记录异常的堆栈跟踪信息,只有重载传递异常对象,才支持异常显现堆栈。
    log.debug("异常信息:" , e);
    log.info("异常信息:" , e);
    log.error("异常信息:" , e);
    /**很详细的日志信息,能确定是哪个类,那行,哪个位置
     * 异常信息: java.lang.ArithmeticException: / by zero
     * at ExampleClass.someMethod(ExampleClass.java:14)
     * at ExampleClass.main(ExampleClass.java:21)
     * ...省略N行...
     */
}

SpringBoot与LogBack日志(动态更新级别)

在开发和运维过程中,日志是诊断问题的重要工具。Spring Boot 提供了强大的日志管理功能,但默认情况下,日志级别是在启动时配置的(如上,启动的时候已经配置了日志级别为INFO)。

有时候,我们希望在应用程序运行时动态地调整日志级别,以便更灵活地进行调试和监控。比如生产日志通产为了节省服务器资源,会选择INFO级别,但是问题出现后,又想要DEBU详情,就需要动态更新了。

问题:更新日志级别,由 INFO -→ DEBUG || DEBUG -→ INFO 如果是更改项目配置文件,哪就需要进行服务重启,生产中在不进行版本迭代的情况下,服务重启,是禁忌!如何实现动态更新日志级别,且不重启应用呢?

方式一:SpringBoot监控(不推荐)

先说不推荐原因:生产,对外开放端点,是不明智的选择。当然,安全管理好了,也不是不可以。

Spring Boot Actuatorspring boot项目一个监控模块,提供了很多原生的端点,包含了对应用系统的自省和监控的集成功能,比如应用程序上下文里全部的Bean、r日志级别、运行状况检查、健康指标、环境变量及各类重要度量指标等等。因此可以通过其自带的日志监控,实现动态更新日志级别。

1. 搭建SpringBoot监控。

参考阅读"SpringBoot监控",里面有详细说明介绍。

2. 注意端点的开启,指定并开启loggers

复制代码
# 只暴露日志相关的端点
management.endpoints.web.exposure.include=loggers,logfile

# 启用日志文件端点
management.endpoint.logfile.enabled=true

# 配置日志级别
logging.level.root=INFO
logging.level.com.example=DEBUG

# 配置日志文件路径
logging.file.name=app.log
logging.file.path=/var/log/myapp

访问:localhost:8080/actuator/loggers,将获取到一个JSON数据:

复制代码
{
    "levels": [
        "OFF",
        "ERROR",
        "WARN",
        "INFO",
        "DEBUG",
        "TRACE"
    ],
    "loggers": { 
        "ROOT": {
            "configuredLevel": "INFO",
            "effectiveLevel": "INFO"
        },
        "com": {
            "configuredLevel": null,
            "effectiveLevel": "INFO"
        },
        "com.alibaba": {
            "configuredLevel": null,
            "effectiveLevel": "INFO"
        },
        "com.alibaba.druid": {
            "configuredLevel": null,
            "effectiveLevel": "INFO"
        }
        ....此处省略N行...
    }
}

简单说明:"ROOT"、"com"、"com.alibaba".....这些,就是日志记录器的名称,说白了就是你的包层次....着这个包层次,下的所有类,如果有日志,那么其对应的日志级别。(其中ROOT,最高级)

****    configuredLevel:配置级别(希望配置的级别)
    effectiveLevel:有效级别(当前正在使用的级别)****

****  另:子包路径的日志级别,会被父级别传递,反之,单独设置子包日志级别,不影响父包日志级别****

****  如上:若更新 ROOT 日志级别,其他所有级别会同步更新,更新**** com.alibaba 日志级别,com.alibaba......所有子级层次的,都会被更新,ROOT 、 com 则不受影响!

*****  3. 动态更新日志级别*****

*****  POST请求:http://localhost:8080/actuator/loggers/日志记录器名称*****

*****  请求参数:{"configuredLevel": "DEBUG"}*****

*****  重新访问:http://localhost:8080/actuator/loggers,看下JSON日志级别,就被更新了。此时,查看服务日志即可。*****

方式二:接口更新LoggerContext(不是强烈推荐,但条件下可以考虑)

先说不推荐原因:自己写接口维护,每次更新调用接口,不是特别方便,但可以用。

接口方式更新日志级别:条件情况可以考虑。如除了对外服务端,还是内部运维管理服务。可以将此接口提供内部服务的管理。由运维部操作使用(本身动态更新日志级别目的,就是为运维开展的)

调用接口:http://IP:端口/api/levelSetting?levelName=root&level=debug

复制代码
@Slf4j
@RestController
@RequestMapping("api")
public class HealthExaminationController {
    // 获取LoggerContext日志上下文对象
    private LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();

    /**
     * 设置指定日志记录器的日志等级【日志记录器名称,可查询SpringBoot的监控器,开启日志监控,访问:http://localhost:端口/actuator/loggers】
     *
     * @param levelName 日志记录器名称
     * @param level     日志等级
     * @return
     * @throws Exception
     */
    @GetMapping(value = "/levelSetting")
    public String levelSetting(@RequestParam("levelName") String levelName, @RequestParam("level") String level) {
        // 根据指定的日志记录器名称获取 logger
        Logger logger = loggerContext.exists(levelName);
        if (logger == null) {
            return levelName + ": The parameter logger Name not exist!";
        }
        log.info("更新日志之前:日志等级【{}】", logger.getLevel());
        // 解析 level 参数,第二个参数表示当 level 参数非法时的默认值
        Level newLevel = Level.toLevel(level, null);
        if (newLevel == null) {
            return level + ": The parameter logger level is not legal!";
        }
        // 重写设置 logger 的 level
        logger.setLevel(newLevel);
        log.debug("更新日志之后:日志等级【{}】", logger.getLevel());
        return "success! update logger Level to:" + logger.getLevel();
    }
}

方式三:Nacos配置中心(推荐,但不写)

先说不写原因:微服务知识点,既然本次编辑的是SpringBoot的,Cloud的东东放进来不是很好吧。所以分个类,可以看后续SpringCloud 、SpringCloud Alibaba的更新。这里由Nacos官方文档,也可以参考

1. 下载 Nacos 并启动 Nacos server

2. 引入Nacos配置依赖

3. 配置整合Nacos

4. 测试在Nacos控制台更新配置