You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Logback配置问题:两个Appender输出至同一文件失败

问题分析与解决方案

你遇到的核心问题是Logback不允许多个FileAppender/RollingFileAppender实例写入同一个物理文件,这是Logback内置的保护机制,避免文件竞争、滚动策略冲突以及日志内容混乱。你看到的错误正是该机制触发的结果;而未配置rollingPolicy的TRIMMED_STDOUT不工作,是因为RollingFileAppender必须配置滚动策略才能正常初始化运行。

你的需求完全可行,但不需要创建两个Appender,而是在单个Appender内实现条件化的堆栈修剪——根据日志来源(Logger名称)自动切换是否使用修剪后的堆栈输出。


解决方案:单Appender实现条件化堆栈修剪

以下提供两种简洁的实现方式,无需额外Appender即可满足需求:

方式1:自定义复合转换器(最简洁)

直接编写一个转换器,根据当前日志的Logger名称自动决定是否修剪堆栈:

  1. 实现自定义转换器类:
import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;

public class ConditionalStackTraceConverter extends MessageConverter {
    private MessageStacktraceTrimmerTemplate trimmedConverter = new MessageStacktraceTrimmerTemplate();
    // 可通过配置修改目标Logger前缀
    private String targetLoggerPrefix = "com.netflix.discovery";

    public void setTargetLoggerPrefix(String targetLoggerPrefix) {
        this.targetLoggerPrefix = targetLoggerPrefix;
    }

    @Override
    public String convert(ILoggingEvent event) {
        // 判断日志来源是否为目标第三方库,是则使用修剪后的堆栈
        if (event.getLoggerName().startsWith(targetLoggerPrefix)) {
            return trimmedConverter.convert(event);
        }
        // 否则使用原始消息输出
        return super.convert(event);
    }

    @Override
    public void start() {
        trimmedConverter.setContext(context);
        trimmedConverter.start();
        super.start();
    }
}
  1. 修改Logback配置,注册并使用该转换器:
// 注册自定义转换器
conversionRule("conditional_message", ConditionalStackTraceConverter)

appender("STDOUT", RollingFileAppender) {
    file = "${LOG_PATH}/nohup.service"
    rollingPolicy(SizeAndTimeBasedRollingPolicy) {
        fileNamePattern = "${LOG_ARCHIVE}/stdout/nohup.service.%d{yyyy-MM-dd}.%i"
        maxHistory = 30
        maxFileSize = FileSize.valueOf("10 MB")
        totalSizeCap = FileSize.valueOf("2 GB")
    }

    encoder(PatternLayoutEncoder) {
        charset = StandardCharsets.UTF_8
        // 使用自定义的条件化转换器
        pattern = "%date %-5level - %conditional_message%n"
    }
}

root(INFO, ["STDOUT"])
logger("com.package.my", INFO, ["STDOUT"], false)
logger("com.netflix.discovery", INFO, ["STDOUT"], false)

方式2:TurboFilter + MDC标记(灵活扩展)

通过TurboFilter给目标日志打标记,再在Pattern中根据标记选择输出格式:

  1. 实现TurboFilter类:
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.turbo.TurboFilter;
import ch.qos.logback.core.spi.FilterReply;
import org.slf4j.MDC;

public class StackTraceTrimMarkerFilter extends TurboFilter {
    private static final String TRIM_FLAG = "TRIM_STACK_TRACE";
    private String targetLoggerPrefix;

    public void setTargetLoggerPrefix(String targetLoggerPrefix) {
        this.targetLoggerPrefix = targetLoggerPrefix;
    }

    @Override
    public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {
        // 给目标第三方库的日志打标记
        if (logger.getName().startsWith(targetLoggerPrefix)) {
            MDC.put(TRIM_FLAG, "true");
        } else {
            MDC.remove(TRIM_FLAG);
        }
        return FilterReply.NEUTRAL;
    }
}
  1. 修改Logback配置:
conversionRule("trimmed_message", MessageStacktraceTrimmerTemplate)
conversionRule("original_message", ch.qos.logback.classic.pattern.MessageConverter)

// 注册TurboFilter
turboFilter("StackTraceTrimMarkerFilter") {
    targetLoggerPrefix = "com.netflix.discovery"
}

appender("STDOUT", RollingFileAppender) {
    file = "${LOG_PATH}/nohup.service"
    rollingPolicy(SizeAndTimeBasedRollingPolicy) {
        fileNamePattern = "${LOG_ARCHIVE}/stdout/nohup.service.%d{yyyy-MM-dd}.%i"
        maxHistory = 30
        maxFileSize = FileSize.valueOf("10 MB")
        totalSizeCap = FileSize.valueOf("2 GB")
    }

    encoder(PatternLayoutEncoder) {
        charset = StandardCharsets.UTF_8
        // 根据MDC标记选择输出格式
        pattern = "%date %-5level - %mdc{TRIM_STACK_TRACE, %trimmed_message, %original_message}%n"
    }
}

root(INFO, ["STDOUT"])
logger("com.package.my", INFO, ["STDOUT"], false)
logger("com.netflix.discovery", INFO, ["STDOUT"], false)

关于Prudent模式的说明

如果你坚持使用双Appender方案,确实可以开启prudent = true模式,但该模式会强制以追加方式打开文件、禁用文件锁,会显著影响日志写入性能,且滚动策略的同步逻辑容易出问题。此外,两个Appender的fileNamePattern必须区分(比如加后缀),不符合你输出到同一文件的需求,因此不推荐使用。

内容的提问来源于stack exchange,提问作者Vasiliy Tungusov

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 08:50:55