|
@@ -18,6 +18,9 @@ import java.util.Objects;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Feign客户端调用日志输出
|
|
* Feign客户端调用日志输出
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 由 {@link LogAutoConfiguration#feignClient} 包装为 LoadBalancerFeignClient 注册,
|
|
|
|
|
+ * 保留负载均衡能力;关闭开关(log.feign.enable=false)时行为与 Client.Default 完全一致。
|
|
|
*/
|
|
*/
|
|
|
@Slf4j
|
|
@Slf4j
|
|
|
public class FeignDefaultRequestLogging extends Client.Default {
|
|
public class FeignDefaultRequestLogging extends Client.Default {
|
|
@@ -27,6 +30,11 @@ public class FeignDefaultRequestLogging extends Client.Default {
|
|
|
*/
|
|
*/
|
|
|
private static final String LOG_EVENT = "FEIGN";
|
|
private static final String LOG_EVENT = "FEIGN";
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 调用失败时 LogMessage.httpStatus 的占位状态码(无响应可取)
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final int HTTP_STATUS_ERROR = -1;
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 打印日志开关
|
|
* 打印日志开关
|
|
|
*/
|
|
*/
|
|
@@ -51,7 +59,28 @@ public class FeignDefaultRequestLogging extends Client.Default {
|
|
|
|
|
|
|
|
long startTime = System.currentTimeMillis();
|
|
long startTime = System.currentTimeMillis();
|
|
|
|
|
|
|
|
- Response response = super.execute(request, options);
|
|
|
|
|
|
|
+ Response response;
|
|
|
|
|
+ try {
|
|
|
|
|
+ response = super.execute(request, options);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (IOException e) {
|
|
|
|
|
+ // 连接超时/读超时等调用失败也要留痕:状态码置 -1,记录异常摘要后原样抛出
|
|
|
|
|
+ LogMessage logMsg = new LogMessage();
|
|
|
|
|
+ logMsg.setEvent(LOG_EVENT);
|
|
|
|
|
+ logMsg.setMethod(request.httpMethod().name());
|
|
|
|
|
+ logMsg.setRequestUri(request.url());
|
|
|
|
|
+ logMsg.setIp(NetUtil.getLocalhostStr());
|
|
|
|
|
+ logMsg.setReqHeaders(assembleHeaders(request.headers()));
|
|
|
|
|
+ byte[] reqBody = request.body();
|
|
|
|
|
+ if (Objects.nonNull(reqBody)) {
|
|
|
|
|
+ logMsg.setReqBody(buildBodyText(reqBody));
|
|
|
|
|
+ }
|
|
|
|
|
+ logMsg.setHttpStatus(HTTP_STATUS_ERROR);
|
|
|
|
|
+ logMsg.setRespBody("ERROR: " + e.getClass().getSimpleName() + ": " + e.getMessage());
|
|
|
|
|
+ logMsg.setDuration(System.currentTimeMillis() - startTime);
|
|
|
|
|
+ log.info(logMsg.buildJson());
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
//拼接日志
|
|
//拼接日志
|
|
|
LogMessage logMsg = new LogMessage();
|
|
LogMessage logMsg = new LogMessage();
|
|
@@ -63,22 +92,50 @@ public class FeignDefaultRequestLogging extends Client.Default {
|
|
|
|
|
|
|
|
byte[] reqBody = request.body();
|
|
byte[] reqBody = request.body();
|
|
|
if (Objects.nonNull(reqBody)) {
|
|
if (Objects.nonNull(reqBody)) {
|
|
|
- logMsg.setReqBody(new String(reqBody, 0, Math.min(reqBody.length, maxLength), CharsetUtil.CHARSET_UTF_8));
|
|
|
|
|
|
|
+ logMsg.setReqBody(buildBodyText(reqBody));
|
|
|
}
|
|
}
|
|
|
logMsg.setHttpStatus(response.status());
|
|
logMsg.setHttpStatus(response.status());
|
|
|
logMsg.setRespHeaders(assembleHeaders(response.headers()));
|
|
logMsg.setRespHeaders(assembleHeaders(response.headers()));
|
|
|
|
|
|
|
|
|
|
+ // 响应体必须完整读取并回填(业务方依赖完整 body),仅日志字符串截断;
|
|
|
|
|
+ // 读取失败时流已损坏无法恢复,记录告警并继续(调用方会得到读流异常)
|
|
|
Response.Body responseBody = response.body();
|
|
Response.Body responseBody = response.body();
|
|
|
if (responseBody != null) {
|
|
if (responseBody != null) {
|
|
|
- byte[] respBody = IoUtil.readBytes(responseBody.asInputStream());
|
|
|
|
|
- logMsg.setRespBody(new String(respBody, 0, Math.min(respBody.length, maxLength), CharsetUtil.CHARSET_UTF_8));
|
|
|
|
|
- response = response.toBuilder().body(respBody).build();
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ byte[] respBody = readBody(responseBody);
|
|
|
|
|
+ response = response.toBuilder().body(respBody).build();
|
|
|
|
|
+ logMsg.setRespBody(buildBodyText(respBody));
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (IOException e) {
|
|
|
|
|
+ log.warn("FEIGN 响应体读取失败, uri: {}", request.url(), e);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
logMsg.setDuration(System.currentTimeMillis() - startTime);
|
|
logMsg.setDuration(System.currentTimeMillis() - startTime);
|
|
|
- log.info(logMsg.buildMessage());
|
|
|
|
|
|
|
+ log.info(logMsg.buildJson());
|
|
|
return response;
|
|
return response;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 读取响应体:Content-Length 已知时按长度一次性分配,避免 ByteArrayOutputStream 动态扩容放大内存占用
|
|
|
|
|
+ */
|
|
|
|
|
+ private byte[] readBody(Response.Body body) throws IOException {
|
|
|
|
|
+ long length = body.length();
|
|
|
|
|
+ return length > 0 && length <= Integer.MAX_VALUE
|
|
|
|
|
+ ? IoUtil.readBytes(body.asInputStream(), (int) length)
|
|
|
|
|
+ : IoUtil.readBytes(body.asInputStream());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * body 转日志文本:超过 maxLength 截断并追加总长度标记,防止超大响应撑爆日志
|
|
|
|
|
+ */
|
|
|
|
|
+ private String buildBodyText(byte[] body) {
|
|
|
|
|
+ if (body.length > maxLength) {
|
|
|
|
|
+ return new String(body, 0, maxLength, CharsetUtil.CHARSET_UTF_8)
|
|
|
|
|
+ + String.format("...[TRUNCATED,total=%d]", body.length);
|
|
|
|
|
+ }
|
|
|
|
|
+ return new String(body, CharsetUtil.CHARSET_UTF_8);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private String assembleHeaders(Map<String, Collection<String>> headers) {
|
|
private String assembleHeaders(Map<String, Collection<String>> headers) {
|
|
|
if (CollectionUtils.isEmpty(headers)) {
|
|
if (CollectionUtils.isEmpty(headers)) {
|
|
|
return null;
|
|
return null;
|