如何根据api重试次数 来动态改变日志级别

背景:

一个调用远程 REST API 的请求是可能失败的,可能比较好的解决方案是: 正常调用 INFO, 重试(假设2次)用 WARN, 最后仍然失败才使用Error
当然根据业务不同,有的地方可能不需要重试或不能重试,但我们现在的很多业务都是需要加上幂等重试的。
------------------

在开发中,根据请求的尝试次数(重试次数)来动态改变日志级别,是一个非常专业且优雅的重设。这能有效防止日志刷屏(避免偶尔的网络抖动触发大量的 ERROR 报警)。

要实现这个功能,通常不能靠纯粹的日志配置文件(如 logback.xmlapplication.yml,因为日志框架本身无法感知你的业务重试了多少次。

最佳的解决方案是:在你的代码(代码逻辑或重试框架)中,根据当前的重试计数(Retry Count)来动态决定调用 logger.info()logger.warn() 还是 logger.error()

下面为你提供两种主流、优雅的实现方案:

方案一:使用 Spring Retry 框架(推荐,最优雅)

如果你使用的是 Spring Boot,Spring Retry 提供了强大的重试机制。你可以通过自定义一个 RetryListener(重试监听器),在每次失败时判断次数并输出对应级别的日志。

1. 引入依赖

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
</dependency>

2. 自定义日志重试监听器

 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.listener.RetryListenerSupport;
import org.springframework.stereotype.Component;

@Component
public class LogRetryListener extends RetryListenerSupport {
    private static final Logger log = LoggerFactory.getLogger(LogRetryListener.class);

    @Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        int count = context.getRetryCount(); // 当前是第几次尝试失败(从 1 开始)

        if (count == 1) {
            // 第一次失败,打印 INFO 
            log.info("远程API调用第 1 次失败,准备重试。异常信息: {}", throwable.getMessage());
        } else if (count == 2) {
            // 第二次失败,打印 WARN
            log.warn("远程API调用第 2 次失败,最后一次重试机会!异常信息: {}", throwable.getMessage());
        }
        // 注意:最后一次(第三次)彻底失败,不在这里打印 ERROR,而在 @Recover(兜底方法)里打印
    }
}

3. 在业务代码中使用

 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class RemoteCallService {
    private static final Logger log = LoggerFactory.getLogger(RemoteCallService.class);

    // maxAttempts = 3 代表总共调用3次(1次正常调用 + 2次重试)
    // listeners 指定我们刚才写的日志监听器
    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000), listeners = "logRetryListener")
    public String callRemoteApi() {
        // 模拟你的远程 REST API 调用
        // restTemplate.getForObject(...);
        throw new RuntimeException("连接超时"); 
    }

    // 3次全部失败后,会进入这个兜底方法,此时打印 ERROR
    @Recover
    public String recover(RuntimeException e) {
        log.error("远程API调用经历2次重试后仍然彻底失败!触发报警。错误原因: ", e);
        return "默认降级数据";
    }
}

方案二:纯代码手动控制(不依赖框架,最直观)

如果你没有使用 Spring,或者不想引入复杂的重试框架,可以直接用一个简单的 for 循环加 try-catch 来手写这个逻辑:

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

public class ApiClient {
    private static final Logger log = LoggerFactory.getLogger(ApiClient.class);

    public String executeWithRetry() {
        int maxAttempts = 3; // 1次正常 + 2次重试
        
        for (int attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                // 模拟调用 API
                return doCallApi(); 
            } catch (Exception e) {
                if (attempt == 1) {
                    // 第一次调用失败: e.getMessage(0 打印很干净,不占地方
                    log.info("远程API首次调用失败,进行第1次重试。错误: {}", e.getMessage());
                } else if (attempt == 2) {
                    // 第二次调用失败(第一次重试失败)
                    log.warn("远程API重试失败,进行第2次重试。错误: {}", e.getMessage());
                } else {
                    // 第三次调用失败(第二次重试也失败了):会打印报错的整个堆栈信息,比如哪一行,调用链,底层驱动报错的异常都会有打印出来
                    log.error("远程API历经2次重试后仍然全部失败!", e);
                    throw e; // 抛出异常或返回降级数据
                }
                
                // 可以加一个短暂的等待再重试,比如 1 秒
                try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
            }
        }
        return null;
    }

    private String doCallApi() {
        throw new RuntimeException("网络连接异常");
    }
}

总结与建议

  • 为什么不推荐在日志配置文件里配? 因为日志文件只能根据“包名”、“类名”或“特定的 Marker”来区分级别,它不知道你当前是在进行第几次重试。

  • 选哪种? 如果你的项目已经是 Spring Boot 项目,强烈建议用 方案一(Spring Retry),代码侵入性极低,后续修改重试次数(比如改成重试 3 次)也非常好扩展。

  • 在日志文件中的“待遇”不同(可以根据项目的日志管理框架去配置)

    在生产环境中,日志配置文件(如 logback-spring.xml)通常会做日志分流

    • INFO 日志:通常只保存在 info.log 中。因为流量大,为了防止硬盘被撑爆,可能只保留最近 7 天,过期的就自动删除了。

    • ERROR 日志:通常会额外单独输出到一个 error.log 文件中,并且保留时间非常长(比如 30 天甚至 90 天)。

posted @ 2026-06-15 15:08  苹果芒  阅读(4)  评论(0)    收藏  举报