mirror of
https://gitee.com/dromara/dax-pay
synced 2026-08-10 14:56:06 +08:00
feat: 引入 OpenTelemetry 实现主子应用全链路追踪
- daxpay-start 引入 actuator + opentelemetry 依赖 - application.yml 新增 tracing 配置(OTLP 关闭, 全采样), 修正 management 配置位置 - TraceIdFilter 简化为读取 MDC(OTel 注入) 并写入 ThreadLocal + 响应头 - RestClientConfiguration 改用注入 RestClient.Builder, 新增业务上下文透传拦截器 - logback pattern 补充 spanId
This commit is contained in:
@@ -3,8 +3,6 @@ package cn.daxpay.open.platform.common.request.context.filter;
|
||||
import cn.daxpay.open.platform.common.request.context.local.RequestContextStorage;
|
||||
import cn.daxpay.open.platform.core.code.CommonCode;
|
||||
import cn.daxpay.open.platform.core.code.WebHeaderCode;
|
||||
import cn.hutool.core.lang.Validator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
@@ -21,13 +19,16 @@ import java.io.IOException;
|
||||
|
||||
/// # 追踪ID过滤器
|
||||
///
|
||||
/// 基于 OpenTelemetry 自动注入的 MDC traceId, 将其透传到 ThreadLocal 与响应 header。
|
||||
/// 本过滤器不再自行生成 traceId, 完全依赖 OTel ServerHttpObservationFilter 的注入。
|
||||
/// 执行顺序晚于 OTel Filter(HIGHEST_PRECEDENCE + 1), 以确保 MDC 中已有 traceId。
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||
public class TraceIdFilter extends OncePerRequestFilter implements Ordered {
|
||||
|
||||
/// 过滤器优先级
|
||||
public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 1;
|
||||
/// 过滤器优先级: 晚于 OTel ServerHttpObservationFilter, 确保 MDC 已注入 traceId
|
||||
public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
@@ -38,34 +39,18 @@ public class TraceIdFilter extends OncePerRequestFilter implements Ordered {
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
try {
|
||||
String traceId = extractOrGenerateTraceId(request);
|
||||
RequestContextStorage.put(WebHeaderCode.X_TRACE_ID, traceId);
|
||||
MDC.put(CommonCode.TRACE_ID, traceId);
|
||||
response.setHeader(WebHeaderCode.X_TRACE_ID, traceId);
|
||||
// 读取 OTel ServerHttpObservationFilter 已注入的 traceId
|
||||
String traceId = MDC.get(CommonCode.TRACE_ID);
|
||||
if (StrUtil.isNotBlank(traceId)) {
|
||||
// 透传到 ThreadLocal, 供业务代码通过 RequestContextHolder 访问
|
||||
RequestContextStorage.put(WebHeaderCode.X_TRACE_ID, traceId);
|
||||
// 写入响应 header, 便于前端/调用方关联
|
||||
response.setHeader(WebHeaderCode.X_TRACE_ID, traceId);
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
} finally {
|
||||
MDC.clear();
|
||||
// 注意: 不要调用 MDC.clear(), OTel 自行管理 MDC 生命周期
|
||||
RequestContextStorage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// 从请求头提取或生成追踪ID
|
||||
private String extractOrGenerateTraceId(HttpServletRequest request) {
|
||||
String traceId = request.getHeader(WebHeaderCode.X_TRACE_ID);
|
||||
if (isValidTraceId(traceId)) {
|
||||
return traceId;
|
||||
}
|
||||
return String.valueOf(IdUtil.getSnowflakeNextId());
|
||||
}
|
||||
|
||||
/// 验证追踪ID的有效性
|
||||
private boolean isValidTraceId(String traceId) {
|
||||
if (StrUtil.isBlank(traceId)) {
|
||||
return false;
|
||||
}
|
||||
if (!Validator.isNumber(traceId)) {
|
||||
return false;
|
||||
}
|
||||
int length = traceId.length();
|
||||
return length >= 10 && length <= 20;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.daxpay.open.platform.common.spring.configuration;
|
||||
|
||||
import cn.daxpay.open.platform.common.config.properties.PlatformCommonProperties;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
@@ -12,16 +13,32 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
/// # RestClient 配置
|
||||
///
|
||||
/// 使用 Spring Boot 自动配置的 [RestClient.Builder], 以便 OpenTelemetry 的
|
||||
/// ClientRequestObservation 拦截器自动追加(W3C traceparent 头自动透传)。
|
||||
/// 另通过 BusinessContextInterceptor 透传业务上下文(国际化语言、终端编码)。
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class RestClientConfiguration {
|
||||
private final PlatformCommonProperties platformCommonProperties;
|
||||
|
||||
/// 业务上下文 header 名(与 WebHeaderCode 保持一致, 此处为避免底层模块依赖循环而硬编码)
|
||||
private static final String HEADER_ACCEPT_LANGUAGE = "accept-language";
|
||||
private static final String HEADER_X_CLIENT_CODE = "x-client-code";
|
||||
|
||||
@Bean
|
||||
public CloseableHttpClient httpClient() {
|
||||
var rest = platformCommonProperties.getSpring().getRest();
|
||||
@@ -55,18 +72,58 @@ public class RestClientConfiguration {
|
||||
.build();
|
||||
}
|
||||
|
||||
// 4. 将 HttpClient 适配为 Spring 的 ClientHttpRequestFactory
|
||||
// 将 HttpClient 适配为 Spring 的 ClientHttpRequestFactory
|
||||
@Bean
|
||||
public HttpComponentsClientHttpRequestFactory httpRequestFactory(CloseableHttpClient httpClient) {
|
||||
return new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||
}
|
||||
|
||||
// 5. 创建 RestClient Bean
|
||||
/// 创建 RestClient Bean
|
||||
///
|
||||
/// 使用自动配置的 RestClient.Builder 而非静态 [RestClient.builder],
|
||||
/// 以便 OTel 拦截器自动注入(W3C traceparent 头自动透传)。
|
||||
@Bean
|
||||
public RestClient restClient(HttpComponentsClientHttpRequestFactory httpRequestFactory) {
|
||||
return RestClient.builder()
|
||||
public RestClient restClient(RestClient.Builder restClientBuilder,
|
||||
HttpComponentsClientHttpRequestFactory httpRequestFactory) {
|
||||
return restClientBuilder
|
||||
.requestFactory(httpRequestFactory)
|
||||
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
.requestInterceptor(new BusinessContextInterceptor())
|
||||
.build();
|
||||
}
|
||||
|
||||
/// 业务上下文透传拦截器
|
||||
///
|
||||
/// OTel 仅自动透传 W3C traceparent, 业务上下文(国际化语言、终端编码)需手动透传。
|
||||
/// 通过 Spring 原生 [RequestContextHolder] 获取当前请求, 避免模块循环依赖。
|
||||
static class BusinessContextInterceptor implements ClientHttpRequestInterceptor {
|
||||
@Override
|
||||
public ClientHttpResponse intercept(org.springframework.http.HttpRequest request,
|
||||
byte[] body,
|
||||
ClientHttpRequestExecution execution) throws IOException {
|
||||
HttpServletRequest currentRequest = getCurrentRequest();
|
||||
if (currentRequest != null) {
|
||||
// 透传国际化语言(子应用异常消息按语言返回)
|
||||
String language = currentRequest.getHeader(HEADER_ACCEPT_LANGUAGE);
|
||||
if (StrUtil.isNotBlank(language)) {
|
||||
request.getHeaders().set(HttpHeaders.ACCEPT_LANGUAGE, language);
|
||||
}
|
||||
// 透传终端编码(运营端/H5/小程序/API)
|
||||
String clientCode = currentRequest.getHeader(HEADER_X_CLIENT_CODE);
|
||||
if (StrUtil.isNotBlank(clientCode)) {
|
||||
request.getHeaders().set(HEADER_X_CLIENT_CODE, clientCode);
|
||||
}
|
||||
}
|
||||
return execution.execute(request, body);
|
||||
}
|
||||
|
||||
/// 获取当前线程绑定的 HttpServletRequest(若存在)
|
||||
private static HttpServletRequest getCurrentRequest() {
|
||||
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
|
||||
if (attrs instanceof ServletRequestAttributes servletAttrs) {
|
||||
return servletAttrs.getRequest();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,16 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- 监控端点 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<!-- 链路追踪(OpenTelemetry, 仅日志关联, 不导出) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-opentelemetry</artifactId>
|
||||
</dependency>
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
||||
@@ -13,14 +13,24 @@ spring:
|
||||
multipart:
|
||||
# 上传文件大小限制为100M
|
||||
max-file-size: 100MB
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
# 监控端点与链路追踪(actuator 配置必须放在顶层, 不能放在 spring: 下)
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,httpexchanges
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
# 链路追踪配置(仅日志关联, 不导出到后端)
|
||||
tracing:
|
||||
sampling:
|
||||
probability: 1.0
|
||||
opentelemetry:
|
||||
tracing:
|
||||
export:
|
||||
otlp:
|
||||
enabled: false
|
||||
# ORM
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:mapper/**/*Mapper.xml
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<!-- 控制台 -->
|
||||
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||
<pattern>%yellow(%date{yyyy-MM-dd HH:mm:ssS})| %highlight(%-5level[%X{traceId:-}]) %boldYellow(%thread) - %boldGreen(%logger{36})| %msg%n</pattern>
|
||||
<pattern>%yellow(%date{yyyy-MM-dd HH:mm:ssS})| %highlight(%-5level[%X{traceId:-},%X{spanId:-}]) %boldYellow(%thread) - %boldGreen(%logger{36})| %msg%n</pattern>
|
||||
</layout>
|
||||
</appender>
|
||||
<!-- log格式日志收集 -->
|
||||
@@ -27,7 +27,7 @@
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<charset>UTF-8</charset>
|
||||
<pattern>%date{yyyy-MM-dd HH:mm:ssS} | %-5level[%X{traceId:-}] %thread - %logger{36}| %msg%n</pattern>
|
||||
<pattern>%date{yyyy-MM-dd HH:mm:ssS} | %-5level[%X{traceId:-},%X{spanId:-}] %thread - %logger{36}| %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user