1. 日志基礎概念
1.1 什么是日志
日志是應用程序運行時記錄的事件、狀態(tài)和信息的集合,用于跟蹤應用程序的運行狀況、調(diào)試問題和監(jiān)控系統(tǒng)行為。
通俗理解:就像飛機的黑匣子,記錄著系統(tǒng)運行的所有關鍵信息,當出現(xiàn)問題時可以回放查看。
1.2 為什么需要日志管理
需求
說明
日常生活類比
問題診斷
當系統(tǒng)出現(xiàn)問題時快速定位
像醫(yī)院的病歷記錄
性能監(jiān)控
跟蹤系統(tǒng)性能指標
汽車的儀表盤
安全審計
記錄關鍵操作以備審查
銀行的交易記錄
行為分析
分析用戶行為模式
超市的購物小票
1.3 Java常見日志框架對比
框架
特點
適用場景
Spring Boot默認支持
Log4j
老牌日志框架,配置靈活
傳統(tǒng)Java項目
是(1.x)
Log4j2
Log4j升級版,性能更好
高性能需求項目
是
Logback
SLF4J原生實現(xiàn),性能好
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 日志級別詳解
級別
數(shù)值
說明
使用場景
TRACE
0
最詳細的跟蹤信息
開發(fā)階段深度調(diào)試
DEBUG
1
調(diào)試信息
開發(fā)階段問題排查
INFO
2
運行重要信息
生產(chǎn)環(huán)境常規(guī)監(jiān)控
WARN
3
潛在問題警告
需要注意但不緊急的問題
ERROR
4
錯誤事件但不影響系統(tǒng)
需要關注的問題
FATAL
5
嚴重錯誤導致系統(tǒng)退出
極少使用
通俗理解:就像醫(yī)院的分診系統(tǒng),TRACE是全面體檢,DEBUG是??茩z查,INFO是常規(guī)體檢,WARN是輕微癥狀,ERROR是需要立即處理的病癥。
3. 日志配置詳解
3.1 配置文件格式
Spring Boot支持以下格式的日志配置文件:
logback-spring.xml (推薦)logback.xmlapplication.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" />
<!-- 環(huán)境特定配置 -->
<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
輸出到控制臺
開發(fā)環(huán)境調(diào)試
RollingFileAppender
滾動文件輸出
生產(chǎn)環(huán)境持久化
SMTPAppender
郵件發(fā)送日志
錯誤報警
DBAppender
數(shù)據(jù)庫存儲日志
日志分析系統(tǒng)
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內(nèi)容
{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");
// 業(yè)務邏輯...
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 日志過濾
可以根據(jù)條件過濾日志,只記錄滿足條件的日志。
示例:只記錄包含"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 多環(huán)境日志配置
利用Spring Profile為不同環(huán)境配置不同的日志策略。
<!-- 開發(fā)環(huán)境配置 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
</springProfile>
<!-- 生產(chǎn)環(huán)境配置 -->
<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);適當?shù)娜罩炯墑e:ERROR:需要立即處理的問題WARN:潛在問題INFO:重要業(yè)務事件DEBUG:調(diào)試信息TRACE:詳細跟蹤避免副作用:日志記錄不應該改變程序行為不好:logger.debug("Value: " + expensiveOperation());好:logger.debug("Value: {}", () -> expensiveOperation());
5.2 性能優(yōu)化
使用參數(shù)化日志:
// 不好 - 即使日志級別高于DEBUG也會執(zhí)行字符串拼接
logger.debug("User " + userId + " accessed resource " + resourceId);
// 好 - 只有在DEBUG級別才會格式化字符串
logger.debug("User {} accessed resource {}", userId, resourceId);
異步日志:對于文件、網(wǎng)絡等慢速Appender使用異步方式合理配置日志級別:生產(chǎn)環(huán)境適當提高日志級別
5.3 日志監(jiān)控與分析
ELK Stack (Elasticsearch, Logstash, Kibana)SplunkPrometheus + Grafana (配合日志指標)
6. 常見問題與解決方案
6.1 日志文件過大
解決方案:
配置合理的滾動策略
logs/app-%d{yyyy-MM-dd}.%i.log
50MB
30
5GB
</rollingPolicy>
定期歸檔和清理舊日志
6.2 日志輸出混亂
解決方案:
使用MDC區(qū)分不同請求配置合理的日志格式<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{requestId}] %-5level %logger{36} - %msg%n</pattern>
6.3 日志性能問題
解決方案:
使用異步日志減少不必要的日志記錄避免在日志中執(zhí)行復雜操作
7. 實戰(zhàn)案例:電商系統(tǒng)日志配置
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>
<!-- 慢查詢?nèi)罩?-->
<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" />
<!-- 開發(fā)環(huán)境特殊配置 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
</root>
<logger name="com.ecommerce" level="DEBUG" />
</springProfile>
<!-- 生產(chǎn)環(huán)境特殊配置 -->
<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. 日志監(jiān)控與告警
9.1 常用監(jiān)控指標
指標
說明
監(jiān)控方式
ERROR日志頻率
單位時間內(nèi)ERROR日志數(shù)量
計數(shù)/分鐘
慢請求日志
超過閾值的請求響應時間
日志內(nèi)容分析
關鍵操作日志
如登錄、支付等
日志內(nèi)容匹配
日志量突變
日志量突然增加或減少
數(shù)量對比
9.2 集成Prometheus監(jiān)控
@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();
}
}
});
});
};
}
}
本文結(jié)束得如此突然,就像你永遠猜不到老板下一秒要改的需求。
頭條對markdown的文章顯示不太友好,想了解更多的可以關注微信公眾號:“Eric的技術雜貨庫”,后期會有更多的干貨以及資料下載。