This commit is contained in:
2026-08-25 12:30:07 +08:00
parent 12dad6673d
commit 1382902f86
16 changed files with 687 additions and 19 deletions
@@ -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;
}
}