做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 就不输出;设成 DEBUG,INFO 及以上的都输出。
SpringBoot 的默认日志级别是 INFO。也就是说,默认情况下 TRACE 和 DEBUG 不会出现在日志文件里。这一点经常有人忽略,线上排查问题时想看 SQL 参数、看请求头细节,发现日志里没有,其实是级别没放开。
想调级别有两种方式。一种是临时改,通过 Spring Boot Actuator 的/actuator/loggers端点,POST 请求就能动态调整某个类的日志级别,不用重启服务。另一种是永久改,在application.properties里写:
logging.level.com.example.mapper=debug logging.level.root=info上面这段表示com.example.mapper包下的所有类按 DEBUG 级别输出,其他包按默认 INFO。实际开发里,排查问题时我经常先把 MyBatis Mapper 包的日志级别临时调到 DEBUG,就能看到完整的 SQL 语句和参数绑定过程,查完再调回去。
这里有一个值得说的点:日志级别不是“配置了就会真的全部打印”。Logback 内部还有一个判断逻辑,如果某个 Logger 没有显式配置级别,会往上层 Logger 找,一直找到 root。所以你在 properties 里写的logging.level.root=info,是兜底的规则,某个包单独设置了级别,就优先用包的级别。
1.3 为什么控制台有日志,但磁盘上没有文件
这是新人最常见的问题之一。SpringBoot 默认的spring-boot-starter-logging确实帮你配好了 Logback,但默认配置里只有CONSOLE输出,没有FILE输出。也就是说,你第一眼能看到控制台密密麻麻的日志,但这些日志不会主动落盘,磁盘上不会自己冒出个 log 文件。
想要生成日志文件,必须在配置里告诉 SpringBoot 两件事:文件放哪、文件名是什么。这就是下一节要讲的核心内容。我见过不少人折腾半天“日志文件不生成”,其实原因就是根本没配logging.file.name或logging.file.path,SpringBoot 不知道你想写文件。
2. 日志文件核心配置:从properties到logback-spring.xml
2.1 最省事的配置:logging.file.name 与 logging.file.path
如果你不想碰 XML,SpringBoot 提供了一组直接可用的配置项。在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/myapp,SpringBoot 就会在那个目录下生成一个固定名字的日志文件,叫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 version="1.0" encoding="UTF-8"?> <configuration> <property name="LOG_HOME" value="./logs"/> <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{40} - %msg%n"/> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_HOME}/app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${LOG_HOME}/app.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> <maxFileSize>100MB</maxFileSize> <maxHistory>15</maxHistory> <totalSizeCap>5GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <root level="info"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> </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 name="dev"> <root level="debug"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> </root> </springProfile> <springProfile name="prod"> <root level="warn"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> </root> </springProfile>这里的name对应spring.profiles.active配置。如果你启动时带上--spring.profiles.active=prod,那么 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 服务器上查看就是乱码。一定要显式配置<charset>UTF-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 "orderId=123456" app.log | less # 统计错误次数 grep -c "ERROR" app.log # 查找错误日志以及前后的行 grep -A 5 -B 5 "NullPointerException" app.log日志文件被 gzip 压缩后,grep直接搜不进去,得用zgrep:
zgrep "ERROR" app.2025-01-15.0.log.gz这几个命令配合起来,线上排查的速度会比打开文件肉眼找快一个数量级。
3.2 排查压测下的性能瓶颈:从日志文件里找线索
有一次我做性能压测,接口 QPS 上不去,CPU 居高不下。我先看app.log,发现有大量的 DEBUG 日志在滚动,每次请求至少打印十几行 SQL 和参数。当时日志级别是 DEBUG,Logback 光是格式化字符串、写文件就占了不少 CPU。把级别改回 INFO 后,QPS 直接翻了一倍。
这种问题在日志文件里非常直观:同一条接口路径在日志中出现大量 DEBUG 记录,而且每个请求对应十几条输出,基本可以断定日志级别配低了。我用grep统计某个时间段内的日志量:
grep "2025-01-15 14:00" app.log | wc -l如果一分钟内日志量达到几十万行,说明日志输出过于嘈杂,该考虑降级别、加条件判断、或者用异步 Appender。
3.3 利用MDC实现链路追踪:让日志文件能串起一次请求
微服务或复杂的单体应用里,一个请求会经过 Controller、Service、Mapper 多层调用。如果日志文件只是零散地记录每一行,排查问题时很难把同一请求的所有日志串起来。MDC(Mapped 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 "traceId=8f2a1c3e" 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日志文件怎么查看内容”,这个其实是汽车电子领域的 DLT(Diagnostic 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.fileName,SpringBoot 根本不会识别。 - 确认启动时加载的是哪个配置文件。多环境时,很容易出现你改了
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> <charset>UTF-8</charset> </encoder>同时,在application.properties里也设置项目编码:
spring.mandatory-file-encoding=UTF-8 server.tomcat.uri-encoding=UTF-8Windows 上使用 IDEA 时,还需要检查 IDEA 的 File Encoding 设置,确保源码文件本身是 UTF-8 保存。有时候源码里的中文本来就是乱码,那不管日志框架怎么配都救不回来。
4.3 日志文件把磁盘撑爆:两个关键参数必须设置
前面已经提过,只用logging.file.name不做滚动策略,日志会无限增长。换成logback-spring.xml后,maxHistory和totalSizeCap一定要设。
我经历过一个真实事故:一个服务接口内部循环打印日志,每个请求打印 50 行,一天下来单个日志文件长到 6GB。当时配置里没有totalSizeCap,maxHistory设的 30,因为 30 天前还没有这么大的日志量,归档文件还没触发删除。加上totalSizeCap限制后,总大小一超就立刻触发清理策略,磁盘空间再也不会失控。
这两个参数建议根据服务量级一起调整。业务量大、日志多的服务,maxHistory可以设小一点,比如 7 天,totalSizeCap设 2GB;业务量小、需要长期留痕的,maxHistory设 30,totalSizeCap设 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,全程不用重启服务,非常顺手。把这个接口用起来,日志文件在你手里就是一把真正的“手术刀”,而不是一个只会吃磁盘的“硬盘杀手”。