恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Spring Boot日志配置:logback-spring.xml异步与滚动
首页
资讯中心
/
Spring Boot日志配置:logback-spring.xml异步与滚动
Spring Boot日志配置:logback-spring.xml异步与滚动
发布时间:2026/10/1 14:03:28
接手一个陌生项目我有个习惯先看resources目录下有没有logback-spring.xml再看它把日志写到了哪里。原因很朴素——线上出问题的时候代码是死的日志是活的。日志配置写得糙排查故障基本等于蒙着眼睛摸象配得清楚很多时候扫一眼日志就知道是哪段逻辑出了岔子。这篇文章就把我这些年反复验证过的一份logback-spring.xml配置完整摊开来讲包括每个标签为什么这么写、参数怎么算出来的、哪些写法看着没问题其实是坑最后给一份改掉包名和路径就能直接跑的完整文件。刚接触 Spring Boot 的人可以照着抄想把老项目日志规范化的人也能拿它当参照。1. 先把 logback-spring.xml 的加载规则搞明白很多人配日志失败不是内容写错了而是根本没搞清楚这个文件什么时候被读、被谁读。先把这层规则捋清楚后面写配置才不会被明明改了却没生效这种问题反复折腾。1.1 文件名的那个 -spring 后缀不是装饰品Spring Boot 支持两种日志配置文件logback.xml和logback-spring.xml。前者的名字是 logback 官方约定的Spring Boot 还没启动完成时logback 自己就会去 classpath 根目录找它并解析后者的名字是 Spring Boot 约定的由 Spring Boot 的日志系统在初始化阶段接管解析。这个差别听起来很学术实际影响却非常直接logback 原生解析器不认识springProfile和springProperty这两个标签如果你把它们写进logback.xml解析时会报 no applicable action for [springProfile] 之类的错误然后整块配置被跳过。所以只要你想用 profile 分环境、想在配置文件里引用application.yml中的属性就必须用logback-spring.xml这个名字。反过来如果项目里有历史原因必须保留一个纯净的、不依赖 Spring 的日志配置比如某些独立工具类在 Spring 上下文之外运行那就用logback.xml两者分工明确。最怕的是两个文件同时躺在resources目录下这时候到底谁生效要看加载顺序行为会变得非常难以预测。我的做法是一个项目里只留一个想分环境就用 profile绝对不给自己埋雷。1.2 和其他配置项的优先级关系日志最终生效的级别往往不是由 XML 单独决定的。Spring Boot 还会读取application.yml里的logging.level.*、logging.file.name、logging.pattern.console这些键它们是程序化配置执行时机在 XML 解析之后。这就解释了一个经典困惑XML 里把某个包的 logger 写成DEBUGyml 里把同一个包写成WARN最终看到的是WARN。不是 XML 没生效而是被覆盖了。我的处理原则很明确日志的格式、appender、滚动策略放在 XML 里管日志级别的临时调整放在 yml 或者配置中心里管两边的职责不要重叠。生产环境要临时把某个包的日志级别调低排查问题改 yml 重启或者用 Actuator 的loggers端点动态改级别都比改 XML 更合适。顺手记一下动态改级别的调用方式临时排查时非常好用# 查看某个 logger 当前级别 curl -X GET http://localhost:8080/actuator/loggers/com.example.mapper # 临时改成 DEBUG curl -X POST http://localhost:8080/actuator/loggers/com.example.mapper \ -H Content-Type: application/json \ -d {configuredLevel:DEBUG}需要提醒的是这个端点要显式暴露才能用在 yml 里加management.endpoints.web.exposure.include: loggers。它能动态改级别但改不了 appender 和 pattern所以格式类的东西还是得老老实实在 XML 里定。1.3 一个配置文件里的四块结构不管配置多复杂logback-spring.xml拆开看就四块变量定义区、appender 定义区、logger 与 root 定义区、profile 环境区。变量区用property和springProperty声明路径、格式串这类会重复出现的东西appender 区定义日志往哪里写、怎么写、怎么滚动logger 区决定哪条日志交给哪个 appenderprofile 区负责在不同环境下切换这两者的组合。顺序上有个小细节appender必须定义在引用它的root或logger之前因为 logback 是按顺序解析的引用一个还没定义的名字会直接报 appender not found。这不是什么高深规则但我在代码评审里见过不止一次有人把 appender 写在 root 后面然后对着报错找半天。养成变量 → appender → logger → profile的固定顺序这类低级错误就绝迹了。2. 一份配置的四层结构逐块拆解下面这份配置我用了很久从单体应用到微服务都跑过结构上没有大改。我按块拆开讲每块都说明为什么这么选你在自己项目里做取舍时心里有数。2.1 变量区把会变的东西全部抽出来?xml version1.0 encodingUTF-8? configuration scanfalse !-- 应用名从 application.yml 的 spring.application.name 取 -- springProperty scopecontext nameAPP_NAME sourcespring.application.name defaultValueapplication/ !-- 日志根目录允许通过 JVM 参数 -DLOG_HOME/data/logs 覆盖 -- property nameLOG_HOME value${LOG_HOME:-./logs}/ !-- 统一格式串 -- property nameLOG_PATTERN value%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-}] %logger{40} - %msg%n/ /configurationspringProperty是从 Spring 的 Environment 里取值等价于读取 yml 里的配置项property是 logback 自己的变量。两者都可以用${}引用区别在于作用域和来源。${LOG_HOME:-./logs}这种写法里的:-是默认值语法意思是如果没有名为 LOG_HOME 的属性就用./logs。这个语法非常关键容器化部署时尤其有用默认写相对路径运维只需要加一个-DLOG_HOME/data/logs的 JVM 参数就能把日志挪到挂载卷上不用改代码。scanfalse是我强制要求写上的。scantrue会让 logback 定期重新读取配置文件看起来是个好功能但和springProfile放在一起有风险——重新加载走的是另一条解析路径实测中会出现 Spring 扩展标签解析不了、配置被整块忽略的情况。需要动态调级别就用前面说的 Actuator 端点不要靠 scan。2.2 控制台 appender只在终端里花哨appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr([%thread]){magenta} %clr(%-5level) %clr([%X{traceId:-}]){cyan} %clr(%logger{40}){cyan} - %msg%n/pattern charsetUTF-8/charset /encoder /appender%clr(...)是 Spring Boot 提供的着色转换词配合{faint}、{magenta}这类参数给不同字段上色。它只在 Spring Boot 环境下可用因为 Boot 在初始化日志系统时注册了这些转换规则。有人从别的项目复制了带%clr的 pattern 到非 Spring Boot 工程里结果日志里直接输出%clr字面量就是这个原因。格式串里的字段我按重要性排了序时间戳精确到毫秒线程名用来判断是不是异步线程、有没有线程安全问题级别左对齐 5 位保证竖向对齐[%X{traceId:-}]是 MDC 里的链路 ID取不到就打印空%logger{40}是 logger 名缩写到 40 个字符超长会自动从包名中间截断最后%n是跨平台的换行符——用\n在 Windows 上会出问题养成用%n的习惯。%clr千万不要用在写文件的 pattern 里。颜色是 ANSI 转义序列落到文件里就是一堆[32m之类的乱码用 grep 查日志的时候会非常难受。2.3 文件 appender滚动策略是重点appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_HOME}/${APP_NAME}.log/file rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy fileNamePattern${LOG_HOME}/archive/${APP_NAME}-%d{yyyy-MM-dd}.%i.log.gz/fileNamePattern maxFileSize100MB/maxFileSize maxHistory15/maxHistory totalSizeCap5GB/totalSizeCap cleanHistoryOnStartfalse/cleanHistoryOnStart /rollingPolicy encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder immediateFlushfalse/immediateFlush /appender先说file和fileNamePattern的关系这是最容易搞混的地方。file是当前正在写的活动文件名永远就一个fileNamePattern是被滚动归档后的历史文件名模板。归档名里必须同时包含%d和%i前者是日期后者是同一天内的分片序号因为一天可能产生多个文件。后缀加.gz表示自动 gzip 压缩文本日志的压缩比通常在 10:1 到 20:1 之间磁盘能省下不少代价是查历史日志要先解压用zcat或者zgrep直接读倒也不麻烦。immediateFlushfalse是性能考量。默认每次写日志都刷盘QPS 高的服务里这部分开销不小关掉之后由操作系统缓冲区决定何时落盘吞吐会好一些。代价是进程被kill -9时缓冲区里的日志可能丢。我的判断标准是普通业务服务关掉涉及资金、审计、状态机的核心链路保持开启。这个取舍必须基于业务自己判断没有通用答案。2.4 异步 appender别让它成为丢日志的元凶appender nameASYNC_FILE classch.qos.logback.classic.AsyncAppender appender-ref refFILE/ queueSize1024/queueSize discardingThreshold0/discardingThreshold includeCallerDatafalse/includeCallerData maxFlushTime3000/maxFlushTime /appenderAsyncAppender是包装器它本身不写文件只是把日志事件塞进一个内存队列交给独立的 worker 线程去调用实际 appender。业务线程打日志的开销就变成一次入队性能提升在高并发场景下非常明显。queueSize默认只有 256稍微有点并发就容易满。我一般调到 1024 或 2048具体看服务峰值 QPS 和单条日志的平均耗时。discardingThreshold默认是队列容量的 20%意思是队列剩余空间低于 20% 时TRACE、DEBUG、INFO这类低级别日志会被直接丢掉WARN和ERROR不受影响。这个默认值很容易造成日志时有时无的诡异现象所以我直接设成 0等于关闭基于剩余容量的丢弃。但要清楚设成 0 只解决了队列没满也丢的问题队列真正写满时仍然会丢。logback 1.3 之后提供了neverBlocktrue队列满时改为阻塞业务线程而不是丢弃代价是业务会卡在打日志这一步。选哪个取决于你的场景日志绝对不能丢就阻塞宁可慢一点也不丢日志量大、丢失可容忍就用默认丢弃策略。这个决定要在压测阶段验证别等到生产出问题才发现日志缺片。includeCallerData控制是否输出打日志的类名和行号。默认false是为了性能因为获取调用栈开销不小而且是发生在业务线程上的。如果你在 pattern 里写了%L、%M却不打开这个开关打出来的永远是问号或者错误位置。我的习惯是不打印行号靠%logger{40}定位到类就够了真需要行号的时候本地开一下。3. 按包打印 SQL 与日志级别的精细控制日志级别配得好不好直接决定你能不能在几百兆的日志里找到想看的那几行。这一块我踩过的坑最多值得单独讲。3.1 root 与 logger 的继承关系logback 的 logger 是有层级结构的com.example.service的父级是com.example再往上是com最顶层是root。查找规则是这样的为一个 logger 打日志时如果它自己没有显式配置 level就往上找最近的、有显式 level 的祖先用那个级别appender 则是沿着链路累积的子 logger 打完日志后会继续往上传递到祖先的 appender除非某一层设置了additivityfalse把传递链路掐断。这个机制解释了两个常见现象。第一为什么只配了 root 的 appender所有包都能打出来因为 appender 是累积的。第二为什么WARN和ERROR在子 logger 设了INFO之后还是能打出来因为级别是大于等于的关系INFO意味着高于等于它的所有级别都放行。所以配置里最稳的写法是root只设一个保底级别和公共 appender包级别的特殊需求单独用logger覆盖。反过来说如果你在logger里也加了 appender 却忘了写additivityfalse同一条日志会被 root 和 logger 各写一遍——这就是日志重复打印最常见的原因下一小节细讲。3.2 打印 MyBatis SQL 的正确姿势想让控制台看到 SQL很多人第一反应是把 root 改成DEBUG结果整个应用框架的日志全涌出来几百行里夹着一条 SQL。正确做法是只把 Mapper 接口所在包设成DEBUGlogger namecom.example.project.mapper levelDEBUG/原因是 MyBatis 在执行时是拿 Mapper 接口的全限定名作为 logger 名去打印 SQL 的所以只要把 Mapper 接口的包路径配成 DEBUG就能精确命中。MyBatis-Plus 用的是自己的日志实现如果包路径配置没效果可以试着把com.baomidou这个包设成DEBUG看看原理是一样的。有个细节要注意DEBUG级别打出来的 SQL 是参数拼好之后的完整语句参数直接内联在 SQL 里而不是用?占位。这在排查问题时很方便但也意味着敏感参数会明文落盘。涉及手机号、证件号、金额的表我建议在生产环境不要开这个级别或者配合自定义的 SQL 日志拦截器做脱敏。另外别忘了前面说的覆盖问题如果application.yml里同时写了logging.level.com.example.project.mapper: INFOXML 里的 DEBUG 会被覆盖掉表现就是配了没用。两边选一边配就行。3.3 把第三方框架的噪音压下去框架自身的日志量往往比业务日志还大。常见的几个噪音源和我的处理方式logger 名常见噪音建议级别org.apache.ibatisSQL 执行过程、结果集映射细节WARNorg.springframework启动阶段的 bean 加载细节INFOorg.apache.kafka消费者组心跳、分区分配WARNio.netty连接建立与释放WARNorg.apache.http连接池、请求头WARNcom.zaxxer.hikari连接池启动与回收INFOlogger nameorg.apache.ibatis levelWARN/ logger nameio.netty levelWARN/ logger nameorg.apache.kafka levelWARN/ logger namecom.zaxxer.hikari levelINFO/级别压到WARN而不是ERROR是有讲究的。压到ERROR意味着框架遇到一些可恢复的异常时你完全看不到线索等到真出事的时候日志里连个前后文都没有。WARN是个相对安全的界线它保留了异常和告警同时过滤掉海量的调试过程。这个原则我管它叫压噪音不压线索。3.4 用 springProfile 做环境切换!-- 非生产环境控制台 文件Mapper 开 DEBUG -- springProfile name!prod logger namecom.example.project.mapper levelDEBUG/ root levelINFO appender-ref refCONSOLE/ appender-ref refASYNC_FILE/ /root /springProfile !-- 生产环境只写文件Mapper 关掉 DEBUG -- springProfile nameprod root levelINFO appender-ref refASYNC_FILE/ appender-ref refERROR_FILE/ /root /springProfilename!prod是取反写法表示激活的 profile 里不含 prod。用它做兜底比列举 profile 名稳妥得多——万一哪天有人加了个stagingprofile!prod会自动命中不会出现所有 springProfile 块都不匹配、root 完全没配置的情况。那种情况下 logback 会告警并且不输出任何日志排查起来非常迷惑。需要明确的是logback 里只允许存在一个root元素。所以千万不能在 springProfile 外面再写一个 root 做默认值那样 prod 环境下就会出现两个 root解析直接报错。正确做法就是像上面这样用互斥且覆盖全部情况的 profile 条件把 root 分开。较新版本的 Spring Boot 还支持namedev | test这种表达式写法版本较老的项目老实地拆成多个块更保险。3.5 日志重复打印的三种成因日志重复是高频问题我按出现频率排一下第一种logger里加了 appender 但没写additivityfalse。日志先由 logger 自己的 appender 输出一遍再向上传递给 root 的 appender 输出第二遍。修法就是补上additivityfalse。第二种同一个日志被多个 appender 覆盖。比如你配了一个FILE和一个ERROR_FILE后者用LevelFilter只收 ERROR但前者没加任何过滤于是 ERROR 日志在两个文件里各写一份。这在文件层面是合理的错误日志单独归档方便快速定位但如果两个 appender 都往控制台输出就会看到双份。判断方法是看重复的两行是否格式完全一致——一致说明是 appender 层面的问题。第三种代码里自己加了ConsoleAppender或者用了LoggerFactory.getLogger拿到不同的 LoggerContext。这种情况在引入多个日志门面桥接包时容易出现比如同时引入了slf4j-log4j12和logback-classic日志会由两套系统各打一遍。典型症状是控制台能看到两种完全不同的格式一眼就能认出来。处理方式是mvn dependency:tree查日志相关的依赖把多余的桥接包排掉。4. 日志磁盘占用怎么算滚动参数拆解日志把磁盘写满导致服务不可用这种事我亲身经历过两次。参数不是随便填几个数字就完事得算。4.1 三个参数的真实含义maxFileSize是单个活动文件的大小上限超过就切分片。它决定的是一个文件有多大不影响总量。maxHistory是保留多少个时间周期。注意这里的单位跟着fileNamePattern的%d精度走pattern 里是%d{yyyy-MM-dd}那 15 就是保留 15 天如果写成%d{yyyy-MM}15 就是保留 15 个月。这个细节很多人忽略把按天改成按月之后忘了调整数字一下多留了 30 倍。totalSizeCap是所有归档文件的总大小上限超过后从最老的文件开始删。这个参数才是真正的磁盘保险丝但它有个前提必须和maxHistory一起用而且它只在滚动触发的时刻做检查不是实时监控。也就是说如果某天日志量突然翻十倍在当天第一次滚动发生之前磁盘占用是可以突破这个上限的。4.2 一个真实的容量推算假设一个中等规模的服务日均产生日志 2GB我们想保留 15 天同时磁盘上最多占 5GB。参数这么配的maxFileSize100MB/maxFileSize maxHistory15/maxHistory totalSizeCap5GB/totalSizeCap推算一下单日 2GB ÷ 100MB 20 个分片15 天就是 300 个分片理论总量 30GB。但totalSizeCap是 5GB实际只能留下约 2.5 天的日志maxHistory的 15 天根本轮不到生效。这时候要问自己一个问题真的要保留 15 天吗如果只是为了应对突发排查2.5 天已经足够那就把口径统一成 5GB别让数字互相打架如果合规上必须留 15 天那就得把 5GB 提到 32GB 以上并确保挂载卷有空间。最后别忘了压缩。如果 pattern 里加了.gz实际占用大约是上面算出来的 1/10 到 1/20。也就是说 30GB 的原始日志压缩后可能只有 2GB 左右。我的建议是归档一定要加.gz这是性价比最高的一次优化几乎没有代价。4.3 归档路径和清理的注意点归档路径我建议单独建一个子目录比如${LOG_HOME}/archive/。原因是运维做巡检、写清理脚本、配监控时按目录匹配要比按文件名匹配可靠得多。活动文件在根目录归档文件在archive子目录一眼就能区分。cleanHistoryOnStart默认是false意思是应用启动时不去清理历史文件清理只在滚动时发生。如果你的服务是频繁重启的短生命周期任务可以设成true每次启动顺手清一遍旧文件避免长期积累。但要注意它是在启动阶段同步执行的历史文件特别多时会拖慢启动这时候设回false更好。还有一点容易忽略多个实例共享同一个挂载目录时绝对不能让它们写同一个活动文件。两个进程同时往一个文件里写滚动的时候互相删对方的文件最终结果是一堆乱码和丢日志。容器环境下的对策是每个实例写自己的子目录或者直接用 stdout 输出交给容器运行时收集具体选哪种取决于你们的日志采集链路。5. 高频问题速查与排查路径下面这些是我被问得最多的问题按症状 → 排查 → 解决整理配速查表方便对照。5.1 配置完全没生效怎么一步步定位第一步确认文件名。必须是logback-spring.xml放在src/main/resources根目录下target/classes里也能看到。文件名少个字母、放在子目录里都不会被加载。第二步确认没有第二个配置文件抢场。搜一下logback.xml、logback-test.xml有的话删掉。第三步打开 logback 自己的调试输出。在configuration上临时加debugtrue启动时控制台会打印每一个 appender 的创建过程和 logger 的级别赋值过程非常直观。排查完记得删掉这东西的输出量很大。第四步检查是不是被 yml 覆盖了。前面说过logging.level.*的优先级高于 XML出现XML 里是 DEBUG实际是 INFO的情况八成是这个原因。5.2 中文乱码与控制台颜色异常文件里出现乱码基本是编码问题。encoder 上加charsetUTF-8/charset同时确保 IDE 的文件编码、Maven 的project.build.sourceEncoding都是 UTF-8。这三个地方有一个不一致就会出乱码。Windows 控制台里中文显示成方块是因为控制台默认代码页是 GBK而日志按 UTF-8 输出。临时解决可以执行chcp 65001切到 UTF-8但更建议直接换用支持 UTF-8 的终端。至于日志文件里出现[32m、[1;31m这类奇怪字符说明你把带%clr的 pattern 用在了文件 appender 上把着色去掉就好。5.3 异步日志丢数据与关闭时的处理丢日志分两种运行中丢和停机时丢。运行中丢看queueSize是否太小、discardingThreshold是否在使用默认值参考 2.4 节的调整思路。停机时丢是因为异步 worker 线程可能还没把队列消化完JVM 就开始关停了。maxFlushTime就是给这个场景准备的单位毫秒表示 shutdown 时最多等多久让队列排空。默认 1000 毫秒日志量大或者某次写入卡顿时不够用我一般给到 3000 到 5000。但这只是缓解不是保证——它到点就走。对日志完整性要求极高的场景关键链路的那部分日志建议走同步 appender异步只留给普通的业务流水日志。5.4 问题速查表症状最可能的原因处理方式修改后完全没反应文件名不对或存在竞争配置文件改为 logback-spring.xml删除 logback.xml报 no applicable action for [springProfile]文件名是 logback.xml改名或去掉 Spring 扩展标签日志重复打印additivity 未关闭 / 多 appender 覆盖加 additivityfalse检查 appender 引用级别改了不生效被 application.yml 覆盖两边只留一处配置日志文件时有时无异步队列丢弃低级别日志discardingThreshold 设为 0日志里没有行号includeCallerData 为 false打开开关或改用 logger 名定位磁盘被写满totalSizeCap 未配或过大设置总量上限归档加 .gz历史日志清理不掉maxHistory 单位与 %d 精度不匹配对齐 pattern 精度与保留数量中文乱码编码不统一encoder 与构建编码统一为 UTF-8停机丢最后几条异步队列未排空调大 maxFlushTime关键日志走同步6. 生产环境里我额外加的几样东西前面是骨架这一节是我在生产环境里一定会补上的部分属于配了不显眼不配会难受的那类东西。6.1 traceId 的正确接入方式分布式环境里一次请求跨多个服务日志没有链路 ID 基本没法串。做法是在入口过滤器里往 MDC 塞一个 IDComponent public class TraceIdFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { String traceId UUID.randomUUID().toString().replace(-, ).substring(0, 16); MDC.put(traceId, traceId); try { chain.doFilter(req, resp); } finally { MDC.clear(); } } }然后 pattern 里用%X{traceId:-}取出来。:-后面的部分是取不到值时的默认值写成空表示取不到就不打印。这里有个非常关键的细节MDC 底层是 ThreadLocal异步线程里读不到主线程的值。但AsyncAppender 不受这个限制因为 logback 在构造日志事件时就把 MDC 的内容快照进去了。真正有问题的是业务代码里自己用线程池起的任务——那些线程里打日志traceId 会是空的。解决办法是用MDC.getCopyOfContextMap()手动传递或者用 Spring 提供的TaskDecorator统一包装线程池。这个问题我在微服务项目里遇到过一次排查了半天才发现是线程池的锅后来把它写进了团队的代码规范。6.2 别把大对象直接塞进日志我见过有人图省事把整个 List 或者完整响应体log.info(result: {}, result)打出来。测试环境数据少没事生产环境一个分页查询几千条记录一行日志几十 KB日志量瞬间爆炸磁盘、采集、存储全线告急。我的经验规则是集合只打元素个数对象只打关键标识字段。比如log.info(查询完成userId{}, size{}, userId, list.size())。真需要看完整内容的时候用log.debug并配合条件判断或者只在异常分支里打。这一条说起来简单但它在日志治理里的收益比调任何参数都大。6.3 完整的整合版配置把前面所有内容拼起来就是下面这份。改掉包名和路径可以直接用非生产环境看 SQL、生产环境只落盘、异步写入、错误单独归档、总量有上限几个核心诉求都覆盖了?xml version1.0 encodingUTF-8? configuration scanfalse springProperty scopecontext nameAPP_NAME sourcespring.application.name defaultValueapplication/ property nameLOG_HOME value${LOG_HOME:-./logs}/ property nameLOG_PATTERN value%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId:-}] %logger{40} - %msg%n/ !-- 控制台 -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr([%thread]){magenta} %clr(%-5level) %clr([%X{traceId:-}]){cyan} %clr(%logger{40}){cyan} - %msg%n/pattern charsetUTF-8/charset /encoder /appender !-- 全量日志文件 -- appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_HOME}/${APP_NAME}.log/file rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy fileNamePattern${LOG_HOME}/archive/${APP_NAME}-%d{yyyy-MM-dd}.%i.log.gz/fileNamePattern maxFileSize100MB/maxFileSize maxHistory15/maxHistory totalSizeCap5GB/totalSizeCap cleanHistoryOnStartfalse/cleanHistoryOnStart /rollingPolicy encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder immediateFlushfalse/immediateFlush /appender !-- 只收 ERROR -- appender nameERROR_FILE 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}/archive/${APP_NAME}-error-%d{yyyy-MM-dd}.%i.log.gz/fileNamePattern maxFileSize50MB/maxFileSize maxHistory30/maxHistory totalSizeCap2GB/totalSizeCap /rollingPolicy encoder pattern${LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 异步包装 -- appender nameASYNC_FILE classch.qos.logback.classic.AsyncAppender appender-ref refFILE/ queueSize1024/queueSize discardingThreshold0/discardingThreshold includeCallerDatafalse/includeCallerData maxFlushTime3000/maxFlushTime /appender !-- 第三方噪音压制 -- logger nameorg.apache.ibatis levelWARN/ logger nameio.netty levelWARN/ logger nameorg.apache.kafka levelWARN/ logger namecom.zaxxer.hikari levelINFO/ !-- 非生产控制台 文件看 SQL -- springProfile name!prod logger namecom.example.project.mapper levelDEBUG/ root levelINFO appender-ref refCONSOLE/ appender-ref refASYNC_FILE/ /root /springProfile !-- 生产只落盘 错误单独归集 -- springProfile nameprod root levelINFO appender-ref refASYNC_FILE/ appender-ref refERROR_FILE/ /root /springProfile /configuration关于这份配置我还想补最后一点日志这件事配好之后的收益是隐性的出问题时才知道它值多少钱。我个人的做法是把这段配置抽成公司内部的模板新项目直接复制只改com.example.project.mapper这一行包名其他参数由运维根据挂载卷的容量统一调整。这样既不会每个项目各写一套风格迥异的格式也避免了有人在生产环境忘了关 DEBUG 把 SQL 明文刷到磁盘上。真正要动的只有两处LOG_HOME通过启动参数注入profile 通过启动参数指定剩下的一律不动。