1. 日志基础概念
1.1 什么是日志
日志是应用程序运行时记录的事件、状态和信息的集合,用于跟踪应用程序的运行状况、调试问题和监控系统行为。
通俗理解:就像飞机的黑匣子,记录着系统运行的所有关键信息,当出现问题时可以回放查看。
1.2 为什么需要日志管理
需求 | 说明 | 日常生活类比 |
---|---|---|
问题诊断 | 当系统出现问题时快速定位 | 像医院的病历记录 |
性能监控 | 跟踪系统性能指标 | 汽车的仪表盘 |
安全审计 | 记录关键操作以备审查 | 银行的交易记录 |
行为分析 | 分析用户行为模式 | 超市的购物小票 |
1.3 Java常见日志框架对比
框架 | 特点 | 适用场景 | Spring Boot默认支持 |
---|---|---|---|
Log4j | 老牌日志框架,配置灵活 | 传统Java项目 | 是(1.x) |
Log4j2 | Log4j升级版,性能更好 | 高性能需求项目 | 是 |
Logback | SLF4J原生实现,性能好 | Spring Boot默认 | 是 |
JUL (java.util.logging) | JDK自带,功能简单 | 简单应用 | 是 |
2. Spring Boot日志基础
2.1 默认日志配置
Spring Boot默认使用Logback作为日志框架,并通过spring-boot-starter-logging
自动配置。
简单使用示例:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
public class MyController {
// 获取Logger实例(通常在每个类中声明)
private static final Logger logger = LoggerFactory.getLogger(MyController.class);
@GetMapping("/hello")
public String hello() {
logger.trace("This is a TRACE message");
logger.debug("This is a DEBUG message");
logger.info("This is an INFO message"); // 最常用
logger.warn("This is a WARN message");
logger.error("This is an ERROR message");
return "Hello World";
}
}
2.2 日志级别详解
级别 | 数值 | 说明 | 使用场景 |
---|---|---|---|
TRACE | 0 | 最详细的跟踪信息 | 开发阶段深度调试 |
DEBUG | 1 | 调试信息 | 开发阶段问题排查 |
INFO | 2 | 运行重要信息 | 生产环境常规监控 |
WARN | 3 | 潜在问题警告 | 需要注意但不紧急的问题 |
ERROR | 4 | 错误事件但不影响系统 | 需要关注的问题 |
FATAL | 5 | 严重错误导致系统退出 | 极少使用 |
通俗理解:就像医院的分诊系统,TRACE是全面体检,DEBUG是专科检查,INFO是常规体检,WARN是轻微症状,ERROR是需要立即处理的病症。
3. 日志配置详解
3.1 配置文件格式
Spring Boot支持以下格式的日志配置文件:
logback-spring.xml
(推荐)logback.xml
application.properties
/application.yml
中的简单配置
3.2 application.properties配置
# 设置全局日志级别
logging.level.root=WARN
# 设置特定包日志级别
logging.level.com.myapp=DEBUG
# 文件输出配置
logging.file.name=myapp.log
# 或者使用logging.file.path指定目录
logging.file.path=/var/logs
# 日志格式配置
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n
# 日志文件大小限制和保留策略
logging.logback.rollingpolicy.max-file-size=10MB
logging.logback.rollingpolicy.max-history=7
3.3 logback-spring.xml详细配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
<!-- 定义变量 -->
<property name="LOG_PATH" value="./logs" />
<property name="APP_NAME" value="my-application" />
<!-- 控制台输出appender -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 文件输出appender -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APP_NAME}-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 异步日志appender -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="FILE" />
</appender>
<!-- 日志级别配置 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ASYNC_FILE" />
</root>
<!-- 特定包日志级别 -->
<logger name="com.myapp" level="DEBUG" />
<logger name="org.springframework" level="WARN" />
<!-- 环境特定配置 -->
<springProfile name="dev">
<logger name="com.myapp" level="TRACE" />
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ASYNC_FILE" />
</root>
</springProfile>
</configuration>
3.4 配置项详细解析
3.4.1 Appender类型
Appender类型 | 作用 | 适用场景 |
---|---|---|
ConsoleAppender | 输出到控制台 | 开发环境调试 |
RollingFileAppender | 滚动文件输出 | 生产环境持久化 |
SMTPAppender | 邮件发送日志 | 错误报警 |
DBAppender | 数据库存储日志 | 日志分析系统 |
AsyncAppender | 异步日志 | 高性能需求 |
3.4.2 RollingPolicy策略
策略类型 | 特点 | 配置示例 |
---|---|---|
TimeBasedRollingPolicy | 按时间滚动 | %d{yyyy-MM-dd}.log |
SizeAndTimeBasedRollingPolicy | 按大小和时间滚动 | %d{yyyy-MM-dd}.%i.log |
FixedWindowRollingPolicy | 固定窗口滚动 | myapp.%i.log.zip |
3.4.3 日志格式模式
模式 | 说明 | 示例输出 |
---|---|---|
%d | 日期时间 | 2023-01-01 12:00:00 |
%thread | 线程名 | main |
%level | 日志级别 | INFO |
%logger | Logger名称 | com.myapp.MyClass |
%msg | 日志消息 | User login success |
%n | 换行符 | - |
%X | MDC内容 | {key:value} |
4. 高级日志功能
4.1 MDC (Mapped Diagnostic Context)
MDC用于在日志中添加上下文信息,如用户ID、请求ID等。
使用示例:
import org.slf4j.MDC;
@RestController
public class OrderController {
private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
@GetMapping("/order/{id}")
public Order getOrder(@PathVariable String id) {
// 添加上下文信息
MDC.put("userId", "user123");
MDC.put("orderId", id);
MDC.put("ip", "192.168.1.1");
try {
logger.info("Fetching order details");
// 业务逻辑...
return orderService.getOrder(id);
} finally {
// 清除MDC
MDC.clear();
}
}
}
logback配置中添加MDC:
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [%X{userId}] [%X{orderId}] %-5level %logger{36} - %msg%n</pattern>
4.2 日志过滤
可以根据条件过滤日志,只记录满足条件的日志。
示例:只记录包含"important"的ERROR日志
<appender name="IMPORTANT_ERRORS" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>important-errors.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
<expression>message.contains("important")</expression>
</evaluator>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 其他配置 -->
</appender>
4.3 日志异步输出
对于性能敏感的应用,可以使用异步日志减少I/O阻塞。
配置示例:
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 队列大小,默认256 -->
<queueSize>512</queueSize>
<!-- 当队列剩余容量小于此值时,丢弃TRACE/DEBUG/INFO级别日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 引用实际的appender -->
<appender-ref ref="FILE" />
</appender>
4.4 多环境日志配置
利用Spring Profile为不同环境配置不同的日志策略。
<!-- 开发环境配置 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
<!-- 生产环境配置 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ASYNC_FILE" />
</root>
<logger name="org.hibernate.SQL" level="WARN" />
</springProfile>
5. 日志最佳实践
5.1 日志记录原则
-
有意义的消息:避免无意义的日志,如"进入方法"、“退出方法”
- 不好:
logger.info("Method called");
- 好:
logger.info("Processing order {} for user {}", orderId, userId);
- 不好:
-
适当的日志级别:
- ERROR:需要立即处理的问题
- WARN:潜在问题
- INFO:重要业务事件
- DEBUG:调试信息
- TRACE:详细跟踪
-
避免副作用:日志记录不应该改变程序行为
- 不好:
logger.debug("Value: " + expensiveOperation());
- 好:
logger.debug("Value: {}", () -> expensiveOperation());
- 不好:
5.2 性能优化
-
使用参数化日志:
// 不好 - 即使日志级别高于DEBUG也会执行字符串拼接 logger.debug("User " + userId + " accessed resource " + resourceId); // 好 - 只有在DEBUG级别才会格式化字符串 logger.debug("User {} accessed resource {}", userId, resourceId);
-
异步日志:对于文件、网络等慢速Appender使用异步方式
-
合理配置日志级别:生产环境适当提高日志级别
5.3 日志监控与分析
- ELK Stack (Elasticsearch, Logstash, Kibana)
- Splunk
- Prometheus + Grafana (配合日志指标)
6. 常见问题与解决方案
6.1 日志文件过大
解决方案:
- 配置合理的滚动策略
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>logs/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxFileSize>50MB</maxFileSize> <maxHistory>30</maxHistory> <totalSizeCap>5GB</totalSizeCap> </rollingPolicy>
- 定期归档和清理旧日志
6.2 日志输出混乱
解决方案:
- 使用MDC区分不同请求
- 配置合理的日志格式
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{requestId}] %-5level %logger{36} - %msg%n</pattern>
6.3 日志性能问题
解决方案:
- 使用异步日志
- 减少不必要的日志记录
- 避免在日志中执行复杂操作
7. 实战案例:电商系统日志配置
7.1 完整logback-spring.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 公共属性 -->
<property name="LOG_HOME" value="/var/logs/ecommerce" />
<property name="APP_NAME" value="ecommerce-app" />
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{userId}] [%X{requestId}] %-5level %logger{36} - %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_NAME}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 错误日志单独文件 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/${APP_NAME}-error.log</file>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${APP_NAME}-error-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>90</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 异步appender -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>1024</queueSize>
<discardingThreshold>0</discardingThreshold>
<includeCallerData>true</includeCallerData>
<appender-ref ref="FILE" />
</appender>
<!-- 异步错误appender -->
<appender name="ASYNC_ERROR" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>512</queueSize>
<appender-ref ref="ERROR_FILE" />
</appender>
<!-- 慢查询日志 -->
<appender name="SLOW_QUERY" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/slow-query.log</file>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
<expression>
(message.contains("SQL") || message.contains("Query"))
&& (contains("took") || contains("duration"))
&& (getMarker() != null && getMarker().contains("SLOW"))
</expression>
</evaluator>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/slow-query-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 根日志配置 -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ASYNC_FILE" />
<appender-ref ref="ASYNC_ERROR" />
</root>
<!-- 特定包配置 -->
<logger name="com.ecommerce.dao" level="DEBUG" />
<logger name="com.ecommerce.service" level="INFO" />
<logger name="org.hibernate.SQL" level="WARN" />
<logger name="org.springframework" level="WARN" />
<!-- 开发环境特殊配置 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
<logger name="com.ecommerce" level="DEBUG" />
</springProfile>
<!-- 生产环境特殊配置 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ASYNC_FILE" />
<appender-ref ref="ASYNC_ERROR" />
</root>
<logger name="com.ecommerce.api" level="INFO" additivity="false">
<appender-ref ref="SLOW_QUERY" />
</logger>
</springProfile>
</configuration>
7.2 日志使用示例代码
@RestController
@RequestMapping("/orders")
public class OrderController {
private static final Logger logger = LoggerFactory.getLogger(OrderController.class);
private static final Marker SLOW_OPERATION_MARKER = MarkerFactory.getMarker("SLOW");
@Autowired
private OrderService orderService;
@GetMapping("/{id}")
public ResponseEntity<Order> getOrder(@PathVariable String id, HttpServletRequest request) {
// 设置MDC
MDC.put("requestId", UUID.randomUUID().toString());
MDC.put("userId", request.getRemoteUser());
MDC.put("clientIp", request.getRemoteAddr());
try {
logger.info("Fetching order with id: {}", id);
long startTime = System.currentTimeMillis();
Order order = orderService.getOrderById(id);
long duration = System.currentTimeMillis() - startTime;
if (duration > 500) {
logger.warn(SLOW_OPERATION_MARKER, "Slow order retrieval took {}ms for order {}", duration, id);
}
logger.debug("Order details: {}", order);
return ResponseEntity.ok(order);
} catch (OrderNotFoundException e) {
logger.error("Order not found with id: {}", id, e);
return ResponseEntity.notFound().build();
} catch (Exception e) {
logger.error("Unexpected error fetching order {}", id, e);
return ResponseEntity.internalServerError().build();
} finally {
MDC.clear();
}
}
@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request,
@RequestHeader("X-User-Id") String userId) {
MDC.put("userId", userId);
try {
logger.info("Creating new order for user {}", userId);
logger.debug("Order request details: {}", request);
Order createdOrder = orderService.createOrder(request, userId);
logger.info("Order created successfully with id: {}", createdOrder.getId());
return ResponseEntity.ok(createdOrder);
} catch (InvalidOrderException e) {
logger.warn("Invalid order request from user {}: {}", userId, e.getMessage());
return ResponseEntity.badRequest().build();
} finally {
MDC.clear();
}
}
}
8. 日志框架切换
8.1 切换到Log4j2
- 排除默认的Logback依赖
- 添加Log4j2依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
8.2 Log4j2配置示例
log4j2-spring.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
<Properties>
<Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] [%X{requestId}] %-5level %logger{36} - %msg%n</Property>
<Property name="LOG_DIR">logs</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}"/>
</Console>
<RollingFile name="File" fileName="${LOG_DIR}/app.log"
filePattern="${LOG_DIR}/app-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<SizeBasedTriggeringPolicy size="50MB"/>
<TimeBasedTriggeringPolicy/>
</Policies>
<DefaultRolloverStrategy max="30"/>
</RollingFile>
<Async name="AsyncFile" bufferSize="512">
<AppenderRef ref="File"/>
</Async>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="AsyncFile"/>
</Root>
<Logger name="com.myapp" level="debug" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
</Loggers>
</Configuration>
9. 日志监控与告警
9.1 常用监控指标
指标 | 说明 | 监控方式 |
---|---|---|
ERROR日志频率 | 单位时间内ERROR日志数量 | 计数/分钟 |
慢请求日志 | 超过阈值的请求响应时间 | 日志内容分析 |
关键操作日志 | 如登录、支付等 | 日志内容匹配 |
日志量突变 | 日志量突然增加或减少 | 数量对比 |
9.2 集成Prometheus监控
@Configuration
public class LogMetricsConfig {
private static final Counter errorCounter = Counter.build()
.name("log_errors_total")
.help("Total number of ERROR logs")
.labelNames("logger", "exception")
.register();
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags("application", "my-spring-app");
}
@Bean
public ApplicationListener<ApplicationReadyEvent> logMetricsListener() {
return event -> {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
loggerContext.getLoggerList().forEach(logger -> {
((ch.qos.logback.classic.Logger) logger).addAppender(new AppenderBase<ILoggingEvent>() {
@Override
protected void append(ILoggingEvent event) {
if (event.getLevel().isGreaterOrEqual(Level.ERROR)) {
errorCounter.labels(
event.getLoggerName(),
event.getThrowableProxy() != null ?
event.getThrowableProxy().getClassName() : "none"
).inc();
}
}
});
});
};
}
}
本文结束得如此突然,就像你永远猜不到老板下一秒要改的需求。