很多人写代码,只管业务逻辑,不知道系统在跑的时候:
-
哪个接口慢?
-
哪个服务 QPS 高?
-
哪个下游抖了?
-
哪条链路撑不住了?
-
哪个线程池快爆了?
-
哪次 GC 卡顿导致 RT 抖动?
这些其实都可以通过观测发现,所以我想通过一系列的文章分享下"观测"的实现。
一:观测
1.1 "事件记录"
你可以理解为:
"发生了什么"
比如:订单创建、用户登录、异常、重试、降级等等。
如果没有 traceId,日志是碎片;
如果有 traceId,日志就是"故事"。
1.2 "系统运行状态的数字化"
你可以理解为:
"系统现在的健康状况是什么"
比如:
-
QPS:有没有被压?
-
RT:变慢了吗?
-
P99:高峰压力如何?
-
JVM 堆:是否泄漏?
-
线程池:是否被打满?
1.3 "服务之间的全链路剖面"
你可以理解为:
"系统调用链长什么样"
比如:一次下单 → 走了哪些服务、哪些接口、哪些耗时?
二:日志
2.1依赖
java
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.3</version>
</dependency>
2.2配置
在resources文件夹下,添加logback-spring.xml文件,配置如下,
java
<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"/>打印日志在控制台(可以删除,减小开销)
<appender-ref ref="JSON_FILE"/>输出日志到文件
</root>
</configuration>
三:异常机制
3.1统一相应结构
java
@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错误码枚举
java
@Getter
public enum ErrorCode {
SYSTEM_ERROR(10001, "系统异常,请稍后再试"),
BAD_REQUEST(10002, "请求参数错误"),
NOT_FOUND(10003, "资源不存在"),
BUSINESS_ERROR(20001, "业务异常");
private final int code;
private final String msg;
ErrorCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
3.3业务异常类
java
@Getter
public class BizException extends RuntimeException {
private final int code;
public BizException(ErrorCode errorCode) {
super(errorCode.getMsg());
this.code = errorCode.getCode();
}
}
3.4全局异常处理器(核心)
java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler(BizException.class)
public ApiResponse<?> handleBizException(BizException e) {
log.warn("Business exception: {}", e.getMessage(), e);
return ApiResponse.fail(e.getCode(), e.getMessage());
}
/**
* 处理系统异常
*/
@ExceptionHandler(Exception.class)
public ApiResponse<?> handleException(Exception e) {
log.error("System exception:", e);
return ApiResponse.fail(
ErrorCode.SYSTEM_ERROR.getCode(),
ErrorCode.SYSTEM_ERROR.getMsg()
);
}
}
四:日志进阶
4.1加强日志
java
<configuration>
<!-- JSON 文件输出,按日期滚动 -->
<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>
<!-- 时间戳 -->
<timestamp />
<!-- 日志等级 -->
<logLevel />
<!-- 线程名 -->
<threadName />
<!-- 日志位置 -->
<callerData />
<!-- 日志内容 -->
<message />
<!-- 核心:输出 MDC,如 traceId -->
<mdc />
<!-- 记录 logger 名字 -->
<loggerName />
</providers>
</encoder>
</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" />
<appender-ref ref="JSON_FILE" />
</root>
</configuration>
4.2TraceId 工具类
java
//追踪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
java
@Component
public class TraceIdFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// 如果 header 已经有 traceId,如网关或 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线程池追踪
java
@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());
// 任务装饰器(增强:日志、链路追踪、异常等)
executor.setTaskDecorator(runnable -> () -> {
try {
runnable.run();
} catch (Exception e) {
System.err.println("Execute error: " + e.getMessage());
throw e;
}
});
executor.initialize();
return executor;
}
}
日志篇就到此结束了,之后有空会出剩下的内容的