优化
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
| 链路上下文 | `src/main/java/com/baiyue/common/web/CorrelationContext.java` |
|
||||
| 请求 ID 过滤器 | `src/main/java/com/baiyue/common/web/CorrelationIdFilter.java` |
|
||||
| W3C Trace Context | `src/main/java/com/baiyue/common/web/TraceParent.java`;严格接收 version 00,响应 `traceparent`、`X-Trace-Id` 与 MDC `traceId` 使用同一 trace ID |
|
||||
| Feign Trace 传播 | `FeignTraceRequestInterceptor.java`、`FeignTracePropagationAutoConfiguration.java`;仅 Feign 存在时装配,optional 依赖不向无 Feign 服务传递 |
|
||||
| 自动配置入口 | `src/main/java/com/baiyue/common/web/CommonWebAutoConfiguration.java`;为只扫描自身包的服务注册异常处理器和 Correlation 过滤器 |
|
||||
| 全局异常映射与业务失败指标 | `src/main/java/com/baiyue/common/web/GlobalExceptionHandler.java`;指标 `baiyue.api.business.failures` 按有限 `code` 标记 |
|
||||
| 过滤器测试 | `src/test/java/com/baiyue/common/web/CorrelationIdFilterTest.java` |
|
||||
@@ -25,5 +26,6 @@
|
||||
- 修改自动配置时必须验证 `audit-service`、`event-bridge-service` 等只扫描自身包的应用仍能加载 `GlobalExceptionHandler` 和 `CorrelationIdFilter`。
|
||||
- 修改 Correlation ID 时同步检查当前请求头契约、线程上下文清理和日志链路。
|
||||
- 入站 Trace 优先级固定为:合法单值 `traceparent` > 完全缺失 `traceparent` 时的 32 位非零十六进制 `X-Trace-Id` > 新 trace。只要 `traceparent` 已出现但重复、超长、格式非法或含全零 ID,就必须生成新 trace,不得回退信任 `X-Trace-Id`;请求 ID 独立按 `X-Request-Id` 校验或生成。
|
||||
- 同步 Feign 出站固定先移除调用模板中的 `X-Request-Id`、`X-Trace-Id`、`traceparent`,再仅从当前 MDC 的合法请求 ID 和 trace ID 重建;`traceparent` 沿用当前 trace ID、每次调用生成新非零 span ID、flags 固定 `01`。不得从业务参数、远端返回头或无效 MDC 建立上下文;本能力不处理 Reactor、异步线程、重试、超时或 fallback。
|
||||
- 从父目录运行 `mvn -pl common-web -am test`。
|
||||
- 所有对外 `Long/long` 值统一序列化为 JSON 字符串,避免 19 位雪花 ID 在浏览器中丢失精度;禁止业务模块重复添加局部转换器。
|
||||
|
||||
@@ -39,6 +39,11 @@
|
||||
<groupId>io.micrometer</groupId>
|
||||
<artifactId>micrometer-registry-prometheus</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-core</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
/** Enables synchronous Feign trace propagation without making Feign a required dependency. */
|
||||
@AutoConfiguration(afterName = "org.springframework.cloud.openfeign.FeignAutoConfiguration")
|
||||
@ConditionalOnClass(name = "feign.RequestInterceptor")
|
||||
public class FeignTracePropagationAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(FeignTraceRequestInterceptor.class)
|
||||
FeignTraceRequestInterceptor feignTraceRequestInterceptor() {
|
||||
return new FeignTraceRequestInterceptor();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import feign.RequestInterceptor;
|
||||
import feign.RequestTemplate;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.core.Ordered;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** Propagates the trusted synchronous request trace context to every Feign invocation. */
|
||||
public final class FeignTraceRequestInterceptor implements RequestInterceptor, Ordered {
|
||||
private static final Pattern SAFE_REQUEST_ID = Pattern.compile("[A-Za-z0-9._:-]{1,128}");
|
||||
|
||||
@Override
|
||||
public void apply(RequestTemplate template) {
|
||||
removeManagedHeaders(template);
|
||||
|
||||
String requestId = MDC.get(CorrelationContext.REQUEST_ID);
|
||||
if (requestId != null && SAFE_REQUEST_ID.matcher(requestId).matches()) {
|
||||
template.header(CorrelationContext.REQUEST_ID_HEADER, requestId);
|
||||
}
|
||||
|
||||
String traceId = MDC.get(CorrelationContext.TRACE_ID);
|
||||
if (TraceParent.validLegacyTraceId(traceId)) {
|
||||
TraceParent outbound = TraceParent.outboundChild(traceId);
|
||||
template.header(CorrelationContext.TRACE_ID_HEADER, outbound.traceId());
|
||||
template.header(CorrelationContext.TRACE_PARENT_HEADER, outbound.value());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.LOWEST_PRECEDENCE;
|
||||
}
|
||||
|
||||
private void removeManagedHeaders(RequestTemplate template) {
|
||||
template.removeHeader(CorrelationContext.REQUEST_ID_HEADER);
|
||||
template.removeHeader(CorrelationContext.TRACE_ID_HEADER);
|
||||
template.removeHeader(CorrelationContext.TRACE_PARENT_HEADER);
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,14 @@ public record TraceParent(String traceId, String parentId, String traceFlags) {
|
||||
return new TraceParent(traceId, randomNonZero(8), "00");
|
||||
}
|
||||
|
||||
/** Creates a sampled outbound child for a trusted current trace ID. */
|
||||
public static TraceParent outboundChild(String currentTraceId) {
|
||||
if (!validLegacyTraceId(currentTraceId)) {
|
||||
throw new IllegalArgumentException("Current trace ID is invalid");
|
||||
}
|
||||
return new TraceParent(currentTraceId.toLowerCase(java.util.Locale.ROOT), randomNonZero(8), "01");
|
||||
}
|
||||
|
||||
public static boolean validLegacyTraceId(String value) {
|
||||
return value != null && LEGACY_TRACE_ID.matcher(value).matches()
|
||||
&& !ZERO_TRACE_ID.equalsIgnoreCase(value);
|
||||
|
||||
+1
@@ -1,2 +1,3 @@
|
||||
com.baiyue.common.web.LongIdJacksonConfiguration
|
||||
com.baiyue.common.web.CommonWebAutoConfiguration
|
||||
com.baiyue.common.web.FeignTracePropagationAutoConfiguration
|
||||
|
||||
@@ -33,6 +33,8 @@ class CorrelationIdFilterTest {
|
||||
.matches("00-4bf92f3577b34da6a3ce929d0e0e4736-[0-9a-f]{16}-01");
|
||||
assertThat(chain.traceId).isEqualTo("4bf92f3577b34da6a3ce929d0e0e4736");
|
||||
assertThat(chain.spanId).matches("[0-9a-f]{16}").isNotEqualTo("0000000000000000");
|
||||
assertThat(org.slf4j.MDC.get(CorrelationContext.TRACE_ID)).isNull();
|
||||
assertThat(org.slf4j.MDC.get(CorrelationContext.SPAN_ID)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import feign.RequestInterceptor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FeignTracePropagationAutoConfigurationTest {
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(FeignTracePropagationAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void registersTraceInterceptorWhenFeignIsPresent() {
|
||||
contextRunner.run(context -> {
|
||||
assertThat(context).hasSingleBean(FeignTraceRequestInterceptor.class);
|
||||
assertThat(context.getBeansOfType(RequestInterceptor.class)).hasSize(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotLoadFeignConfigurationWhenFeignIsAbsent() {
|
||||
contextRunner.withClassLoader(new FilteredClassLoader(RequestInterceptor.class))
|
||||
.run(context -> {
|
||||
assertThat(context).hasNotFailed();
|
||||
assertThat(context).doesNotHaveBean("feignTraceRequestInterceptor");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void backsOffOnlyForAnApplicationProvidedTraceInterceptor() {
|
||||
contextRunner.withBean(FeignTraceRequestInterceptor.class, FeignTraceRequestInterceptor::new)
|
||||
.run(context -> assertThat(context).hasSingleBean(FeignTraceRequestInterceptor.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import feign.RequestTemplate;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.core.Ordered;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class FeignTraceRequestInterceptorTest {
|
||||
private final FeignTraceRequestInterceptor interceptor = new FeignTraceRequestInterceptor();
|
||||
|
||||
@AfterEach
|
||||
void clearMdc() {
|
||||
MDC.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
void runsLastSoTrustedContextHasFinalHeaderAuthority() {
|
||||
assertThat(interceptor.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overwritesCallerHeadersFromTrustedMdcAndCreatesNewSampledSpan() {
|
||||
MDC.put(CorrelationContext.REQUEST_ID, "req-trusted-123");
|
||||
MDC.put(CorrelationContext.TRACE_ID, "4bf92f3577b34da6a3ce929d0e0e4736");
|
||||
RequestTemplate template = templateWithSpoofedHeaders();
|
||||
|
||||
interceptor.apply(template);
|
||||
|
||||
assertThat(template.headers().get(CorrelationContext.REQUEST_ID_HEADER))
|
||||
.containsExactly("req-trusted-123");
|
||||
assertThat(template.headers().get(CorrelationContext.TRACE_ID_HEADER))
|
||||
.containsExactly("4bf92f3577b34da6a3ce929d0e0e4736");
|
||||
assertThat(template.headers().get(CorrelationContext.TRACE_PARENT_HEADER))
|
||||
.singleElement().asString()
|
||||
.matches("00-4bf92f3577b34da6a3ce929d0e0e4736-[0-9a-f]{16}-01");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsDifferentNonZeroSpanForEachSynchronousInvocation() {
|
||||
MDC.put(CorrelationContext.TRACE_ID, "4bf92f3577b34da6a3ce929d0e0e4736");
|
||||
RequestTemplate first = new RequestTemplate();
|
||||
RequestTemplate second = new RequestTemplate();
|
||||
|
||||
interceptor.apply(first);
|
||||
interceptor.apply(second);
|
||||
|
||||
assertThat(singleHeader(first, CorrelationContext.TRACE_PARENT_HEADER))
|
||||
.isNotEqualTo(singleHeader(second, CorrelationContext.TRACE_PARENT_HEADER));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stripsManagedCallerHeadersWhenTrustedContextIsMissingOrInvalid() {
|
||||
MDC.put(CorrelationContext.REQUEST_ID, "invalid request id");
|
||||
MDC.put(CorrelationContext.TRACE_ID, "not-a-w3c-trace-id");
|
||||
RequestTemplate template = templateWithSpoofedHeaders();
|
||||
|
||||
interceptor.apply(template);
|
||||
|
||||
assertThat(template.headers()).doesNotContainKeys(
|
||||
CorrelationContext.REQUEST_ID_HEADER,
|
||||
CorrelationContext.TRACE_ID_HEADER,
|
||||
CorrelationContext.TRACE_PARENT_HEADER);
|
||||
}
|
||||
|
||||
private RequestTemplate templateWithSpoofedHeaders() {
|
||||
RequestTemplate template = new RequestTemplate();
|
||||
template.header(CorrelationContext.REQUEST_ID_HEADER, "req-business-parameter");
|
||||
template.header(CorrelationContext.TRACE_ID_HEADER, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
template.header(CorrelationContext.TRACE_PARENT_HEADER,
|
||||
"00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-00");
|
||||
return template;
|
||||
}
|
||||
|
||||
private String singleHeader(RequestTemplate template, String name) {
|
||||
return template.headers().get(name).iterator().next();
|
||||
}
|
||||
}
|
||||
@@ -43,4 +43,12 @@ class TraceParentTest {
|
||||
assertThat(root.value()).matches(
|
||||
"00-abcdef0123456789abcdef0123456789-[0-9a-f]{16}-00");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsSampledOutboundChildForTrustedCurrentTrace() {
|
||||
TraceParent child = TraceParent.outboundChild("4BF92F3577B34DA6A3CE929D0E0E4736");
|
||||
|
||||
assertThat(child.value()).matches(
|
||||
"00-4bf92f3577b34da6a3ce929d0e0e4736-[0-9a-f]{16}-01");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user