代码环境
- 仓库:https://github.com/springvortex/spring-cloud-alibaba.git
- 分支:
release/v1.0.0- JDK:
21- Spring Boot:
4.1.1
日志先要能被找到,然后才谈得上分析。这次把四个服务的 Logback 滚动文件名统一成“应用名 + 级别 + 日期”的格式。
应用名来自 Spring 属性:
<springProperty name="application"
scope="context"
source="spring.application.name"
defaultValue="app"/>
<property name="LOG_HOME" value="logs/${application}"/>
各级别文件都使用同一命名规则:
<fileNamePattern>
${LOG_HOME}/info/${application}-info-%d{yyyy-MM-dd}.%i.log
</fileNamePattern>
<maxFileSize>1MB</maxFileSize>
<maxHistory>180</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
以前的问题是格式不统一:DEBUG 文件里有构建时生成的 logPrefix 和小时分钟,INFO、WARN、ERROR 又叫 log-info、log-warn、log-error。排查跨服务问题时,文件名本身无法说明来源。调整后是:
service-provider-info-2026-08-25.0.log
service-gateway-error-2026-08-25.0.log
service-mail-warn-2026-08-25.0.log
日期只保留到天,同一天超过 1MB 时由 %i 递增序号。相比把小时分钟放进文件名,这种结构更适合 SizeAndTimeBasedRollingPolicy,也让按天检索保持稳定。
在 Docker 里,Compose 已把 /app/logs/<application> 挂到宿主机的 logs/provider、logs/gateway 等目录:
- ./logs/provider:/app/logs/service-provider
https://github.com/springvortex/spring-cloud-alibaba/blob/release/v1.0.0/deploy/docker-compose.yml
所以生产排查既可以用宿主机文件,也可以用 docker compose logs 看 stdout/stderr。要注意多实例场景:当前文件名按服务名区分,不按容器实例区分,长期横向扩容前应把容器 HOSTNAME 加入日志路径或文件名,避免多个 JVM 写同一组文件。
完整配置
四个可运行服务的 logback-spring.xml 结构一致,下面贴 Provider 的完整配置;Gateway、Consumer、Mail 会通过各自的 spring.application.name 得到自己的日志目录和文件名前缀。
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds" debug="false">
<springProperty name="application" scope="context" source="spring.application.name" defaultValue="app"/>
<springProperty name="port" scope="context" source="server.port" defaultValue="0"/>
<contextName>logback</contextName>
<property name="LOG_HOME" value="logs/${application}"/>
<property name="CONSOLE_LOG_PATTERN"
value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [traceId=%X{traceId:-},spanId=%X{spanId:-}] [%highlight(%-5level)] [${application}:${port}] [%cyan(%logger{50})] [%msg]%n"/>
<property name="FILE_LOG_PATTERN"
value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [traceId=%X{traceId:-},spanId=%X{spanId:-}] [%-5level] [${application}:${port}] [%logger{50}] [%msg]%n"/>
<!-- DEBUG 级别文件输出 -->
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<Pattern>${FILE_LOG_PATTERN}</Pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/debug/${application}-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>1MB</maxFileSize>
<maxHistory>180</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- INFO 级别文件输出(仅 INFO) -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<Pattern>${FILE_LOG_PATTERN}</Pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/info/${application}-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>1MB</maxFileSize>
<maxHistory>180</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- WARN 级别文件输出 -->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<Pattern>${FILE_LOG_PATTERN}</Pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/warn/${application}-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>1MB</maxFileSize>
<maxHistory>180</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- ERROR 级别文件输出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<Pattern>${FILE_LOG_PATTERN}</Pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/error/${application}-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>1MB</maxFileSize>
<maxHistory>180</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- Logback AsyncAppender 只允许挂载一个下游 Appender,因此每个级别单独异步 -->
<appender name="ASYNC_DEBUG_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>8192</queueSize>
<discardingThreshold>0</discardingThreshold>
<neverBlock>false</neverBlock>
<includeCallerData>false</includeCallerData>
<maxFlushTime>3000</maxFlushTime>
<appender-ref ref="DEBUG_FILE"/>
</appender>
<appender name="ASYNC_INFO_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>8192</queueSize>
<discardingThreshold>0</discardingThreshold>
<neverBlock>false</neverBlock>
<includeCallerData>false</includeCallerData>
<maxFlushTime>3000</maxFlushTime>
<appender-ref ref="INFO_FILE"/>
</appender>
<appender name="ASYNC_WARN_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>8192</queueSize>
<discardingThreshold>0</discardingThreshold>
<neverBlock>false</neverBlock>
<includeCallerData>false</includeCallerData>
<maxFlushTime>3000</maxFlushTime>
<appender-ref ref="WARN_FILE"/>
</appender>
<appender name="ASYNC_ERROR_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>8192</queueSize>
<discardingThreshold>0</discardingThreshold>
<neverBlock>false</neverBlock>
<includeCallerData>false</includeCallerData>
<maxFlushTime>3000</maxFlushTime>
<appender-ref ref="ERROR_FILE"/>
</appender>
<!-- 开发/测试环境:控制台同步输出,文件异步输出 -->
<springProfile name="dev,test">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 业务代码输出 DEBUG,第三方依赖保持 INFO,避免噪声过大 -->
<logger name="com.zjc" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ASYNC_DEBUG_FILE"/>
<appender-ref ref="ASYNC_INFO_FILE"/>
<appender-ref ref="ASYNC_WARN_FILE"/>
<appender-ref ref="ASYNC_ERROR_FILE"/>
</root>
</springProfile>
<!-- 生产环境:仅保留异步文件输出 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ASYNC_DEBUG_FILE"/>
<appender-ref ref="ASYNC_INFO_FILE"/>
<appender-ref ref="ASYNC_WARN_FILE"/>
<appender-ref ref="ASYNC_ERROR_FILE"/>
</root>
</springProfile>
</configuration>
经验总结
滚动文件名带上应用名,是最便宜的日志元数据。服务、级别、日期一眼可辨,再配合大小滚动和保留上限,日志才不会变成部署机上的黑洞。
评论
评论由 GitHub Discussions 承载,需要 GitHub 账号登录。