task-176: 重试计数与日志字段(IdempotentRetryGuard 观测扩展:WARN 日志字段 + aiimage.http.retry 指标)+ 8 条测试
- 重试/最终失败打 WARN:client/url(去 query)/method/retry/耗时/延迟/原因类名,不打印含敏感信息的异常消息体 - 重试记 aiimage.http.retry 指标(method/client 标签),Micrometer 注册表可选,无注册表静默跳过 - 保留 task-172 原 API(构造器/execute 兼容),原 8 条测试不受影响;守卫仍不接调用点
This commit is contained in:
+207
@@ -0,0 +1,207 @@
|
||||
package com.nanri.aiimage.config;
|
||||
|
||||
import ch.qos.logback.classic.Logger;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import ch.qos.logback.core.read.ListAppender;
|
||||
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* task-176:重试计数与日志字段契约(plan 10,扩展 task-172 守卫的观测面)。
|
||||
*
|
||||
* 守卫每次重试打 WARN 日志(client/url/method/retry/耗时时长/延迟/原因类名),重试记
|
||||
* aiimage.http.retry 指标(Micrometer 可选);成功不记重试日志;日志不打印可能含敏感
|
||||
* 信息的异常消息体。
|
||||
*/
|
||||
class IdempotentRetryGuardObservabilityTest {
|
||||
|
||||
private static ListAppender<ILoggingEvent> attachCapture() {
|
||||
Logger guardLogger = (Logger) LoggerFactory.getLogger(IdempotentRetryGuard.class);
|
||||
ListAppender<ILoggingEvent> appender = new ListAppender<>();
|
||||
appender.start();
|
||||
guardLogger.addAppender(appender);
|
||||
return appender;
|
||||
}
|
||||
|
||||
private static void detachCapture(ListAppender<ILoggingEvent> appender) {
|
||||
Logger guardLogger = (Logger) LoggerFactory.getLogger(IdempotentRetryGuard.class);
|
||||
guardLogger.detachAppender(appender);
|
||||
}
|
||||
|
||||
private static List<String> messages(ListAppender<ILoggingEvent> appender) {
|
||||
return appender.list.stream().map(ILoggingEvent::getFormattedMessage).toList();
|
||||
}
|
||||
|
||||
private static String join(ListAppender<ILoggingEvent> appender) {
|
||||
return String.join("\n", messages(appender));
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryLoggedWithGuardPrefix() throws Exception {
|
||||
HttpClientProperties props = fastProps();
|
||||
IdempotentRetryGuard guard = new IdempotentRetryGuard(props);
|
||||
ListAppender<ILoggingEvent> captured = attachCapture();
|
||||
try {
|
||||
AtomicInteger n = new AtomicInteger();
|
||||
guard.execute("GET", false, "brand", "http://example/brand/check", () -> {
|
||||
if (n.incrementAndGet() < 2) {
|
||||
throw new IOException("连接中断");
|
||||
}
|
||||
return "ok";
|
||||
});
|
||||
assertTrue(join(captured).contains("http-retry-guard"), "重试应打 [http-retry-guard] 日志");
|
||||
} finally {
|
||||
detachCapture(captured);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryLogCarriesClientUrlMethodAndAttemptFields() throws Exception {
|
||||
HttpClientProperties props = fastProps();
|
||||
IdempotentRetryGuard guard = new IdempotentRetryGuard(props);
|
||||
ListAppender<ILoggingEvent> captured = attachCapture();
|
||||
try {
|
||||
AtomicInteger n = new AtomicInteger();
|
||||
guard.execute("GET", false, "brand", "http://example/brand/check", () -> {
|
||||
if (n.incrementAndGet() < 2) {
|
||||
throw new IOException("连接中断");
|
||||
}
|
||||
return "ok";
|
||||
});
|
||||
String text = join(captured);
|
||||
assertTrue(text.contains("client=brand"), "应含 client 字段");
|
||||
assertTrue(text.contains("url=http://example/brand/check"), "应含 url 字段");
|
||||
assertTrue(text.contains("method=GET"), "应含 method 字段");
|
||||
assertTrue(text.contains("retry=1/3"), "应含重试计数与上限字段");
|
||||
} finally {
|
||||
detachCapture(captured);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryLogCarriesDurationAndDelay() throws Exception {
|
||||
HttpClientProperties props = fastProps();
|
||||
IdempotentRetryGuard guard = new IdempotentRetryGuard(props);
|
||||
ListAppender<ILoggingEvent> captured = attachCapture();
|
||||
try {
|
||||
AtomicInteger n = new AtomicInteger();
|
||||
guard.execute("GET", false, () -> {
|
||||
if (n.incrementAndGet() < 2) {
|
||||
throw new IOException("连接中断");
|
||||
}
|
||||
return "ok";
|
||||
});
|
||||
String text = join(captured);
|
||||
assertTrue(text.contains("durationMs="), "应含耗时字段");
|
||||
assertTrue(text.contains("delayMs="), "应含延迟字段");
|
||||
} finally {
|
||||
detachCapture(captured);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryMetricRecorded() throws Exception {
|
||||
SimpleMeterRegistry registry = new SimpleMeterRegistry();
|
||||
HttpClientProperties props = fastProps();
|
||||
IdempotentRetryGuard guard = new IdempotentRetryGuard(props, registry);
|
||||
AtomicInteger n = new AtomicInteger();
|
||||
guard.execute("GET", false, "ziniao", "http://ziniao/x", () -> {
|
||||
if (n.incrementAndGet() < 2) {
|
||||
throw new IOException("连接中断");
|
||||
}
|
||||
return "ok";
|
||||
});
|
||||
assertEquals(1.0, registry.get("aiimage.http.retry").counter().count(), "一次重试应计数 1");
|
||||
assertEquals("GET", registry.get("aiimage.http.retry").counter().getId().getTag("method"));
|
||||
assertEquals("ziniao", registry.get("aiimage.http.retry").counter().getId().getTag("client"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noSensitiveInfoInRetryLog() throws Exception {
|
||||
HttpClientProperties props = fastProps();
|
||||
IdempotentRetryGuard guard = new IdempotentRetryGuard(props);
|
||||
ListAppender<ILoggingEvent> captured = attachCapture();
|
||||
try {
|
||||
AtomicInteger n = new AtomicInteger();
|
||||
guard.execute("POST", true, "brand", "http://example/brand/check?token=secretQuery", () -> {
|
||||
if (n.incrementAndGet() < 2) {
|
||||
throw new IOException("响应含 Bearer secret-token-abc");
|
||||
}
|
||||
return "ok";
|
||||
});
|
||||
String text = join(captured);
|
||||
assertTrue(!text.contains("secret"), "日志不应含异常消息里的敏感串,实际: " + text);
|
||||
assertTrue(!text.contains("Bearer"), "日志不应含 Authorization 字样");
|
||||
} finally {
|
||||
detachCapture(captured);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void successWithoutRetryLogsNothing() throws Exception {
|
||||
HttpClientProperties props = fastProps();
|
||||
IdempotentRetryGuard guard = new IdempotentRetryGuard(props);
|
||||
ListAppender<ILoggingEvent> captured = attachCapture();
|
||||
try {
|
||||
guard.execute("GET", false, "brand", "http://example/brand/check", () -> "ok");
|
||||
assertEquals(0, captured.list.size(), "成功不应产生任何重试日志");
|
||||
} finally {
|
||||
detachCapture(captured);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryLogLevelIsWarn() throws Exception {
|
||||
HttpClientProperties props = fastProps();
|
||||
IdempotentRetryGuard guard = new IdempotentRetryGuard(props);
|
||||
ListAppender<ILoggingEvent> captured = attachCapture();
|
||||
try {
|
||||
AtomicInteger n = new AtomicInteger();
|
||||
guard.execute("GET", false, () -> {
|
||||
if (n.incrementAndGet() < 2) {
|
||||
throw new IOException("连接中断");
|
||||
}
|
||||
return "ok";
|
||||
});
|
||||
assertTrue(!captured.list.isEmpty());
|
||||
assertEquals(Level.WARN.toString(), captured.list.get(0).getLevel().toString(),
|
||||
"重试日志应为 WARN 级别");
|
||||
} finally {
|
||||
detachCapture(captured);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void finalFailureLoggedAfterRetryExhausted() throws Exception {
|
||||
HttpClientProperties props = fastProps();
|
||||
props.setMaxRetries(2);
|
||||
IdempotentRetryGuard guard = new IdempotentRetryGuard(props);
|
||||
ListAppender<ILoggingEvent> captured = attachCapture();
|
||||
try {
|
||||
assertThrows(IOException.class, () -> guard.execute("GET", false, "brand", "http://example/x", () -> {
|
||||
throw new IOException("连接中断");
|
||||
}));
|
||||
String text = join(captured);
|
||||
assertTrue(text.contains("最终失败"), "重试耗尽应记录最终失败,实际: " + text);
|
||||
assertTrue(text.contains("retry=2/2"), "最终失败应携带已重试次数");
|
||||
} finally {
|
||||
detachCapture(captured);
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpClientProperties fastProps() {
|
||||
HttpClientProperties props = new HttpClientProperties();
|
||||
props.setBaseRetryDelayMillis(1);
|
||||
return props;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user