Logback配置问题:两个Appender输出至同一文件失败
问题分析与解决方案
你遇到的核心问题是Logback不允许多个FileAppender/RollingFileAppender实例写入同一个物理文件,这是Logback内置的保护机制,避免文件竞争、滚动策略冲突以及日志内容混乱。你看到的错误正是该机制触发的结果;而未配置rollingPolicy的TRIMMED_STDOUT不工作,是因为RollingFileAppender必须配置滚动策略才能正常初始化运行。
你的需求完全可行,但不需要创建两个Appender,而是在单个Appender内实现条件化的堆栈修剪——根据日志来源(Logger名称)自动切换是否使用修剪后的堆栈输出。
解决方案:单Appender实现条件化堆栈修剪
以下提供两种简洁的实现方式,无需额外Appender即可满足需求:
方式1:自定义复合转换器(最简洁)
直接编写一个转换器,根据当前日志的Logger名称自动决定是否修剪堆栈:
- 实现自定义转换器类:
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(); } }
- 修改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中根据标记选择输出格式:
- 实现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; } }
- 修改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
相关产品推荐
相关产品推荐

