
日志这件事说起来简单做起来坑多。我见过太多项目开发阶段日志全打在控制台上线之后服务器一重启出问题想查日志发现啥都没留下只能靠猜。也见过日志文件一天涨到几十个G把磁盘撑爆运维半夜打电话来骂人。所以今天我想认真聊聊 SpringBoot 项目里怎么用 Logback 把日志规规矩矩地写到本地文件包括滚动策略、异步写入、分级输出、格式定制这些实战细节。这篇内容适合所有用 SpringBoot 做后端开发的同学不管你是刚接触日志配置的新手还是已经用了几年但一直用默认配置的老手应该都能从里面找到一些之前没注意到的点。我会从整体设计思路讲起然后拆解核心配置再给出一套可以直接抄的完整方案最后分享一些踩坑记录和排查技巧。1. 整体设计思路与方案选型1.1 为什么不用默认配置SpringBoot 默认使用 Logback 作为日志框架这一点很多人知道。但默认配置有个问题它只往控制台输出不写文件。你如果在生产环境用java -jar启动项目日志全在终端里一旦终端关闭或者进程重启之前的日志就没了。有人会说我可以重定向到文件啊nohup java -jar xxx.jar app.log 21 这样确实能存下来但问题也很明显所有日志混在一个文件里没有滚动没有分级时间长了文件巨大查起来非常痛苦。所以我们需要自己配置 Logback让它按照我们的规则来写文件。核心目标有这么几个按天滚动、按大小切分、不同级别分开存、保留一定天数后自动清理、异步写入不阻塞业务线程。这几个目标看起来简单但每个都有细节要注意。1.2 Logback 的配置文件加载顺序在动手写配置之前得先搞清楚 SpringBoot 是怎么找到 Logback 配置文件的。默认情况下它会按以下顺序查找classpath:logback-spring.xmlclasspath:logback-spring.groovyclasspath:logback.xmlclasspath:logback.groovy注意这里有个关键区别logback-spring.xml和logback.xml是不一样的。前者是 SpringBoot 专属的可以使用springProfile和springProperty标签能根据不同的环境激活不同的配置。后者是 Logback 原生的SpringBoot 虽然也能加载但用不了 Spring 的扩展标签。我的建议是永远用logback-spring.xml。原因很简单你总不希望开发环境和生产环境用同一套日志配置吧开发的时候日志级别低一点方便调试生产环境级别高一点减少磁盘压力。用logback-spring.xml配合springProfile标签一个文件就能搞定多环境。1.3 滚动策略的选择逻辑日志文件如果不做滚动迟早会把磁盘写满。Logback 提供了两种滚动策略TimeBasedRollingPolicy和SizeAndTimeBasedRollingPolicy。前者按时间滚动后者按时间和大小双重条件滚动。那到底选哪个我的经验是用SizeAndTimeBasedRollingPolicy。为什么因为纯按时间滚动有个隐患如果某一天日志量突然暴增比如出了个死循环疯狂打日志那当天的文件可能涨到几十个G等你发现的时候磁盘已经满了。加上大小限制之后单个文件到了指定大小就会切分风险可控很多。具体参数怎么定一般来说单个文件控制在 100MB 到 500MB 之间比较合适。太小了文件数量多查日志要来回切换太大了打开慢用tail命令看也不方便。我一般设 200MB保留 30 天总大小上限设 10GB 左右。这样即使日志量大的项目也不会把磁盘撑爆。1.4 异步写入的必要性日志写入是磁盘 IO 操作如果同步写每次打日志都要等磁盘写完才继续执行在高并发场景下会严重影响性能。Logback 提供了AsyncAppender可以把日志写入操作放到单独的线程里执行业务线程只管往队列里丢不等待磁盘写完。但异步也有代价如果队列满了默认策略是丢弃日志。这个在排查问题时可能是致命的因为最关键的日志可能恰好被丢了。所以需要配置discardingThreshold为 0让队列满了之后阻塞而不是丢弃。同时队列大小要合理太小了容易满太大了内存占用高。我一般设 8192 或者 16384根据项目并发量调整。2. 核心配置细节与实操要点2.1 配置文件骨架先给一个完整的logback-spring.xml骨架后面再逐段解释?xml version1.0 encodingUTF-8? configuration springProperty scopecontext nameAPP_NAME sourcespring.application.name defaultValueapp/ property nameLOG_HOME value./logs/ property nameLOG_PATTERN value%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n/ !-- 控制台输出 -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 全部日志文件 -- appender nameFILE_ALL classch.qos.logback.core.rolling.RollingFileAppender file${LOG_HOME}/${APP_NAME}-all.log/file rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy fileNamePattern${LOG_HOME}/${APP_NAME}-all.%d{yyyy-MM-dd}.%i.log/fileNamePattern maxFileSize200MB/maxFileSize maxHistory30/maxHistory totalSizeCap10GB/totalSizeCap /rollingPolicy encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 错误日志文件 -- appender nameFILE_ERROR classch.qos.logback.core.rolling.RollingFileAppender file${LOG_HOME}/${APP_NAME}-error.log/file filter classch.qos.logback.classic.filter.LevelFilter levelERROR/level onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy fileNamePattern${LOG_HOME}/${APP_NAME}-error.%d{yyyy-MM-dd}.%i.log/fileNamePattern maxFileSize100MB/maxFileSize maxHistory60/maxHistory totalSizeCap5GB/totalSizeCap /rollingPolicy encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 异步包装 -- appender nameASYNC_ALL classch.qos.logback.classic.AsyncAppender discardingThreshold0/discardingThreshold queueSize8192/queueSize neverBlockfalse/neverBlock appender-ref refFILE_ALL/ /appender appender nameASYNC_ERROR classch.qos.logback.classic.AsyncAppender discardingThreshold0/discardingThreshold queueSize4096/queueSize neverBlockfalse/neverBlock appender-ref refFILE_ERROR/ /appender !-- 开发环境 -- springProfile namedev root levelINFO appender-ref refCONSOLE/ appender-ref refASYNC_ALL/ /root logger namecom.example levelDEBUG/ /springProfile !-- 生产环境 -- springProfile nameprod root levelINFO appender-ref refASYNC_ALL/ appender-ref refASYNC_ERROR/ /root logger namecom.example levelINFO/ /springProfile /configuration这个骨架已经能覆盖大部分场景了下面逐段拆解关键点。2.2 日志格式的定制LOG_PATTERN决定了每条日志长什么样。默认的格式信息太少排查问题时不够用。我推荐的格式是%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n逐段解释一下%d{yyyy-MM-dd HH:mm:ss.SSS}时间戳精确到毫秒。排查问题时毫秒级时间很重要尤其是并发场景下。[%thread]线程名。这个非常关键能帮你判断是哪个线程出的问题比如是业务线程还是定时任务线程。%-5level日志级别左对齐占5个字符。这样对齐好看也方便用grep过滤。%logger{50}日志所属的类名最多显示50个字符。太长了会省略包名保留类名。%msg%n日志内容和换行符。有人喜欢加%X{traceId}来做链路追踪这个看项目需求。如果用了 MDC 做链路追踪可以加上格式变成[%X{traceId}]。但要注意MDC 里的值需要手动清理否则会串到其他请求的日志里。2.3 滚动策略的参数计算SizeAndTimeBasedRollingPolicy的几个参数需要根据实际情况算一下maxFileSize单个文件最大大小。假设你的项目每天产生 2GB 日志那设 200MB 的话每天会切出 10 个文件。这个数量可以接受。如果设 50MB每天 40 个文件查起来就有点烦了。所以一般建议 100MB 到 500MB 之间。maxHistory保留多少天。这个要看你的合规要求和磁盘容量。一般业务系统保留 30 天够了核心系统可能需要 90 天甚至更长。计算公式是maxHistory × 每天日志量 总占用空间。比如每天 2GB保留 30 天就是 60GB。你得确保磁盘有这么多空间。totalSizeCap总大小上限。这个是最后一道防线即使没到 maxHistory 天数总大小超了也会删除最旧的文件。我一般设成maxHistory × 每天日志量 × 1.2左右留点余量。注意totalSizeCap需要配合maxHistory一起用单独设置totalSizeCap在某些 Logback 版本上不生效。另外清理动作是异步的不是实时触发所以实际占用可能会短暂超过设定值。2.4 分级输出的实现方式把 ERROR 级别的日志单独存一个文件好处是排查问题时直接看 error 文件不用在几万行 INFO 日志里翻。实现方式是用LevelFilterfilter classch.qos.logback.classic.filter.LevelFilter levelERROR/level onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter这段配置的意思是匹配到 ERROR 级别就接受不匹配就拒绝。注意这里是精确匹配WARN 级别的日志不会进这个文件。如果你想把 WARN 和 ERROR 都收进来需要用ThresholdFilterfilter classch.qos.logback.classic.filter.ThresholdFilter levelWARN/level /filterThresholdFilter是阈值过滤WARN 及以上级别都会通过。两种方式按需选择。2.5 异步 Appender 的配置陷阱AsyncAppender有几个参数需要特别注意queueSize队列大小默认 256。这个值太小了高并发下很容易满。建议设 8192 或 16384。但也不能太大因为队列里的日志对象占内存太大会导致 GC 压力。discardingThreshold默认值是 queueSize 的 20%。意思是队列剩余容量低于 20% 时会丢弃 TRACE、DEBUG、INFO 级别的日志只保留 WARN 和 ERROR。这个默认行为很坑因为出问题时恰恰是 INFO 日志最有价值。所以一定要设成 0禁止丢弃。neverBlock默认 false意思是队列满了之后阻塞业务线程。设成 true 的话队列满了直接丢弃日志不阻塞。生产环境建议保持 false宁可慢一点也不要丢日志。但如果你的业务对延迟极其敏感可以设 true但要接受丢日志的风险。实操心得异步 Appender 包装之后日志写入顺序可能和代码执行顺序不一致。因为多个线程往同一个队列里丢消费线程按队列顺序写。如果对顺序有严格要求比如审计日志建议不要用异步或者用单独的同步 Appender。3. 完整实操流程与关键环节3.1 环境准备与依赖确认SpringBoot 项目默认已经引入了spring-boot-starter-logging里面包含了 Logback 和 SLF4J。你不需要额外加依赖只要确认没有排除它就行。检查pom.xmldependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId !-- 不要排除 spring-boot-starter-logging -- /dependency如果你用了其他日志框架比如 Log4j2需要先排除 Logback。但既然标题是 Logback我们就默认用默认的。3.2 配置文件放置位置把logback-spring.xml放在src/main/resources目录下。SpringBoot 启动时会自动加载。不需要在application.yml里配置logging.config除非你用了自定义的文件名。如果你确实想自定义路径可以在application.yml里加logging: config: classpath:custom-logback-spring.xml但一般没必要用默认名字最省事。3.3 多环境配置的激活方式前面骨架里用了springProfile namedev和springProfile nameprod。这个name对应的是spring.profiles.active的值。比如你在application.yml里配了spring: profiles: active: dev那springProfile namedev里的配置就会生效。生产环境启动时用--spring.profiles.activeprod指定即可。这里有个细节springProfile的name属性支持逗号分隔的多个值比如namedev,test表示 dev 和 test 环境都生效。也支持!取反比如name!prod表示非生产环境生效。3.4 日志文件路径的处理骨架里用了./logs作为日志目录这是相对路径相对于项目启动时的工作目录。用java -jar启动时工作目录就是你执行命令的目录。这个路径在生产环境可能不太可控建议用绝对路径或者通过环境变量指定。更好的做法是用springProperty从application.yml里读取springProperty scopecontext nameLOG_HOME sourcelogging.file.path defaultValue./logs/然后在application.yml里配logging: file: path: /var/log/myapp这样不同环境可以配不同的路径配置文件不用改。3.5 验证配置是否生效配置写完之后怎么确认生效了有几个方法启动项目看控制台有没有报错。Logback 配置有问题会在启动时打印错误信息。检查日志目录下有没有生成对应的文件。在代码里打一条日志看文件里有没有写进去。用jinfo或者 SpringBoot Actuator 的/actuator/loggers端点查看当前生效的日志级别。如果文件没生成先检查目录权限。Linux 下如果目录不存在Logback 会尝试创建但如果没有写权限就会失败。建议提前创建好目录并赋予权限。3.6 代码里的日志写法配置再好代码里写得不规范也白搭。几个基本原则用 SLF4J 的 API不要直接用 Logback 的 API。这样以后换日志框架不用改代码。用占位符而不是字符串拼接。log.info(user {} login from {}, userId, ip)比log.info(user userId login from ip)好因为前者在日志级别不匹配时不会执行字符串拼接。异常日志要传异常对象。log.error(process failed, e)而不是log.error(process failed: e.getMessage())前者会打印完整堆栈。不要打敏感信息。密码、身份证号、银行卡号这些不要出现在日志里。import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class UserService { private static final Logger log LoggerFactory.getLogger(UserService.class); public void login(String userId, String ip) { log.info(user {} login from {}, userId, ip); try { // 业务逻辑 } catch (Exception e) { log.error(user {} login failed, userId, e); } } }4. 常见问题与排查技巧实录4.1 日志文件不生成这是最常见的问题。排查步骤排查项检查方法可能原因配置文件位置确认在src/main/resources下位置不对导致没加载文件名必须是logback-spring.xml或logback.xml名字写错目录权限ls -ld /var/log/myapp没有写权限配置语法看启动日志有没有报错XML 格式错误profile 激活确认spring.profiles.active的值profile 不匹配导致配置没生效我遇到过一次配置文件里用了springProfile nameprod但启动时没指定 profile结果所有 appender 都没生效。后来加了个默认的springProfile namedefault才解决。4.2 日志文件过大如果发现日志文件涨得很快先看看是什么日志在刷。常见原因框架的 DEBUG 日志没关。比如 MyBatis 打印 SQLHibernate 打印参数。这些在开发环境有用生产环境一定要关掉。定时任务打太多日志。比如每秒执行一次的任务每次都打 INFO 日志一天就是 86400 条。异常被循环捕获打印。比如在循环里 catch 异常然后 log.error一次请求可能打几千条。解决办法针对特定包设置日志级别。比如logger nameorg.mybatis levelWARN/ logger nameorg.hibernate levelWARN/ logger namecom.example.task levelWARN/4.3 异步日志丢失如果发现日志有缺失先检查discardingThreshold是不是默认值。默认情况下队列剩余 20% 时会丢弃 INFO 及以下级别。设成 0 可以解决。如果设了 0 还是丢那可能是队列满了并且neverBlock设成了 true。检查一下这个参数。还有一种可能是应用关闭时队列里的日志还没写完。Logback 在 JVM 关闭时会尝试 flush但如果用kill -9强制杀进程就来不及写了。建议用kill -15优雅关闭或者配置 SpringBoot 的优雅停机。4.4 日志时间不对日志时间比实际时间差几个小时一般是时区问题。检查 JVM 的时区设置java -XshowSettings:properties -version 21 | grep timezone如果时区不对启动时加参数java -Duser.timezoneAsia/Shanghai -jar app.jar另外日志格式里的%d{yyyy-MM-dd HH:mm:ss.SSS}默认用 JVM 时区一般不用额外配置。4.5 日志文件被占用无法删除Linux 下如果日志文件正在被写入直接rm删除后磁盘空间不会释放因为文件句柄还被进程持有。正确的做法是清空文件内容 /var/log/myapp/app-all.log或者用truncatetruncate -s 0 /var/log/myapp/app-all.log但更好的做法是让 Logback 自己管理滚动和清理不要手动删。4.6 排查技巧速查表问题现象可能原因解决方向文件不生成配置未加载/权限不足检查配置位置和目录权限文件过大日志级别过低/循环打印调整包级别检查代码日志丢失异步队列丢弃设 discardingThreshold0时间不对JVM 时区错误设置 user.timezone磁盘不释放文件句柄未释放用 truncate 而非 rm启动报错XML 语法错误检查标签闭合和属性最后分享一个我常用的排查命令组合tail -f app-all.log | grep -i error实时看错误日志grep -c ERROR app-error.log统计错误数量awk {print $1} app-all.log | sort | uniq -c按天统计日志量。这几个命令在排查线上问题时非常高效。这套配置我在多个项目里用过从日活几千的小系统到日活百万的中型系统基本都能扛住。关键是要根据实际日志量调整参数不要照搬。上线前一定要压测一下日志写入对性能的影响尤其是异步队列的大小和磁盘 IO 的瓶颈。