This commit is contained in:
2026-08-25 14:46:06 +08:00
parent 5abc7543ae
commit f325d57fc1
106 changed files with 3682 additions and 450 deletions
@@ -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,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
@@ -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");
}
}