Spring Boot 项目 Logback 日志配置全攻略:从入门到线上实战

发布时间:2026/10/11 20:21:52
Spring Boot 项目 Logback 日志配置全攻略:从入门到线上实战
日志这东西平时没人爱看一出事人人都喊救命。我在项目里折腾过好几套日志方案从刚开始把System.out.println用得飞起到后来被线上日志坑得连夜加班最终稳定在 Spring Boot 加 Logback 这一套上。说实话Spring Boot 默认就帮我们把日志集成了但默认归默认想让它贴合自己的项目比如按天分割、保留多少天、不同环境不同级别、把关键请求串起来查还是要自己动手写配置。这篇文章就是把我这些年踩过的坑、沉淀下来的配置模板、还有排查思路一次性交代清楚。1. 先搞清楚 Logback 在 Spring Boot 里的地位1.1 Spring Boot 为什么默认就选了 Logback很多人用了很长时间 Spring Boot都不知道日志这件事它已经替你做完了大半。Spring Boot 的spring-boot-starter-logging是被自动引入的里面整合了 SLF4J 作为门面Logback 作为实现。也就是说你只要在类里写上private static final Logger logger LoggerFactory.getLogger(Xxx.class)就能直接往控制台输出日志不需要任何额外依赖和配置。为什么要这样设计层面的道理其实很朴素SLF4J 相当于一个统一的插座Logback 是插头上那个具体的电器。开发者写代码的时候面对的是 SLF4J 的 API将来想换成 Log4j2、想换成 JUL代码基本不用动只改依赖和配置文件。我第一次接触这个设计的时候觉得有点多此一举后来维护过一个老项目日志从 Log4j 1.x 迁移到 Logback因为业务代码里全是具体实现类的调用改了几百处那才叫一个酸爽。所以不管你现在项目多小我都建议日志 API 只用 SLF4J。还有个细节很多人不知道Spring Boot 官方文档里明确写过Logback 是首选因为它在性能和功能之间平衡得好配置语法也灵活。那为什么还经常有人问“我的日志为什么不输出”十有八九是配置文件没被加载或者被别的框架的日志实现劫持了。这类问题我在后面专门列一节。1.2 自定义日志前要想清楚的三件事我在接手任何一个新项目的日志需求时都会先问自己三个问题日志写到哪儿、长什么样、留多久。别急着写配置这三件事没想明白后面的配置全是白搭。第一件写到哪儿。本地调试阶段控制台就够了但部署到服务器上日志必须落到文件里因为进程一重启控制台的内容就没了。文件还得考虑按天还是按大小切分比如一天一个文件或者单个文件超过 100MB 就切一个新的。第二件长什么样。每行日志包含哪些信息决定了你排查问题的时候效率有多高。我常用的格式里至少有这些时间、级别、线程名、logger 名也就是类名、消息内容。再加上 MDC 里的 traceId就能把一次请求的所有日志串成一条线。后面我会详细展开。第三件留多久。这个最容易被忽略。磁盘不是无限的日志文件动辄几个 GB不设置清理策略半年后你的磁盘就会报警。常见的策略是保留 7 天或者 30 天同时限制总大小。这三件事对应到 Logback 配置里分别就是 Appender、EncoderPatternLayout和 RollingPolicy。弄明白这三大块Logback 的配置你就掌握了七八成。2. 配置文件选型logback.xml 还是 logback-spring.xml2.1 两者的区别与 Spring Boot 的扩展点刚学 Logback 的时候肯定有人告诉你要建logback.xml也有人说要logback-spring.xml我刚接触时也晕。这两者到底什么关系logback.xml是 Logback 原生的配置文件Logback 自己就能识别logback-spring.xml是 Spring Boot 额外支持的命名。Spring Boot 的官方推荐就是后者因为它允许你使用 Spring 的两个扩展标签springProfile和springProperty。前者可以针对不同的环境 Profiledev、test、prod加载不同的配置段后者可以直接读取application.yml里的属性值到日志配置中使用。如果你用了logback.xmlSpring Boot 虽然也能加载但springProfile标签是用不了的因为 Spring Boot 的扩展逻辑只在解析logback-spring.xml时才生效。我在项目里吃过这个亏刚开始图省事写了logback.xml想用 Profile 区分日志级别结果配置一点效果都没有日志查了半天才知道是文件命名的问题。所以结论很简单在 Spring Boot 项目里优先使用logback-spring.xml除非你有本事让 Logback 原生支持那套扩展。2.2 分环境配置的三种实现方式环境隔离是日志配置里最常见的需求。开发环境刷 INFO 甚至 DEBUG 都无所谓生产环境一般只保留 INFO 以上而且文件路径也完全不同。我总结下来有三种实现方式。第一种在同一个logback-spring.xml里用springProfile做分区。我把控制台 Appender 和文件 Appender 都声明好然后用springProfile namedev把 dev 环境的 logger 级别包起来用springProfile nameprod包另一个。一个文件通吃所有环境的好处是配置集中坏处是配置乱起来以后文件里到处都是if else的感觉每次想调整格式都要小心翼翼。第二种通过application.yml里的属性来控制。比如在 yml 里定义log.level.root: INFO、log.path: /data/logs/myapp然后logback-spring.xml里用springProperty把这个值引进来。想改级别不用动 XML只改配置文件对运维同学特别友好。我现在的主力项目就是这套玩法配置结构清晰安全性也高。第三种不同的环境放不同的配置文件比如logback-dev.xml、logback-prod.xml然后在 yml 里通过logging.config指定用哪一个。这种方式最直接但坏处是同一套 Appender 定义要在多个文件里复制改一处漏一处的问题特别容易出现。提示如果你的日志配置想通过配置中心统一管理第二种方式是最合适的。因为配置中心一般擅长下发 key-value而不是让你替换整个 XML。把级别、路径这些全部抽成配置项运维同学不用碰代码就能调整日志行为。3. 核心配置项详解从格式到滚动策略3.1 Pattern 格式串里的大多数常用符号Logback 输出日志的每一行内容是由encoder里的pattern决定的。我见过不少人图省事直接复制网上的模板出了事又说日志格式不符合要求其实就是没搞懂每个符号是干什么的。最常用的几个占位符我用一张表给你列明白占位符含义示例输出%d时间2024-12-15 10:15:30.123%level日志级别INFO / WARN / ERROR%thread线程名http-nio-8080-exec-1%loggerlogger 名称一般指类名com.example.order.OrderService%msg日志消息用户下单成功订单号: 10001%n换行符%X{traceId}MDC 中的键值8f3a2c9d1e5b4a7f%highlight根据级别让控制台输出带颜色INFO 绿色ERROR 红色%line代码行号42%logger默认会把完整包名都打出来挺长。可以用精度控制最常见的是%logger{0}只显示类名或者%logger{36}限制长度。我不建议只用类名因为排查问题时经常遇到同名类分布在不同包光看类名还得去翻代码。我一般保留到最后一层包加类名的形式比如com.example.order.OrderService一眼就能定位。还有个细节格式串里那些文字比如[、]、-会原样输出。有人喜欢把日志输出成 JSON 格式给日志采集系统用那就不该用 Pattern 而应该用LogstashEncoder或者手动拼接 JSON 字符串。不过这是另一套玩法入门阶段先把基础 Pattern 玩熟。3.2 RollingPolicy 滚动策略与备份清理滚动策略是指日志文件写到一定条件后自动切换新文件同时老文件可以被压缩、被清理。Logback 里的核心类有TimeBasedRollingPolicy和SizeAndTimeBasedRollingPolicy。我的经验是直接上 SizeAndTimeBasedRollingPolicy因为它同时兼顾按时间和按大小两个维度。拿一个典型的配置来说appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_PATH}/${APP_NAME}.log/file rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy fileNamePattern${LOG_PATH}/history/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz/fileNamePattern maxFileSize100MB/maxFileSize maxHistory15/maxHistory totalSizeCap10GB/totalSizeCap /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{40} - %msg%n/pattern /encoder /appender这里maxFileSize100MB意味着当天的文件到了 100MB 就切一个带%i序号的新文件%d{y-MM-dd}让文件名里带上日期。maxHistory15表示最多保留 15 天超过的自动删除。totalSizeCap10GB表示所有日志文件的总大小限制在 10GB如果超过了即便还没到 15 天也会清理最老的文件。从实际使用来说totalSizeCap这个参数最容易被忽略。前面提的磁盘报警很多时候就是因为没设这个上限。我见过有同事只配了maxHistory结果某业务系统被刷屏一天写出 20GB 日志磁盘直接被打满。所以两个参数一起设双保险。3.3 异步日志到底要不要开高并发场景下同步写日志是会被鄙视的。因为一次磁盘 IO 可能阻塞业务线程接口耗时蹭蹭往上涨。Logback 提供了AsyncAppender把日志事件先丢进一个队列后台线程再真正写到文件里业务线程就快速返回了。配置上就是在文件 Appender 外面再包一层appender nameASYNC_FILE classch.qos.logback.classic.AsyncAppender queueSize8192/queueSize discardingThreshold0/discardingThreshold neverBlocktrue/neverBlock appender-ref refFILE/ /appender这里几个参数口头解释一下队列大小queueSize默认 256我一般调成 8192discardingThreshold为 0 表示队列满了也不丢日志但注意如果和neverBlock搭配要小心neverBlocktrue的意思是队列满了直接丢弃日志也不阻塞业务线程。从业务稳定性角度来说丢日志比拖垮接口要好但你要是特别在意日志完整性就得自己权衡。我的建议是核心业务系统、接口 QPS 比较高的上异步内部管理系统、并发量不大的同步完全够用没必要为了异步而异步。异步会带来日志延迟真出故障排查的时候日志可能滞后几秒钟这个心理预期要有。4. 实操搭一套可复用的自定义日志方案4.1 一版直接能用的 logback-spring.xml前面理论讲那么多不如直接给一套我项目里沉淀下来的配置。这套配置我用了两年多改过几格细节整体骨架一直没动过。?xml version1.0 encodingUTF-8? configuration scantrue scanPeriod60 seconds !-- 从 application.yml 读取属性 -- springProperty scopecontext nameAPP_NAME sourcespring.application.name defaultValueapp/ springProperty scopecontext nameLOG_PATH sourcelog.path defaultValue/data/logs/ !-- 控制台输出 -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) [%thread] %cyan(%logger{40}) - %msg%n/pattern charsetUTF-8/charset /encoder /appender !-- 文件输出按天大小滚动保留15天总大小上限 -- appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_PATH}/${APP_NAME}.log/file rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy fileNamePattern${LOG_PATH}/history/${APP_NAME}.%d{yyyy-MM-dd}.%i.log.gz/fileNamePattern maxFileSize100MB/maxFileSize maxHistory15/maxHistory totalSizeCap10GB/totalSizeCap /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{40} - %msg%n/pattern charsetUTF-8/charset /encoder /appender !-- 异步包装 -- appender nameASYNC_FILE classch.qos.logback.classic.AsyncAppender queueSize8192/queueSize neverBlocktrue/neverBlock appender-ref refFILE/ /appender !-- 不同环境的级别控制 -- springProfile namedev root levelDEBUG appender-ref refCONSOLE/ appender-ref refASYNC_FILE/ /root /springProfile springProfile nametest,prod root levelINFO appender-ref refCONSOLE/ appender-ref refASYNC_FILE/ /root /springProfile /configuration这套配置放到src/main/resources下命名为logback-spring.xml应用启动时就会被自动加载。对应的application.yml里加上两个配置项spring: application: name: order-service log: path: /data/logs/order-service启动后控制台会输出彩色日志文件里是纯文本格式。文件路径不存在时Logback 会自动创建history目录不需要你手动建。4.2 关键扩展敏感字段脱敏与链路追踪基础配置跑起来之后很多项目会面临两个进阶需求日志脱敏和链路追踪。脱敏这件事说白了就是在日志输出前把手里的手机号、身份证号、银行卡号等敏感信息打码。我见过最粗暴的解法是在业务代码里手动替换比如log.info(用户手机号{}, PhoneUtil.mask(phone))。这种方式代码侵入性太强到处都要改还容易漏。更好的做法是用 Logback 的Converter机制写一个自定义转换规则。我来演示一个简易版把消息里的手机号自动正则替换成中间四位带星号的形式import ch.qos.logback.classic.pattern.MessageConverter; import ch.qos.logback.classic.spi.ILoggingEvent; import java.util.regex.Matcher; import java.util.regex.Pattern; public class MaskingConverter extends MessageConverter { private static final Pattern PHONE_PATTERN Pattern.compile((1[3-9]\\d)\\d{4}(\\d{4})); Override public String convert(ILoggingEvent event) { String message super.convert(event); if (message null) { return null; } Matcher matcher PHONE_PATTERN.matcher(message); return matcher.replaceAll($1****$2); } }然后在配置里注册这个转换器并把%msg换成%maskconversionRule conversionWordmask converterClasscom.example.common.log.MaskingConverter/ ... pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{40} - %mask%n/pattern这样所有日志消息里的手机号都会被自动打码业务代码完全不需要动。这个思路可以扩展到身份证、邮箱等任意规则核心就是自己写正则替换逻辑。链路追踪那个就更好办了利用 Logback 的 MDC。MDC 的全称是 Mapped Diagnostic Context简单理解就是一个线程局部 Map。你在过滤器里往 MDC 里放一个请求 ID之后在这个线程里打的每条日志都能通过%X{traceId}把这个 ID 打出来而且随着 ThreadLocal 自动传递。import org.slf4j.MDC; public class TraceIdFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { String traceId UUID.randomUUID().toString().replace(-, ); MDC.put(traceId, traceId); try { chain.doFilter(request, response); } finally { MDC.remove(traceId); } } }注意MDC.remove要放在finally里。因为线程池里的线程会被复用你不 remove 的话下一条请求打日志时会带着上一条请求的 traceId排查问题的时候直接怀疑人生。4.3 让日志打出来的信息更有价值配置到位只是第一步日志打得好不好还得看你写日志的功力。我在 code review 里对日志提过不少意见总结下来最常说的是这几点。第一别打无用日志。有人习惯在每个方法入口打一条 “进入方法 xxx”每个出口打一条 “离开方法 xxx”结果全链路几十条日志关键信息全被淹没了。日志的价值在于关键节点比如参数校验失败、调用外部接口超时、数据库操作异常、业务状态变更这些节点值得打日志。第二使用占位符而不是字符串拼接。这是一个老生常谈的问题但总有人犯。log.info(user: userId ,name: name)这种方式有两个问题一旦name为 null字符串拼接会变成 null 而不是报错容易掩盖问题而且即使日志级别不输出这条日志拼接操作依然会发生白白浪费性能。正确写法是log.info(用户信息 userId{}, name{}, userId, name);占位符只有在真正要输出日志时才会去解析参数而且参数是 null 时打印出来也是清晰可辨的。从对排查问题来说建议把{}前面加上键名这样你看到userId123, name张x比看一句 “123 张x” 要舒服得多。第三打异常日志把堆栈打全。log.error(xxxx 失败)只打一句话异常堆栈全部丢掉这是最让人头疼的。至少要这样写log.error(调用订单服务失败, orderId{}, orderId, exception);只要把异常对象作为最后一个参数传进去Logback 就会自动把堆栈打出来。这么做也是排查线上问题的基本素养。我在实际排查中见过太多“只给结论不给证据”的日志看到这种日志只能去翻源码猜问题。5. 常见问题与排查技巧实录5.1 日志不输出、级别不生效先检查这三处每次有人群里问“我配了 logback 为什么日志没出来”我脑子里就会自动跳出三个排查点配置文件名字对不对、是不是被其他日志框架劫持了、级别是否被覆盖。配置文件名字是第一道关卡。如果你建的是logback.xmlSpring Boot 会加载它但springProfile等 Spring Boot 的扩展标签就完全不会生效于是你会发现日志永远只有默认 console 输出文件也不生成。解决方案前面提过了——用logback-spring.xml。第二道关卡是依赖冲突。Spring Boot 项目里如果引入了别的框架而那个框架自带了 Log4j 或者别的日志实现就可能导致 SLF4J 绑定混乱。经典的报错信息是 “SLF4J: Class path contains multiple SLF4J bindings”出现这个基本就是 pom 里有重复的日志依赖。排查方式很简单在 pom.xml 里搜索log4j、logback-classic、slf4j-log4j12等关键词把多余的依赖用exclusions排除掉。第三道关卡是application.yml里的logging.level配置覆盖了 XML 里的级别。Spring Boot 的logging.level.com.exampleDEBUG这一招是可以在配置文件中直接调级别但如果配置跟 XML 冲突实际生效的是更具体的那一个。所以排查级别问题时先看这两个地方有没有矛盾。5.2 文件乱码、路径不可写、时区不对文件乱码这种问题十有八九是编码问题。项目里统一 UTF-8 的情况下控制台乱码可能是 Windows 终端的锅文件乱码就一定是 encoder 里没指定 charset。我在配置里每一项都加了charsetUTF-8/charset这是个好习惯不要偷懒。路径不可写的问题在 Linux 上经常出现。比如你把LOG_PATH配成/var/log/myapp但当前用户没有写权限那应用启动的时候要么直接报错要么日志文件根本不出来。这个问题我们曾被坑过后面干脆统一约定应用日志都写到/home/{应用名}/logs下而且启动脚本里自动创建目录并赋权。如果你用容器部署建议把日志目录挂载到宿主机别写在容器内部容器一删日志全没。时区不对是个隐蔽问题。%d默认取 JVM 默认时区如果服务器是 UTC日志时间和北京时间差 8 小时排查线上问题时会非常绕。要么在启动参数里加-Duser.timezoneAsia/Shanghai要么在 pattern 里直接写成%d{yyyy-MM-dd HH:mm:ss.SSS, Asia/Shanghai}。我更喜欢前面那种因为不止日志整个 Java 进程的时间概念都一致。5.3 日志刷屏、磁盘占用过高、应用卡顿日志刷屏这件事我参与过最惊险的一次事故业务方某个接口被恶意调用每秒钟产生几千条 ERROR 日志磁盘半小时被写满应用直接卡死。后来我们做了几件事第一给文件加的totalSizeCap保证磁盘不会被无限占用第二加了限流异常日志也做了聚合同一错误每分钟最多打 N 条第三用异步 Appender 把写磁盘的压力和业务线程隔离。如果你当前的系统也会被刷屏建议至少把第二点和第三点安排上。应用卡顿还有另一种情况某些版本的 Logback 在高并发下同步写文件会有锁竞争表现就是接口变慢、GC 明显。这时候除了上异步还要把队列大小调得足够大避免队列频繁满。但千万别把queueSize调成几十万内存会被撑爆。一套合适的值是 8192 到 65536 之间具体取决于你的日志量级。排查阶段最实用的手段还有两个一个是scantrue和scanPeriod60 seconds这样你改了 XML 配置不需要重启应用60 秒内自动生效本地调格式特别方便另一个是设置statusListener classch.qos.logback.core.status.OnConsoleStatusListener/这样应用启动的时候Logback 会把找到的配置、加载的 Appender、识别出的 profile 等信息全部打到控制台很多配置不生效的问题一眼就能看出来。排障期间我会临时打开调完再关掉保持日志干净。这里再插一个细节。异步日志队列满了之后的行为Logback 默认是丢弃 INFO 及以下级别的日志保留 WARN 和 ERROR。如果你不希望关键日志被丢掉就不要把discardingThreshold设为 0甚至可以把neverBlock设为 false。但代价是可能阻塞业务线程。这套参数的取舍没有标准答案完全取决于你的业务更怕丢日志还是更怕接口超时。日志配置这玩意儿平时不起眼真到事故现场就是救命稻草。我这些年最大的体会是日志不是写了就行它是需要当成正式工程去维护的。每次上线前多问一句日志能不能撑住格式够不够用清理策略有没有定好能帮你省掉很多半夜起来查日志的折腾。最后再分享一个小技巧吧把这套logback-spring.xml做成你们团队内部项目的标准模板新项目直接抄过去只改应用名和日志路径既统一又省心。