
做SpringBoot项目我最怕听到的一句话是“我本地跑好好的一上测试环境就出错”。这种时候 debug 没法用远程断点又嫌麻烦唯一能指望的就是日志文件。SpringBoot 默认集成的日志框架足够成熟日志文件的事其实不难但现实中我见过太多人栽在同一批坑上日志文件不生成、配置了滚动但磁盘还是被撑爆、中文乱码、生产环境日志大得用记事本都打不开。这篇文章就围绕 SpringBoot 日志文件这个主题把那点压箱底的东西翻出来讲讲默认日志体系是怎么工作的、文件配置参数怎么设、日志文件怎么看怎么查、线上排查怎么用、还有我自己踩过的那些坑。适合刚接触 SpringBoot 日志的新手也适合已经写了半年一年业务代码、想系统性搞懂日志文件这回事的朋友。保证你看完能直接上手把日志文件这块安排得明明白白。1. SpringBoot日志体系先搞懂底层是怎么回事1.1 日志门面SLF4J与默认实现Logback的关系SpringBoot 的日志体系可以拆成两层看一层是门面一层是实现。门面用的 SLF4J实现用的是 Logback。打个比方SLF4J 是餐厅里的菜单Logback 是后厨掌勺的师傅。你写业务代码的时候只管照着菜单点菜不用关心今天是哪位师傅做菜。将来想换师傅比如换成 Log4j2也不用把菜单重印一遍代码完全不用动。import org.slf4j.Logger; import org.slf4j.LoggerFactory; RestController public class UserController { private static final Logger log LoggerFactory.getLogger(UserController.class); GetMapping(/user/{id}) public User getUser(PathVariable Long id) { log.info(查询用户信息, id{}, id); return userService.getById(id); } }上面这种写法就是标准的 SLF4J 用法。LoggerFactory.getLogger拿到的是一个日志实例你只管调用info、warn、error这些方法至于底层是 Logback 还是 Log4j2代码层面完全无感知。SpringBoot 的spring-boot-starter-logging这个 starter 已经把 SLF4J 绑定到 Logback 了所以你新建一个 SpringBoot 项目什么都不用配log.info就能直接往控制台打印。这一点和传统的 Spring MVC 项目差别很大以前你得自己往 classpath 里塞 log4j.properties、log4j.xml还要处理各种版本冲突SpringBoot 把这些全自动装配掉了。1.2 日志级别与默认输出规则Logback 的日志级别从低到高分别是 TRACE、DEBUG、INFO、WARN、ERROR。级别本质上是一个过滤器如果你的应用级别设成 INFO那么 DEBUG 和 TRACE 就不输出设成 DEBUGINFO 及以上的都输出。SpringBoot 的默认日志级别是 INFO。也就是说默认情况下 TRACE 和 DEBUG 不会出现在日志文件里。这一点经常有人忽略线上排查问题时想看 SQL 参数、看请求头细节发现日志里没有其实是级别没放开。想调级别有两种方式。一种是临时改通过 Spring Boot Actuator 的/actuator/loggers端点POST 请求就能动态调整某个类的日志级别不用重启服务。另一种是永久改在application.properties里写logging.level.com.example.mapperdebug logging.level.rootinfo上面这段表示com.example.mapper包下的所有类按 DEBUG 级别输出其他包按默认 INFO。实际开发里排查问题时我经常先把 MyBatis Mapper 包的日志级别临时调到 DEBUG就能看到完整的 SQL 语句和参数绑定过程查完再调回去。这里有一个值得说的点日志级别不是“配置了就会真的全部打印”。Logback 内部还有一个判断逻辑如果某个 Logger 没有显式配置级别会往上层 Logger 找一直找到 root。所以你在 properties 里写的logging.level.rootinfo是兜底的规则某个包单独设置了级别就优先用包的级别。1.3 为什么控制台有日志但磁盘上没有文件这是新人最常见的问题之一。SpringBoot 默认的spring-boot-starter-logging确实帮你配好了 Logback但默认配置里只有CONSOLE输出没有FILE输出。也就是说你第一眼能看到控制台密密麻麻的日志但这些日志不会主动落盘磁盘上不会自己冒出个 log 文件。想要生成日志文件必须在配置里告诉 SpringBoot 两件事文件放哪、文件名是什么。这就是下一节要讲的核心内容。我见过不少人折腾半天“日志文件不生成”其实原因就是根本没配logging.file.name或logging.file.pathSpringBoot 不知道你想写文件。2. 日志文件核心配置从properties到logback-spring.xml2.1 最省事的配置logging.file.name 与 logging.file.path如果你不想碰 XMLSpringBoot 提供了一组直接可用的配置项。在application.properties里加一行logging.file.name./logs/app.log启动项目后项目根目录下就会自动创建 logs 目录日志写入app.log。这里注意一点我用的是相对路径./logs/实际运行时会以“当前工作目录”为基准。如果你用java -jar app.jar启动当前目录就是执行这条命令的目录如果你在 IDEA 里启动当前目录通常是项目根目录。还有一个容易混淆的配置logging.file.path。从名字看像是“文件路径”实际上它表示“日志文件的目录”。只设置了logging.file.path/var/log/myappSpringBoot 就会在那个目录下生成一个固定名字的日志文件叫spring.log。logging.file.name和logging.file.path同时存在时logging.file.name优先级更高。我不建议两个都配容易把自己搞糊涂。最简单直接的方案就是只配logging.file.name精确到文件名。想按天滚动、控制文件大小就需要上 XML 了。2.2 深入logback-spring.xml滚动策略才是日志文件管理的核心logging.file.name虽然方便但它有一个致命短板所有日志都往同一个文件里写文件会无限变大。我见过一个运行了几个月的项目单个日志文件几个 GB服务器磁盘直接报警。解决这个问题需要用logback-spring.xml来精细控制滚动策略。滚动策略是什么可以理解为日志文件不是永远写同一个而是写满一定大小或者每隔一段时间就把当前文件重命名归档然后新建一个文件继续写。Logback 里最常用的两个策略TimeBasedRollingPolicy按时间滚动比如每天一个文件SizeAndTimeBasedRollingPolicy按时间和大小双重驱动每天生成一个文件但这个文件超过指定大小后再拆分成多个下面给一份我常用的配置直接可以抄?xml version1.0 encodingUTF-8? configuration property nameLOG_HOME value./logs/ property nameLOG_PATTERN value%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{40} - %msg%n/ appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_HOME}/app.log/file rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy fileNamePattern${LOG_HOME}/app.%d{yyyy-MM-dd}.%i.log.gz/fileNamePattern maxFileSize100MB/maxFileSize maxHistory15/maxHistory totalSizeCap5GB/totalSizeCap /rollingPolicy encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender root levelinfo appender-ref refCONSOLE/ appender-ref refFILE/ /root /configuration这份配置做了几件事日志文件先在./logs/app.log写满 100MB 后触发滚动滚动时生成带日期的归档文件比如app.2025-01-15.1.log.gz并且用 gzip 压缩最多保留 15 个历史文件超过 15 个自动删除最旧的所有历史文件总大小不能超过 5GB超了也会删旧文件maxHistory和totalSizeCap这两个参数很多人分不清。maxHistory管的是“按文件个数”totalSizeCap管的是“按总字节数”。两个可以同时生效哪个先触发就按哪个处理。我建议两个都配双保险防止某个参数估算失误把磁盘写爆。2.3 fileNamePattern 里的 %i 是什么意思app.%d{yyyy-MM-dd}.%i.log.gz这个格式里%i是索引值。同一天内如果第一个文件写满 100MB就会生成app.2025-01-15.0.log.gz第二个文件继续写满 100MB生成app.2025-01-15.1.log.gz。也就是说%i是同一时间段内多个文件的递增编号。理论上%d{yyyy-MM-dd}的最小粒度可以到毫秒但我建议按天就行最多按小时。粒度太细会导致生成大量小文件管理起来反而麻烦。另外所有归档文件的压缩操作是异步执行的如果日志量极大压缩可能跟不上滚动速度这种情况我会把压缩关掉改成纯文本归档性能优先。2.4 多环境配置dev和prod分别用不同的日志策略用logback-spring.xml而不是logback.xml就是为了支持 Spring Profile。SpringBoot 对前者提供了特殊的springProfile标签支持同一个文件里可以按环境激活不同的配置。我通常这样区分开发环境控制台输出完整文件不滚动或少量滚动生产环境只输出 WARN 以上到文件而且滚动策略收紧。springProfile namedev root leveldebug appender-ref refCONSOLE/ appender-ref refFILE/ /root /springProfile springProfile nameprod root levelwarn appender-ref refCONSOLE/ appender-ref refFILE/ /root /springProfile这里的name对应spring.profiles.active配置。如果你启动时带上--spring.profiles.activeprod那么 prod 环境规则生效。这套机制比在代码里判断环境再改 logger 级别要干净得多配置写在 XML 里一目了然。2.5 日志输出格式的细节与常见误区log的 pattern 看起来很复杂但每个字段都有实际用途。核心是时间、级别、线程名、Logger 名、消息体。我平时排查问题线程名和 Logger 名能帮我快速定位是哪个模块、哪个线程出了问题这比只看消息体高效得多。常用占位符%d时间可以指定格式如%d{yyyy-MM-dd HH:mm:ss.SSS}%-5level级别左对齐并占 5 个字符宽度%thread线程名%logger{40}类名长度超过 40 字符会被缩写%msg日志正文%n换行很多人踩过的一个坑日志正文本身包含换行和特殊字符。如果业务代码里打印了一个带换行的长字符串日志文件就会变得面目全非。建议把 pattern 里加上%replace(%msg){[\r\n], \\\\n}之类的清洗逻辑或者干脆在代码里规范日志内容。还有一个我踩过的坑给 pattern 设置字符编码。Logback 默认使用平台默认编码在 Windows 上跑出来的中文日志传到 Linux 服务器上查看就是乱码。一定要显式配置charsetUTF-8/charset保证跨平台一致。3. 日志文件查看、检索与线上问题排查实录3.1 日志文件的基础查看方式tail、grep、zgrep日志文件一旦生成怎么高效查看就是个问题。Windows 上可以装个 notepad、Sublime文件小无所谓文件大了编辑器会卡死。Linux 服务器上我的习惯是先用tail看最新日志# 实时跟踪最新日志 tail -f /var/log/myapp/app.log # 查看最后1000行 tail -n 1000 /var/log/myapp/app.log然后按关键字过滤grep是大杀器# 查找包含ERROR的日志行 grep ERROR app.log # 找某个订单号的全部日志 grep orderId123456 app.log | less # 统计错误次数 grep -c ERROR app.log # 查找错误日志以及前后的行 grep -A 5 -B 5 NullPointerException app.log日志文件被 gzip 压缩后grep直接搜不进去得用zgrepzgrep ERROR app.2025-01-15.0.log.gz这几个命令配合起来线上排查的速度会比打开文件肉眼找快一个数量级。3.2 排查压测下的性能瓶颈从日志文件里找线索有一次我做性能压测接口 QPS 上不去CPU 居高不下。我先看app.log发现有大量的 DEBUG 日志在滚动每次请求至少打印十几行 SQL 和参数。当时日志级别是 DEBUGLogback 光是格式化字符串、写文件就占了不少 CPU。把级别改回 INFO 后QPS 直接翻了一倍。这种问题在日志文件里非常直观同一条接口路径在日志中出现大量 DEBUG 记录而且每个请求对应十几条输出基本可以断定日志级别配低了。我用grep统计某个时间段内的日志量grep 2025-01-15 14:00 app.log | wc -l如果一分钟内日志量达到几十万行说明日志输出过于嘈杂该考虑降级别、加条件判断、或者用异步 Appender。3.3 利用MDC实现链路追踪让日志文件能串起一次请求微服务或复杂的单体应用里一个请求会经过 Controller、Service、Mapper 多层调用。如果日志文件只是零散地记录每一行排查问题时很难把同一请求的所有日志串起来。MDCMapped Diagnostic Context可以解决这个问题。SLF4J 提供了 MDC 机制相当于在每个线程的日志上下文里塞了一个变量日志格式里可以用%X{traceId}输出。我通常写一个过滤器在请求进入时生成一个traceId放入 MDC请求结束时移除Component public class TraceIdFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String traceId UUID.randomUUID().toString().replace(-, ); MDC.put(traceId, traceId); try { chain.doFilter(request, response); } finally { MDC.remove(traceId); } } }然后 logback 的 pattern 里加上[%X{traceId}]。这样同一请求的所有日志都有同一个 traceId排查时一句话grep traceId8f2a1c3e app.log整条请求链路就串起来了。这个方案成本极低收益极高强烈建议在项目里直接落地。3.4 线上日志文件的检索和清理实操服务器磁盘满了第一反应是不是日志文件太大先定位du -sh /var/log/myapp/*.log*如果发现某个日志文件几个 GB不要贸然rm掉。Linux 下如果服务进程仍持有该文件的句柄rm之后磁盘空间不会立即释放。正确做法是cat /dev/null app.log或truncate -s 0 app.log先清空文件内容再考虑删除归档文件。实际操作中我更推荐保留滚动策略让 Logback 自己管理文件生命周期。如果你用了SizeAndTimeBasedRollingPolicy只要maxHistory和totalSizeCap配得合理日志文件数量是自动收敛的很少需要人工清理。人工清理只用来处理突发情况比如有人把某条链路日志打成死循环或者日志级别被改成 DEBUG 后忘记改回来。3.5 日志文件查看的常见疑问dlt日志文件与其它格式有人问我“dlt日志文件怎么查看内容”这个其实是汽车电子领域的 DLTDiagnostic Log and Trace格式日志和 SpringBoot 日志文件不是一回事。SpringBoot 的 Logback 默认输出就是纯文本UTF-8 编码Linux 下cat和tail直接能看。如果你用的不是标准文本格式先确认是不是在配置里改了 encoder 的编码或输出格式如果确实输出成了二进制检查是不是引入了自定义 Appender。4. 日志文件常见问题与避坑技巧实录4.1 日志文件不生成从头排查的完整思路这个问题出现频率最高。排查的时候我建议按顺序查检查application.properties里是否配置了logging.file.name或logging.file.path。确认配置没有拼错。比如logging.file.name写成了logging.fileNameSpringBoot 根本不会识别。确认启动时加载的是哪个配置文件。多环境时很容易出现你改了application-prod.properties本地却用dev环境启动日志写到别的地方去了。检查项目里有没有自定义的logback.xml或logback-spring.xml。如果存在properties 里的logging.file.name会失效因为 XML 配置优先级更高。看控制台有没有报错。Logback 配置有问题时比如路径不存在且没有权限创建目录控制台会有异常信息。还有一个隐藏点如果你把logging.file.name配置成一个不存在的目录比如/var/log/myapp/app.log而当前用户没有创建/var/log/myapp的权限启动时日志文件就不会创建。这种情况下我会先把目录手动建好或者修改为项目可写的相对路径。4.2 日志文件中文乱码编码问题的三种解法中文日志乱码的原因九成是编码不一致。Logback 写入时用了 UTF-8而查看工具按 GBK 打开或者反过来。最稳的解法是在 encoder 里强制指定编码encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder同时在application.properties里也设置项目编码spring.mandatory-file-encodingUTF-8 server.tomcat.uri-encodingUTF-8Windows 上使用 IDEA 时还需要检查 IDEA 的 File Encoding 设置确保源码文件本身是 UTF-8 保存。有时候源码里的中文本来就是乱码那不管日志框架怎么配都救不回来。4.3 日志文件把磁盘撑爆两个关键参数必须设置前面已经提过只用logging.file.name不做滚动策略日志会无限增长。换成logback-spring.xml后maxHistory和totalSizeCap一定要设。我经历过一个真实事故一个服务接口内部循环打印日志每个请求打印 50 行一天下来单个日志文件长到 6GB。当时配置里没有totalSizeCapmaxHistory设的 30因为 30 天前还没有这么大的日志量归档文件还没触发删除。加上totalSizeCap限制后总大小一超就立刻触发清理策略磁盘空间再也不会失控。这两个参数建议根据服务量级一起调整。业务量大、日志多的服务maxHistory可以设小一点比如 7 天totalSizeCap设 2GB业务量小、需要长期留痕的maxHistory设 30totalSizeCap设 10GB。总之不要只设一个。4.4 异步日志的坑日志文件丢失与线程阻塞为了提升性能有人会给 File Appender 套上 AsyncAppender。异步日志的原理是业务线程把日志事件放进队列后台线程池从队列取出来写文件。这样业务线程不会阻塞在 IO 上QPS 会好看很多。但异步日志有两个最隐蔽的坑。第一个是队列容量。默认队列大小是 256如果日志产生速度远大于消费速度队列满了之后新的日志事件会被丢弃。这是个很让人头疼的问题你以为日志都写下来了实际上高峰期丢了不少关键信息。解决方法是调大队列或者配置丢弃策略为“保留 ERROR 级丢弃 INFO 级”。第二个坑是线程池不够。AsyncAppender 默认的线程池很小如果日志量大后台线程处理不过来也会造成积压和丢失。说实话现在的项目除非并发量极大、日志量极其恐怖我不太建议上异步日志。先用同步 Appender 配合滚动策略大多数场景已经足够。真到了性能瓶颈再考虑异步并且一定要加上监控防止日志静默丢失。4.5 日志文件里的敏感信息脱敏是小问题但往往被忽视日志文件是开发排查的神器同时也是信息泄露的重灾区。我见过有人把用户手机号、身份证号直接log.info打出来运维和测试都能在日志文件里看到这就是合规风险。有人觉得等出了问题再清理日志文件就行但归档文件早就备份走了。正确的做法是在源头控制不打印敏感字段或者用脱敏工具处理后再打印。写一个简单的脱敏工具方法对手机号、银行卡号做掩码处理在打印前调用成本极低效果明显。不要依赖日志框架做脱敏那种方案实现复杂、容易漏配。最可靠的还是在代码层面做好规范能不打就不打必须打就打脱敏后的值。4.6 日志文件的权限与多实例部署问题多实例部署时每台机器上都有独立的日志文件。有人会问能不能让所有实例写同一个文件强烈不建议。两个进程写同一个文件轻则日志相互穿插难以阅读重则文件锁冲突导致进程挂掉。正确做法是每台机器写本地日志采集端统一收集到日志中心再汇总检索。另外日志文件所在目录的权限也要注意。如果服务以低权限用户启动而日志目录属于 root启动时会出现 Permission denied 错误日志文件自然无法生成。这个问题排查起来其实不难但很多人在代码和配置里反复找半天忘了看系统层面最简单的问题。写在最后日志文件这件事值得一开始就做好日志文件平时不起眼但它就是你线上排查时唯一的“黑匣子”。我个人这些年最大的体会就是日志配置这种东西一定不要等项目上线了、出了问题再补。等到线上出故障时你会发现日志文件不存在、级别不够、滚动策略没配好那一刻的心情只能用“后悔”来形容。建议你新建项目时就直接把logback-spring.xml放进资源目录把滚动策略、编码、级别、MDC 链路追踪全配好。这套动作花不了多少时间但能让你后续排查问题的效率高一个档次。最后再分享一个小技巧SpringBoot 的日志配置不一定要重启才生效。用 Actuator 的/actuator/loggers端点可以在运行时动态调级别。比如先把某个 Mapper 包调成 DEBUG跑几个请求再调回 INFO全程不用重启服务非常顺手。把这个接口用起来日志文件在你手里就是一把真正的“手术刀”而不是一个只会吃磁盘的“硬盘杀手”。