SpringCloud 微服務(wù)中網(wǎng)關(guān)如何記錄請求響應(yīng)日志?
大家好,我是飄渺。
在基于SpringCloud開發(fā)的微服務(wù)中,我們一般會選擇在網(wǎng)關(guān)層記錄請求和響應(yīng)日志,并將其收集到ELK中用作查詢和分析。
今天我們就來看看如何實(shí)現(xiàn)此功能。
日志實(shí)體類
首先我們在網(wǎng)關(guān)中定義一個日志實(shí)體,用于組裝日志對象;
@Data
public class AccessLog {
/**用戶編號**/
private Long userId;
/**路由**/
private String targetServer;
/**協(xié)議**/
private String schema;
/**請求方法名**/
private String requestMethod;
/**訪問地址**/
private String requestUrl;
/**請求IP**/
private String clientIp;
/**查詢參數(shù)**/
private MultiValueMap<String, String> queryParams;
/**請求體**/
private String requestBody;
/**請求頭**/
private MultiValueMap<String, String> requestHeaders;
/**響應(yīng)體**/
private String responseBody;
/**響應(yīng)頭**/
private MultiValueMap<String, String> responseHeaders;
/**響應(yīng)結(jié)果**/
private HttpStatusCode httpStatusCode;
/**開始請求時間**/
private LocalDateTime startTime;
/**結(jié)束請求時間**/
private LocalDateTime endTime;
/**執(zhí)行時長,單位:毫秒**/
private Integer duration;
}
網(wǎng)關(guān)日志過濾器
接下來我們在網(wǎng)關(guān)中定義一個Filter,用于收集日志信息。
@Component
public class AccessLogFilter implements GlobalFilter, Ordered {
private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
/**
* 打印日志
* @param accessLog 網(wǎng)關(guān)日志
*/
private void writeAccessLog(AccessLog accessLog) {
log.info("----access---- : {}", JsonUtils.obj2StringPretty(accessLog));
}
/**
* 順序必須是<-1,否則標(biāo)準(zhǔn)的NettyWriteResponseFilter將在您的過濾器得到一個被調(diào)用的機(jī)會之前發(fā)送響應(yīng)
* 也就是說如果不小于 -1 ,將不會執(zhí)行獲取后端響應(yīng)的邏輯
* @return
*/
@Override
public int getOrder() {
return -100;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 將 Request 中可以直接獲取到的參數(shù),設(shè)置到網(wǎng)關(guān)日志
ServerHttpRequest request = exchange.getRequest();
AccessLog gatewayLog = new AccessLog();
gatewayLog.setTargetServer(WebUtils.getGatewayRoute(exchange).getId());
gatewayLog.setSchema(request.getURI().getScheme());
gatewayLog.setRequestMethod(request.getMethod().name());
gatewayLog.setRequestUrl(request.getURI().getRawPath());
gatewayLog.setQueryParams(request.getQueryParams());
gatewayLog.setRequestHeaders(request.getHeaders());
gatewayLog.setStartTime(LocalDateTime.now());
gatewayLog.setClientIp(WebUtils.getClientIP(exchange));
// 繼續(xù) filter 過濾
MediaType mediaType = request.getHeaders().getContentType();
if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)
|| MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) { // 適合 JSON 和 Form 提交的請求
return filterWithRequestBody(exchange, chain, gatewayLog);
}
return filterWithoutRequestBody(exchange, chain, gatewayLog);
}
/**
* 沒有請求體的請求只需要記錄日志
*/
private Mono<Void> filterWithoutRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog accessLog) {
// 包裝 Response,用于記錄 Response Body
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);
return chain.filter(exchange.mutate().response(decoratedResponse).build())
.then(Mono.fromRunnable(() -> writeAccessLog(accessLog)));
}
/**
* 需要讀取請求體
* 參考 {@link ModifyRequestBodyGatewayFilterFactory} 實(shí)現(xiàn)
*/
private Mono<Void> filterWithRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog gatewayLog) {
// 設(shè)置 Request Body 讀取時,設(shè)置到網(wǎng)關(guān)日志
ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
gatewayLog.setRequestBody(body);
return Mono.just(body);
});
// 通過 BodyInserter 插入 body(支持修改body), 避免 request body 只能獲取一次
BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// the new content type will be computed by bodyInserter
// and then set in the request decorator
headers.remove(HttpHeaders.CONTENT_LENGTH);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
// 通過 BodyInserter 將 Request Body 寫入到 CachedBodyOutputMessage 中
return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
// 重新封裝請求
ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);
// 記錄響應(yīng)日志
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);
// 記錄普通的
return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build())
.then(Mono.fromRunnable(() -> writeAccessLog(gatewayLog))); // 打印日志
}));
}
/**
* 記錄響應(yīng)日志
* 通過 DataBufferFactory 解決響應(yīng)體分段傳輸問題。
*/
private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, AccessLog accessLog) {
ServerHttpResponse response = exchange.getResponse();
return new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
DataBufferFactory bufferFactory = response.bufferFactory();
// 計(jì)算執(zhí)行時間
accessLog.setEndTime(LocalDateTime.now());
accessLog.setDuration((int) (LocalDateTimeUtil.between(accessLog.getStartTime(),
accessLog.getEndTime()).toMillis()));
accessLog.setResponseHeaders(response.getHeaders());
accessLog.setHttpStatusCode(response.getStatusCode());
// 獲取響應(yīng)類型,如果是 json 就打印
String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
if (StrUtil.isNotBlank(originalResponseContentType)
&& originalResponseContentType.contains("application/json")) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
// 設(shè)置 response body 到網(wǎng)關(guān)日志
byte[] content = readContent(dataBuffers);
String responseResult = new String(content, StandardCharsets.UTF_8);
accessLog.setResponseBody(responseResult);
// 響應(yīng)
return bufferFactory.wrap(content);
}));
}
}
// if body is not a flux. never got there.
return super.writeWith(body);
}
};
}
/**
* 請求裝飾器,支持重新計(jì)算 headers、body 緩存
*
* @param exchange 請求
* @param headers 請求頭
* @param outputMessage body 緩存
* @return 請求裝飾器
*/
private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
// TODO: this causes a 'HTTP/1.1 411 Length Required' // on
// httpbin.org
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
}
/**
* 從dataBuffers中讀取數(shù)據(jù)
* @author jam
* @date 2024/5/26 22:31
*/
private byte[] readContent(List<? extends DataBuffer> dataBuffers) {
// 合并多個流集合,解決返回體分段傳輸
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
// 釋放掉內(nèi)存
DataBufferUtils.release(join);
return content;
}
}
代碼較長建議直接拷貝到編輯器,只要注意下面一個關(guān)鍵點(diǎn):
getOrder()方法返回的值必須要<-1,否則標(biāo)準(zhǔn)的NettyWriteResponseFilter將在您的過濾器被調(diào)用的機(jī)會之前發(fā)送響應(yīng),即不會執(zhí)行獲取后端響應(yīng)參數(shù)的方法。
通過上面的兩步我們已經(jīng)可以獲取到請求的輸入輸出參數(shù)了,在 writeAccessLog()中將其打印到日志文件,方便通過ELK進(jìn)行收集。
在實(shí)際項(xiàng)目中,網(wǎng)關(guān)日志量一般會非常大,不建議使用數(shù)據(jù)庫進(jìn)行存儲。
實(shí)際效果
服務(wù)正常響應(yīng):
服務(wù)異常響應(yīng):
圖片