优化
This commit is contained in:
@@ -9,8 +9,11 @@ import org.slf4j.MDC;
|
||||
public final class CorrelationContext {
|
||||
public static final String REQUEST_ID = "requestId";
|
||||
public static final String TRACE_ID = "traceId";
|
||||
public static final String SPAN_ID = "spanId";
|
||||
public static final String TRACE_PARENT = "traceparent";
|
||||
public static final String REQUEST_ID_HEADER = "X-Request-Id";
|
||||
public static final String TRACE_ID_HEADER = "X-Trace-Id";
|
||||
public static final String TRACE_PARENT_HEADER = TraceParent.HEADER;
|
||||
|
||||
private CorrelationContext() {}
|
||||
|
||||
@@ -22,6 +25,14 @@ public final class CorrelationContext {
|
||||
return value(request, TRACE_ID);
|
||||
}
|
||||
|
||||
public static String spanId(HttpServletRequest request) {
|
||||
return value(request, SPAN_ID);
|
||||
}
|
||||
|
||||
public static String traceParent(HttpServletRequest request) {
|
||||
return value(request, TRACE_PARENT);
|
||||
}
|
||||
|
||||
private static String value(HttpServletRequest request, String key) {
|
||||
Object attribute = request.getAttribute(key);
|
||||
return attribute == null ? MDC.get(key) : attribute.toString();
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -25,24 +26,60 @@ public class CorrelationIdFilter extends OncePerRequestFilter {
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String requestId = acceptedOrGenerated(request.getHeader(CorrelationContext.REQUEST_ID_HEADER), false);
|
||||
String traceId = acceptedOrGenerated(request.getHeader(CorrelationContext.TRACE_ID_HEADER), true);
|
||||
String requestId = requestId(request);
|
||||
TraceParent traceParent = traceParent(request);
|
||||
String traceId = traceParent.traceId();
|
||||
request.setAttribute(CorrelationContext.REQUEST_ID, requestId);
|
||||
request.setAttribute(CorrelationContext.TRACE_ID, traceId);
|
||||
request.setAttribute(CorrelationContext.SPAN_ID, traceParent.parentId());
|
||||
request.setAttribute(CorrelationContext.TRACE_PARENT, traceParent.value());
|
||||
response.setHeader(CorrelationContext.REQUEST_ID_HEADER, requestId);
|
||||
response.setHeader(CorrelationContext.TRACE_ID_HEADER, traceId);
|
||||
response.setHeader(CorrelationContext.TRACE_PARENT_HEADER, traceParent.value());
|
||||
|
||||
try (MDC.MDCCloseable ignoredRequest = MDC.putCloseable(CorrelationContext.REQUEST_ID, requestId);
|
||||
MDC.MDCCloseable ignoredTrace = MDC.putCloseable(CorrelationContext.TRACE_ID, traceId)) {
|
||||
MDC.MDCCloseable ignoredTrace = MDC.putCloseable(CorrelationContext.TRACE_ID, traceId);
|
||||
MDC.MDCCloseable ignoredSpan = MDC.putCloseable(CorrelationContext.SPAN_ID, traceParent.parentId())) {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
private String acceptedOrGenerated(String candidate, boolean compact) {
|
||||
if (candidate != null && SAFE_ID.matcher(candidate).matches()) {
|
||||
return candidate;
|
||||
private String requestId(HttpServletRequest request) {
|
||||
HeaderValue header = singleHeader(request, CorrelationContext.REQUEST_ID_HEADER, 128);
|
||||
if (header.valid() && SAFE_ID.matcher(header.value()).matches()) {
|
||||
return header.value();
|
||||
}
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
private TraceParent traceParent(HttpServletRequest request) {
|
||||
HeaderValue standard = singleHeader(request, CorrelationContext.TRACE_PARENT_HEADER,
|
||||
TraceParent.HEADER_LENGTH);
|
||||
if (standard.present()) {
|
||||
if (standard.valid()) {
|
||||
return TraceParent.parse(standard.value()).map(TraceParent::childOf)
|
||||
.orElseGet(() -> TraceParent.newRoot(null));
|
||||
}
|
||||
return TraceParent.newRoot(null);
|
||||
}
|
||||
|
||||
HeaderValue legacy = singleHeader(request, CorrelationContext.TRACE_ID_HEADER, 32);
|
||||
return TraceParent.newRoot(legacy.valid() ? legacy.value() : null);
|
||||
}
|
||||
|
||||
private HeaderValue singleHeader(HttpServletRequest request, String name, int maxLength) {
|
||||
Enumeration<String> values = request.getHeaders(name);
|
||||
if (values == null || !values.hasMoreElements()) {
|
||||
return HeaderValue.missing();
|
||||
}
|
||||
String value = values.nextElement();
|
||||
boolean duplicate = values.hasMoreElements();
|
||||
return new HeaderValue(true, !duplicate && value != null && value.length() <= maxLength, value);
|
||||
}
|
||||
|
||||
private record HeaderValue(boolean present, boolean valid, String value) {
|
||||
private static HeaderValue missing() {
|
||||
return new HeaderValue(false, false, null);
|
||||
}
|
||||
String id = UUID.randomUUID().toString();
|
||||
return compact ? id.replace("-", "") : id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Strict W3C {@code traceparent} version 00 value.
|
||||
*
|
||||
* <p>This type deliberately accepts only the currently frozen version-00 shape. Unsupported,
|
||||
* ambiguous or malformed input must start a new trace instead of being partially trusted.</p>
|
||||
*/
|
||||
public record TraceParent(String traceId, String parentId, String traceFlags) {
|
||||
public static final String HEADER = "traceparent";
|
||||
public static final int HEADER_LENGTH = 55;
|
||||
|
||||
private static final Pattern VERSION_00 = Pattern.compile(
|
||||
"00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})");
|
||||
private static final Pattern LEGACY_TRACE_ID = Pattern.compile("[0-9A-Fa-f]{32}");
|
||||
private static final String ZERO_TRACE_ID = "0".repeat(32);
|
||||
private static final String ZERO_PARENT_ID = "0".repeat(16);
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
private static final HexFormat HEX = HexFormat.of();
|
||||
|
||||
public TraceParent {
|
||||
if (!validTraceId(traceId) || !validParentId(parentId)
|
||||
|| traceFlags == null || !traceFlags.matches("[0-9a-f]{2}")) {
|
||||
throw new IllegalArgumentException("Invalid W3C traceparent components");
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses one exact, lowercase W3C version-00 header value. */
|
||||
public static Optional<TraceParent> parse(String value) {
|
||||
if (value == null || value.length() != HEADER_LENGTH) {
|
||||
return Optional.empty();
|
||||
}
|
||||
Matcher matcher = VERSION_00.matcher(value);
|
||||
if (!matcher.matches() || !validTraceId(matcher.group(1)) || !validParentId(matcher.group(2))) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(new TraceParent(matcher.group(1), matcher.group(2), matcher.group(3)));
|
||||
}
|
||||
|
||||
/** Creates the local inbound context for a valid remote parent. */
|
||||
public static TraceParent childOf(TraceParent remoteParent) {
|
||||
return new TraceParent(remoteParent.traceId(), randomNonZero(8), remoteParent.traceFlags());
|
||||
}
|
||||
|
||||
/** Creates a new root context, optionally reusing a valid legacy trace ID. */
|
||||
public static TraceParent newRoot(String legacyTraceId) {
|
||||
String traceId = validLegacyTraceId(legacyTraceId)
|
||||
? legacyTraceId.toLowerCase(java.util.Locale.ROOT) : randomNonZero(16);
|
||||
return new TraceParent(traceId, randomNonZero(8), "00");
|
||||
}
|
||||
|
||||
public static boolean validLegacyTraceId(String value) {
|
||||
return value != null && LEGACY_TRACE_ID.matcher(value).matches()
|
||||
&& !ZERO_TRACE_ID.equalsIgnoreCase(value);
|
||||
}
|
||||
|
||||
public String value() {
|
||||
return "00-" + traceId + '-' + parentId + '-' + traceFlags;
|
||||
}
|
||||
|
||||
private static boolean validTraceId(String value) {
|
||||
return value != null && value.matches("[0-9a-f]{32}") && !ZERO_TRACE_ID.equals(value);
|
||||
}
|
||||
|
||||
private static boolean validParentId(String value) {
|
||||
return value != null && value.matches("[0-9a-f]{16}") && !ZERO_PARENT_ID.equals(value);
|
||||
}
|
||||
|
||||
private static String randomNonZero(int byteCount) {
|
||||
byte[] bytes = new byte[byteCount];
|
||||
String value;
|
||||
do {
|
||||
RANDOM.nextBytes(bytes);
|
||||
value = HEX.formatHex(bytes);
|
||||
} while (value.chars().allMatch(character -> character == '0'));
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
|
||||
@@ -16,6 +18,8 @@ class CommonWebAutoConfigurationTest {
|
||||
contextRunner.run(context -> {
|
||||
assertThat(context).hasSingleBean(GlobalExceptionHandler.class);
|
||||
assertThat(context).hasSingleBean(CorrelationIdFilter.class);
|
||||
assertThat(CorrelationIdFilter.class.getAnnotation(Order.class).value())
|
||||
.isEqualTo(Ordered.HIGHEST_PRECEDENCE);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import org.springframework.mock.web.MockFilterChain;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DisplayName("请求链路标识过滤功能")
|
||||
@@ -13,27 +15,85 @@ class CorrelationIdFilterTest {
|
||||
private final CorrelationIdFilter filter = new CorrelationIdFilter();
|
||||
|
||||
@Test
|
||||
void preservesSafeIncomingIds() throws Exception {
|
||||
void validTraceParentWinsAndMapsToLegacyTraceIdAndMdc() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(CorrelationContext.REQUEST_ID_HEADER, "req-123");
|
||||
request.addHeader(CorrelationContext.TRACE_ID_HEADER, "trace-123");
|
||||
request.addHeader(CorrelationContext.TRACE_ID_HEADER, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
request.addHeader(CorrelationContext.TRACE_PARENT_HEADER,
|
||||
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
filter.doFilter(request, response, new MockFilterChain());
|
||||
CapturingFilterChain chain = new CapturingFilterChain();
|
||||
filter.doFilter(request, response, chain);
|
||||
|
||||
assertThat(response.getHeader(CorrelationContext.REQUEST_ID_HEADER)).isEqualTo("req-123");
|
||||
assertThat(request.getAttribute(CorrelationContext.TRACE_ID)).isEqualTo("trace-123");
|
||||
assertThat(response.getHeader(CorrelationContext.TRACE_ID_HEADER))
|
||||
.isEqualTo("4bf92f3577b34da6a3ce929d0e0e4736");
|
||||
assertThat(response.getHeader(CorrelationContext.TRACE_PARENT_HEADER))
|
||||
.matches("00-4bf92f3577b34da6a3ce929d0e0e4736-[0-9a-f]{16}-01");
|
||||
assertThat(chain.traceId).isEqualTo("4bf92f3577b34da6a3ce929d0e0e4736");
|
||||
assertThat(chain.spanId).matches("[0-9a-f]{16}").isNotEqualTo("0000000000000000");
|
||||
}
|
||||
|
||||
@Test
|
||||
void replacesUnsafeIncomingIds() throws Exception {
|
||||
void fallsBackToValidLegacyTraceIdOnlyWhenTraceParentIsMissing() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(CorrelationContext.REQUEST_ID_HEADER, "invalid value\n");
|
||||
request.addHeader(CorrelationContext.TRACE_ID_HEADER, "ABCDEF0123456789ABCDEF0123456789");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
filter.doFilter(request, response, new MockFilterChain());
|
||||
|
||||
assertThat(response.getHeader(CorrelationContext.TRACE_ID_HEADER))
|
||||
.isEqualTo("abcdef0123456789abcdef0123456789");
|
||||
assertThat(response.getHeader(CorrelationContext.TRACE_PARENT_HEADER))
|
||||
.matches("00-abcdef0123456789abcdef0123456789-[0-9a-f]{16}-00");
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidTraceParentStartsNewTraceWithoutTrustingLegacyTraceId() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(CorrelationContext.TRACE_PARENT_HEADER,
|
||||
"00-00000000000000000000000000000000-00f067aa0ba902b7-01");
|
||||
request.addHeader(CorrelationContext.TRACE_ID_HEADER, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
filter.doFilter(request, response, new MockFilterChain());
|
||||
|
||||
assertThat(response.getHeader(CorrelationContext.TRACE_ID_HEADER))
|
||||
.matches("[0-9a-f]{32}")
|
||||
.isNotEqualTo("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
.isNotEqualTo("00000000000000000000000000000000");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDuplicateAndOversizedHeaders() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.addHeader(CorrelationContext.REQUEST_ID_HEADER, "a".repeat(129));
|
||||
request.addHeader(CorrelationContext.TRACE_PARENT_HEADER,
|
||||
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");
|
||||
request.addHeader(CorrelationContext.TRACE_PARENT_HEADER,
|
||||
"00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-00");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
filter.doFilter(request, response, new MockFilterChain());
|
||||
|
||||
assertThat(response.getHeader(CorrelationContext.REQUEST_ID_HEADER))
|
||||
.matches("[0-9a-f-]{36}");
|
||||
assertThat(response.getHeader(CorrelationContext.TRACE_ID_HEADER))
|
||||
.matches("[0-9a-f]{32}");
|
||||
.matches("[0-9a-f]{32}")
|
||||
.isNotEqualTo("4bf92f3577b34da6a3ce929d0e0e4736")
|
||||
.isNotEqualTo("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
}
|
||||
|
||||
private static final class CapturingFilterChain extends MockFilterChain {
|
||||
private String traceId;
|
||||
private String spanId;
|
||||
|
||||
@Override
|
||||
public void doFilter(jakarta.servlet.ServletRequest request,
|
||||
jakarta.servlet.ServletResponse response) throws IOException {
|
||||
traceId = org.slf4j.MDC.get(CorrelationContext.TRACE_ID);
|
||||
spanId = org.slf4j.MDC.get(CorrelationContext.SPAN_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TraceParentTest {
|
||||
|
||||
@Test
|
||||
void parsesCanonicalVersion00AndCreatesLocalChild() {
|
||||
TraceParent remote = TraceParent.parse(
|
||||
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01").orElseThrow();
|
||||
|
||||
TraceParent local = TraceParent.childOf(remote);
|
||||
|
||||
assertThat(local.traceId()).isEqualTo(remote.traceId());
|
||||
assertThat(local.parentId()).matches("[0-9a-f]{16}")
|
||||
.isNotEqualTo(remote.parentId())
|
||||
.isNotEqualTo("0000000000000000");
|
||||
assertThat(local.traceFlags()).isEqualTo("01");
|
||||
assertThat(local.value()).hasSize(TraceParent.HEADER_LENGTH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonCanonicalUnsupportedAndZeroIdentifiers() {
|
||||
assertThat(TraceParent.parse(
|
||||
"00-4BF92F3577B34DA6A3CE929D0E0E4736-00f067aa0ba902b7-01")).isEmpty();
|
||||
assertThat(TraceParent.parse(
|
||||
"01-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")).isEmpty();
|
||||
assertThat(TraceParent.parse(
|
||||
"00-00000000000000000000000000000000-00f067aa0ba902b7-01")).isEmpty();
|
||||
assertThat(TraceParent.parse(
|
||||
"00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01")).isEmpty();
|
||||
assertThat(TraceParent.parse("x".repeat(10_000))).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsCanonicalNonZeroRootAndNormalizesLegacyHex() {
|
||||
TraceParent root = TraceParent.newRoot("ABCDEF0123456789ABCDEF0123456789");
|
||||
|
||||
assertThat(root.traceId()).isEqualTo("abcdef0123456789abcdef0123456789");
|
||||
assertThat(root.parentId()).matches("[0-9a-f]{16}").isNotEqualTo("0000000000000000");
|
||||
assertThat(root.value()).matches(
|
||||
"00-abcdef0123456789abcdef0123456789-[0-9a-f]{16}-00");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user