springboot中的日志實(shí)現(xiàn)方式
很多人寫(xiě)代碼,只管業(yè)務(wù)邏輯,不知道系統(tǒng)在跑的時(shí)候:
- 哪個(gè)接口慢?
- 哪個(gè)服務(wù) QPS 高?
- 哪個(gè)下游抖了?
- 哪條鏈路撐不住了?
- 哪個(gè)線程池快爆了?
- 哪次 GC 卡頓導(dǎo)致 RT 抖動(dòng)?
這些其實(shí)都可以通過(guò)觀測(cè)發(fā)現(xiàn),所以我想通過(guò)一系列的文章分享下“觀測(cè)”的實(shí)現(xiàn)。
一:觀測(cè)
1.1 “事件記錄”
你可以理解為:
“發(fā)生了什么”
比如:訂單創(chuàng)建、用戶登錄、異常、重試、降級(jí)等等。
- 如果沒(méi)有 traceId,日志是碎片;
- 如果有 traceId,日志就是“故事”。
1.2 “系統(tǒng)運(yùn)行狀態(tài)的數(shù)字化”
你可以理解為:
“系統(tǒng)現(xiàn)在的健康狀況是什么”
比如:
- QPS:有沒(méi)有被壓?
- RT:變慢了嗎?
- P99:高峰壓力如何?
- JVM 堆:是否泄漏?
- 線程池:是否被打滿?
1.3 “服務(wù)之間的全鏈路剖面”
你可以理解為:
“系統(tǒng)調(diào)用鏈長(zhǎng)什么樣”
比如:一次下單 → 走了哪些服務(wù)、哪些接口、哪些耗時(shí)?
二:日志
2.1依賴(lài)
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.3</version>
</dependency>
2.2配置
在resources文件夾下,添加logback-spring.xml文件,配置如下,
<configuration>
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>具體文件路徑</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>具體文件路徑.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder"/>
</appender>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>打印日志在控制臺(tái)(可以刪除,減小開(kāi)銷(xiāo))
<appender-ref ref="JSON_FILE"/>輸出日志到文件
</root>
</configuration>三:異常機(jī)制
3.1統(tǒng)一相應(yīng)結(jié)構(gòu)
@Data
public class ApiResponse<T> {
private Integer code;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> resp = new ApiResponse<>();
resp.setCode(0);
resp.setMessage("OK");
resp.setData(data);
return resp;
}
public static ApiResponse<?> fail(Integer code, String message) {
ApiResponse<?> resp = new ApiResponse<>();
resp.setCode(code);
resp.setMessage(message);
return resp;
}
}
3.2錯(cuò)誤碼枚舉
@Getter
public enum ErrorCode {
SYSTEM_ERROR(10001, "系統(tǒng)異常,請(qǐng)稍后再試"),
BAD_REQUEST(10002, "請(qǐng)求參數(shù)錯(cuò)誤"),
NOT_FOUND(10003, "資源不存在"),
BUSINESS_ERROR(20001, "業(yè)務(wù)異常");
private final int code;
private final String msg;
ErrorCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
3.3業(yè)務(wù)異常類(lèi)
@Getter
public class BizException extends RuntimeException {
private final int code;
public BizException(ErrorCode errorCode) {
super(errorCode.getMsg());
this.code = errorCode.getCode();
}
}
3.4全局異常處理器(核心)
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 處理業(yè)務(wù)異常
*/
@ExceptionHandler(BizException.class)
public ApiResponse<?> handleBizException(BizException e) {
log.warn("Business exception: {}", e.getMessage(), e);
return ApiResponse.fail(e.getCode(), e.getMessage());
}
/**
* 處理系統(tǒng)異常
*/
@ExceptionHandler(Exception.class)
public ApiResponse<?> handleException(Exception e) {
log.error("System exception:", e);
return ApiResponse.fail(
ErrorCode.SYSTEM_ERROR.getCode(),
ErrorCode.SYSTEM_ERROR.getMsg()
);
}
}
四:日志進(jìn)階
4.1加強(qiáng)日志
<configuration>
<!-- JSON 文件輸出,按日期滾動(dòng) -->
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>你的文件名</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>你的文件名.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<!-- 核心:使用 Composite Encoder,讓我們能添加 MDC -->
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<!-- 時(shí)間戳 -->
<timestamp />
<!-- 日志等級(jí) -->
<logLevel />
<!-- 線程名 -->
<threadName />
<!-- 日志位置 -->
<callerData />
<!-- 日志內(nèi)容 -->
<message />
<!-- 核心:輸出 MDC,如 traceId -->
<mdc />
<!-- 記錄 logger 名字 -->
<loggerName />
</providers>
</encoder>
</appender>
<!-- 控制臺(tái)輸出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="JSON_FILE" />
</root>
</configuration>
4.2TraceId 工具類(lèi)
//追蹤id
public class TraceUtil {
private static final String TRACE_ID = "traceId";
public static String initTrace() {
String traceId = UUID.randomUUID().toString().replace("-", "");
MDC.put(TRACE_ID, traceId);
return traceId;
}
public static void setTrace(String traceId) {
MDC.put(TRACE_ID, traceId);
}
public static void clear() {
MDC.remove(TRACE_ID);
}
public static String getTrace() {
return MDC.get(TRACE_ID);
}
}
4.3Filter
@Component
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 如果 header 已經(jīng)有 traceId,如網(wǎng)關(guān)或 NGINX 傳遞
String traceId = request.getHeader("traceId");
if (traceId == null || traceId.isEmpty()) {
traceId = TraceUtil.initTrace();
} else {
TraceUtil.setTrace(traceId);
}
try {
filterChain.doFilter(request, response);
} finally {
TraceUtil.clear();
}
}
}
4.4線程池追蹤
@Configuration
public class ThreadPoolConfig {
@Bean("commonExecutor")
public ThreadPoolTaskExecutor commonExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("common-exec-");
// 拒絕策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
// 任務(wù)裝飾器(增強(qiáng):日志、鏈路追蹤、異常等)
executor.setTaskDecorator(runnable -> () -> {
try {
runnable.run();
} catch (Exception e) {
System.err.println("Execute error: " + e.getMessage());
throw e;
}
});
executor.initialize();
return executor;
}
}
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Windows下MySql錯(cuò)誤代碼1045的解決方法
這篇文章主要介紹了Windows下MySql錯(cuò)誤代碼1045的解決方法,文中還包含了2個(gè)Linux下的解決方法,需要的朋友可以參考下2014-06-06
mysql數(shù)據(jù)庫(kù)的全量與增量的備份以及恢復(fù)方式
在數(shù)據(jù)庫(kù)管理中,全量備份與恢復(fù)是將整個(gè)數(shù)據(jù)庫(kù)的數(shù)據(jù)導(dǎo)出并在需要時(shí)完整地恢復(fù),這通常使用mysqldump工具完成,增量備份則是在全量備份的基礎(chǔ)上,只備份那些自上次全量備份后發(fā)生變化的數(shù)據(jù),這需要數(shù)據(jù)庫(kù)的二進(jìn)制日志(binlog)開(kāi)啟2024-09-09
mysql signed unsigned和zerofill使用與區(qū)別
mysql中有符號(hào)signed,無(wú)符號(hào)unsigned與零填充zerofill,本文主要介紹了mysql signed unsigned和zerofill使用與區(qū)別,具有一定的參考價(jià)值,感興趣的可以了解一下2024-07-07
Mysql數(shù)據(jù)庫(kù)時(shí)間查詢(xún)舉例詳解
在項(xiàng)目開(kāi)發(fā)中,一些業(yè)務(wù)表字段經(jīng)常使用日期和時(shí)間類(lèi)型,而且后續(xù)還會(huì)牽涉到這類(lèi)字段的查詢(xún),下面這篇文章主要給大家介紹了關(guān)于Mysql數(shù)據(jù)庫(kù)時(shí)間查詢(xún)的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05
數(shù)據(jù)庫(kù)Sql實(shí)現(xiàn)截取時(shí)間段和日期實(shí)例(SQL時(shí)間截取)
在許多情況下你也許只想得到日期和時(shí)間的一部分,而不是完整的日期和時(shí)間,下面這篇文章主要給大家介紹了關(guān)于數(shù)據(jù)庫(kù)Sql實(shí)現(xiàn)截取時(shí)間段和日期(SQL時(shí)間截取)的相關(guān)資料,需要的朋友可以參考下2023-05-05
Mysql5.7解壓版的安裝和卸載及常見(jiàn)問(wèn)題小結(jié)
這篇文章主要介紹了Mysql5.7解壓版的安裝和卸載及常見(jiàn)問(wèn)題小結(jié),需要的朋友可以參考下2017-11-11

