springboot中的日志实现方式
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li><a href="#_label0">一:观测</a></li><ul class="second_class_ul"><li><a href="#_lab2_0_0">1.1 “事件记录”</a></li><li><a href="#_lab2_0_1">1.2 “系统运行状态的数字化”</a></li><li><a href="#_lab2_0_2">1.3 “服务之间的全链路剖面”</a></li></ul><li><a href="#_label1">二:日志</a></li><ul class="second_class_ul"><li><a href="#_lab2_1_3">2.1依赖</a></li><li><a href="#_lab2_1_4">2.2配置</a></li></ul><li><a href="#_label2">三:异常机制</a></li><ul class="second_class_ul"><li><a href="#_lab2_2_5">3.1统一相应结构</a></li><li><a href="#_lab2_2_6">3.2错误码枚举</a></li><li><a href="#_lab2_2_7">3.3业务异常类</a></li><li><a href="#_lab2_2_8">3.4全局异常处理器(核心)</a></li></ul><li><a href="#_label3">四:日志进阶</a></li><ul class="second_class_ul"><li><a href="#_lab2_3_9">4.1加强日志</a></li><li><a href="#_lab2_3_10">4.2TraceId 工具类</a></li><li><a href="#_lab2_3_11">4.3Filter</a></li><li><a href="#_lab2_3_12">4.4线程池追踪</a></li></ul><li><a href="#_label4">总结</a></li><ul class="second_class_ul"></ul></ul></div><p>很多人写代码,只管业务逻辑,不知道系统在跑的时候:</p><ul><li>哪个接口慢?</li><li>哪个服务 QPS 高?</li><li>哪个下游抖了?</li><li>哪条链路撑不住了?</li><li>哪个线程池快爆了?</li><li>哪次 GC 卡顿导致 RT 抖动?</li></ul>
<p>这些其实都可以通过观测发现,所以我想通过一系列的文章分享下“观测”的实现。</p>
<p class="maodian"><a name="_label0"></a></p><h2>一:观测</h2>
<p class="maodian"><a name="_lab2_0_0"></a></p><h3>1.1 “事件记录”</h3>
<p>你可以理解为:</p>
<p><strong>“发生了什么”</strong></p>
<p>比如:订单创建、用户登录、异常、重试、降级等等。</p>
<ul><li>如果没有 traceId,日志是碎片;</li><li>如果有 traceId,日志就是“故事”。</li></ul>
<p class="maodian"><a name="_lab2_0_1"></a></p><h3>1.2 “系统运行状态的数字化”</h3>
<p>你可以理解为:</p>
<p><strong>“系统现在的健康状况是什么”</strong></p>
<p>比如:</p>
<ul><li>QPS:有没有被压?</li><li>RT:变慢了吗?</li><li>P99:高峰压力如何?</li><li>JVM 堆:是否泄漏?</li><li>线程池:是否被打满?</li></ul>
<p class="maodian"><a name="_lab2_0_2"></a></p><h3>1.3 “服务之间的全链路剖面”</h3>
<p>你可以理解为:</p>
<p><strong>“系统调用链长什么样”</strong></p>
<p>比如:一次下单 → 走了哪些服务、哪些接口、哪些耗时?</p>
<p class="maodian"><a name="_label1"></a></p><h2>二:日志</h2>
<p class="maodian"><a name="_lab2_1_3"></a></p><h3>2.1依赖</h3>
<div class="jb51code"><pre class="brush:xml;"><dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.3</version>
</dependency>
</pre></div>
<p class="maodian"><a name="_lab2_1_4"></a></p><h3>2.2配置</h3>
<p>在resources文件夹下,添加logback-spring.xml文件,配置如下,</p>
<div class="jb51code"><pre class="brush: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"/>打印日志在控制台(可以删除,减小开销)
<appender-ref ref="JSON_FILE"/>输出日志到文件
</root>
</configuration></pre></div>
<p class="maodian"><a name="_label2"></a></p><h2>三:异常机制</h2>
<p class="maodian"><a name="_lab2_2_5"></a></p><h3>3.1统一相应结构</h3>
<div class="jb51code"><pre class="brush:sql;">@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;
}
}
</pre></div>
<p class="maodian"><a name="_lab2_2_6"></a></p><h3>3.2错误码枚举</h3>
<div class="jb51code"><pre class="brush:sql;">@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;
}
}
</pre></div>
<p class="maodian"><a name="_lab2_2_7"></a></p><h3>3.3业务异常类</h3>
<div class="jb51code"><pre class="brush:sql;">@Getter
public class BizException extends RuntimeException {
private final int code;
public BizException(ErrorCode errorCode) {
super(errorCode.getMsg());
this.code = errorCode.getCode();
}
}
</pre></div>
<p class="maodian"><a name="_lab2_2_8"></a></p><h3>3.4全局异常处理器(核心)</h3>
<div class="jb51code"><pre class="brush:sql;">@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()
);
}
}
</pre></div>
<p class="maodian"><a name="_label3"></a></p><h2>四:日志进阶</h2>
<p class="maodian"><a name="_lab2_3_9"></a></p><h3>4.1加强日志</h3>
<div class="jb51code"><pre class="brush:xml;"><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>
</pre></div>
<p class="maodian"><a name="_lab2_3_10"></a></p><h3>4.2TraceId 工具类</h3>
<div class="jb51code"><pre class="brush:sql;">//追踪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);
}
}
</pre></div>
<p class="maodian"><a name="_lab2_3_11"></a></p><h3>4.3Filter</h3>
<div class="jb51code"><pre class="brush:sql;">@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();
}
}
}
</pre></div>
<p class="maodian"><a name="_lab2_3_12"></a></p><h3>4.4线程池追踪</h3>
<div class="jb51code"><pre class="brush:sql;">@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;
}
}
</pre></div>
<p class="maodian"><a name="_label4"></a></p><h2>总结</h2>
<p>以上为个人经验,希望能给大家一个参考,也希望大家多多支持琼殿技术社区。</p>
頁:
[1]