优化
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
| 请求 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 服务传递 |
|
||||
| 可靠事件 HTTP 客户端配置 | `EventDeliveryFeignConfiguration.java`;由业务事件 Feign 客户端显式选择,在最终构建阶段固定连接/读取超时、禁止重定向和客户端内部重试,不作为全局自动配置 |
|
||||
| RestClient Trace 传播 | `RestClientTraceInterceptor.java`、`RestClientTracePropagationAutoConfiguration.java`;Spring 管理的 Builder 自动装配,原始 Builder 可显式复用拦截器 |
|
||||
| 自动配置入口 | `src/main/java/com/baiyue/common/web/CommonWebAutoConfiguration.java`;为只扫描自身包的服务注册异常处理器和 Correlation 过滤器 |
|
||||
| 全局异常映射与业务失败指标 | `src/main/java/com/baiyue/common/web/GlobalExceptionHandler.java`;指标 `baiyue.api.business.failures` 按有限 `code` 标记 |
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import feign.FeignException;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Set;
|
||||
|
||||
/** Explicit failure classification and bounded, credential-free delivery diagnostics. */
|
||||
public class EventDeliveryFailure extends IllegalStateException {
|
||||
private static final Set<String> PERMANENT_CODES = Set.of("A0401", "A0403", "EVT_PROTOCOL");
|
||||
private final boolean permanent;
|
||||
public EventDeliveryFailure(boolean permanent, String message) {
|
||||
super(message);
|
||||
this.permanent = permanent;
|
||||
}
|
||||
public static void requireSuccessful(ApiResponse<?> response) {
|
||||
requireSuccessful(response, Set.of());
|
||||
}
|
||||
public static void requireSuccessful(ApiResponse<?> response, Set<String> additionalPermanentCodes) {
|
||||
if (response == null) throw new EventDeliveryFailure(false, "Consumer returned an empty response");
|
||||
if (!response.successful()) {
|
||||
throw new EventDeliveryFailure(response.code() != null && (PERMANENT_CODES.contains(response.code()) || additionalPermanentCodes.contains(response.code())),
|
||||
"Consumer rejected event: code=" + response.code() + ", message=" + response.message()
|
||||
+ ", requestId=" + response.requestId() + ", traceId=" + response.traceId());
|
||||
}
|
||||
if (response.data() == null) throw new EventDeliveryFailure(false, "Consumer ACK is missing");
|
||||
}
|
||||
public static boolean isPermanent(Throwable failure) {
|
||||
Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
for (Throwable current = failure; current != null && seen.add(current); current = current.getCause()) {
|
||||
if (current instanceof EventDeliveryFailure delivery && delivery.permanent) return true;
|
||||
if (current instanceof FeignException http && (http.status() == 401 || http.status() == 403)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public static String diagnostic(Throwable failure, String internalToken) {
|
||||
Throwable current = failure;
|
||||
Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
while (current.getCause() != null && seen.add(current)
|
||||
&& ("NoFallbackAvailableException".equals(current.getClass().getSimpleName())
|
||||
|| "No fallback available.".equals(current.getMessage()))) current = current.getCause();
|
||||
String message;
|
||||
if (current instanceof FeignException http) message = "HTTP " + http.status();
|
||||
else if (current instanceof JsonProcessingException) message = "Invalid event JSON";
|
||||
else message = current.getMessage() == null ? "unknown" : current.getMessage();
|
||||
String text = current.getClass().getSimpleName() + ": " + message;
|
||||
if (internalToken != null && !internalToken.isBlank()) text = text.replace(internalToken, "***");
|
||||
text = text.replaceAll("(?i)Bearer\\s+[^\\s,;\\\"']+", "Bearer ***")
|
||||
.replaceAll("(?i)((?:X-Internal-Service-Token|Authorization|access[_-]?token|refresh[_-]?token)\\s*[=:]\\s*)[^\\s,;]+", "$1***")
|
||||
.replaceAll("[\\r\\n\\t]+", " ");
|
||||
return text.length() > 500 ? text.substring(0, 500) : text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.baiyue.common.web;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.FeignException;
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class EventDeliveryFailureTest {
|
||||
@Test void onlyExplicitProtocolOrAuthRejectionIsPermanent() {
|
||||
for (String code : new String[]{"EVT_PROTOCOL", "A0401", "A0403"}) {
|
||||
Throwable error = catchThrowable(() -> EventDeliveryFailure.requireSuccessful(response(code)));
|
||||
assertThat(EventDeliveryFailure.isPermanent(error)).as(code).isTrue();
|
||||
}
|
||||
for (String code : new String[]{"EVT_DEPENDENCY", "A0400", "A0409", "A0429", "S0503", "unknown", null}) {
|
||||
Throwable error = catchThrowable(() -> EventDeliveryFailure.requireSuccessful(response(code)));
|
||||
assertThat(error).isInstanceOf(EventDeliveryFailure.class);
|
||||
assertThat(EventDeliveryFailure.isPermanent(error)).as(code).isFalse();
|
||||
}
|
||||
assertThat(EventDeliveryFailure.isPermanent(new IllegalArgumentException("ACK mismatch"))).isFalse();
|
||||
}
|
||||
@Test void httpAuthenticationFailureIsPermanentBut429And503Retry() {
|
||||
for (int status : new int[]{401, 403, 429, 503}) {
|
||||
var http = http(status);
|
||||
assertThat(EventDeliveryFailure.isPermanent(new RuntimeException("wrapper", http)))
|
||||
.isEqualTo(status == 401 || status == 403);
|
||||
assertThat(EventDeliveryFailure.diagnostic(http, "secret")).contains("HTTP " + status)
|
||||
.doesNotContain("remote-body", "secret");
|
||||
}
|
||||
}
|
||||
@Test void diagnosticsRemoveTokenHeadersSecretsAndLineBreaks() {
|
||||
var failure = new IllegalStateException("secret-value\nAuthorization=Bearer abc.def.ghi; X-Internal-Service-Token=another-secret, access_token=third-secret");
|
||||
String value = EventDeliveryFailure.diagnostic(failure, "secret-value");
|
||||
assertThat(value).doesNotContain("secret-value", "abc.def.ghi", "another-secret", "third-secret", "\n");
|
||||
assertThat(EventDeliveryFailure.diagnostic(new RuntimeException("x".repeat(1000)), "")).hasSize(500);
|
||||
}
|
||||
@Test void malformedJsonDiagnosticNeverCopiesPersistedPayload() {
|
||||
Throwable failure = catchThrowable(() -> new ObjectMapper().readTree("private-payload-value"));
|
||||
assertThat(EventDeliveryFailure.diagnostic(failure, ""))
|
||||
.contains("Invalid event JSON").doesNotContain("private-payload-value");
|
||||
}
|
||||
private ApiResponse<Object> response(String code) {
|
||||
return new ApiResponse<>(code, "consumer-message", null, "request-id", "trace-id", Instant.now());
|
||||
}
|
||||
private FeignException http(int status) {
|
||||
var request = Request.create(Request.HttpMethod.POST, "http://consumer/events?token=secret", Map.of(), new byte[0], StandardCharsets.UTF_8, null);
|
||||
return FeignException.errorStatus("receive", Response.builder().request(request).status(status).reason("failure")
|
||||
.headers(Map.of()).body("remote-body-secret", StandardCharsets.UTF_8).build());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user