SpringCloud 微服务中网关如何记录请求响应日志?
来源:JAVA日知录
在基于SpringCloud开发的微服务中,我们一般会选择在网关层记录请求和响应日志,并将其收集到ELK中用作查询和分析。
今天我们就来看看如何实现此功能。
日志实体类首先我们在网关中定义一个日志实体,用于组装日志对象
@Datapublic class AccessLog{
/**用户编号**/ privateLong userId;
/**路由**/ privateString targetServer;
/**协议**/ privateString schema;
/**请求方法名**/ privateString requestMethod;
/**访问地址**/ privateString requestUrl;
/**请求IP**/ privateString clientIp;
/**查询参数**/ private MultiValueMap
String requestBody;
/**请求头**/ private MultiValueMap
String responseBody;
/**响应头**/ private MultiValueMap
HttpStatusCode httpStatusCode;
/**开始请求时间**/ privateLocalDateTime startTime;
/**结束请求时间**/ privateLocalDateTime endTime;
/**执行时长,单位:毫秒**/ privateInteger duration;
}
网关日志过滤器接下来我们在网关中定义一个Filter,用于收集日志信息。
@Componentpublic class AccessLogFilter implements GlobalFilter, Ordered{
private final List
/**
* 打印日志
* @paramaccessLog 网关日志
*/ private void writeAccessLog(AccessLog accessLog){
log.info("----access---- : {}", JsonUtils.obj2StringPretty(accessLog));
}
/**
* 顺序必须是<-1,否则标准的NettyWriteResponseFilter将在您的过滤器得到一个被调用的机会之前发送响应
* 也就是说如果不小于 -1 ,将不会执行获取后端响应的逻辑
* @return */ @Override public int getOrder(){
return -100;
}
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain){
// 将 Request 中可以直接获取到的参数,设置到网关日志ServerHttpRequest request = exchange.getRequest();
AccessLog gatewayLog = newAccessLog();
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));
// 继续 filter 过滤MediaType mediaType = request.getHeaders().getContentType();
if(MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)
|| MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) { // 适合 JSON 和 Form 提交的请求 returnfilterWithRequestBody(exchange, chain, gatewayLog);
}
returnfilterWithoutRequestBody(exchange, chain, gatewayLog);
}
/**
* 没有请求体的请求只需要记录日志
*/ private Mono<Void> filterWithoutRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog accessLog){
// 包装 Response,用于记录 Response BodyServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);
returnchain.filter(exchange.mutate().response(decoratedResponse).build())
.then(Mono.fromRunnable(() -> writeAccessLog(accessLog)));
}
/**
* 需要读取请求体
* 参考 {@linkModifyRequestBodyGatewayFilterFactory} 实现
*/ private Mono<Void> filterWithRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog gatewayLog){
// 设置 Request Body 读取时,设置到网关日志ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body ->{
gatewayLog.setRequestBody(body);
returnMono.just(body);
});
// 通过 BodyInserter 插入 body(支持修改body), 避免 request body 只能获取一次 BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
HttpHeaders headers = newHttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// the new content type will be computed by bodyInserter // and then set in the request decoratorheaders.remove(HttpHeaders.CONTENT_LENGTH);
CachedBodyOutputMessage outputMessage = newCachedBodyOutputMessage(exchange, headers);
// 通过 BodyInserter 将 Request Body 写入到 CachedBodyOutputMessage 中 return bodyInserter.insert(outputMessage, newBodyInserterContext()).then(Mono.defer(() -> {
// 重新封装请求ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);
// 记录响应日志ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);
// 记录普通的 returnchain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build())
.then(Mono.fromRunnable(() -> writeAccessLog(gatewayLog))); // 打印日志}));
}
/**
* 记录响应日志
* 通过 DataBufferFactory 解决响应体分段传输问题。
*/ private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, AccessLog accessLog){
ServerHttpResponse response = exchange.getResponse();
return newServerHttpResponseDecorator(response) {
@Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body){
if (body instanceofFlux) {
DataBufferFactory bufferFactory = response.bufferFactory();
// 计算执行时间accessLog.setEndTime(LocalDateTime.now());
accessLog.setDuration((int) (LocalDateTimeUtil.between(accessLog.getStartTime(),
accessLog.getEndTime()).toMillis()));
accessLog.setResponseHeaders(response.getHeaders());
accessLog.setHttpStatusCode(response.getStatusCode());
// 获取响应类型,如果是 json 就打印String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
if(StrUtil.isNotBlank(originalResponseContentType)
&& originalResponseContentType.contains("application/json")) {
Flux fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
// 设置 response body 到网关日志 byte[] content = readContent(dataBuffers);
String responseResult = newString(content, StandardCharsets.UTF_8);
accessLog.setResponseBody(responseResult);
// 响应 returnbufferFactory.wrap(content);
}));
}
}
// if body is not a flux. never got there. return super.writeWith(body);
}
};
}
/**
* 请求装饰器,支持重新计算 headers、body 缓存
*
* @paramexchange 请求
* @paramheaders 请求头
* @paramoutputMessage body 缓存
* @return请求装饰器
*/ private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage){
return newServerHttpRequestDecorator(exchange.getRequest()) {
@Override public HttpHeaders getHeaders(){
longcontentLength = headers.getContentLength();
HttpHeaders httpHeaders = newHttpHeaders();
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");
}
returnhttpHeaders;
}
@Override public Flux<DataBuffer> getBody(){
returnoutputMessage.getBody();
}
};
}
/**
* 从dataBuffers中读取数据
* @authorjam
* @date2024/5/26 22:31
*/ private byte[] readContent(List dataBuffers) {
// 合并多个流集合,解决返回体分段传输 DataBufferFactory dataBufferFactory = newDefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
// 释放掉内存DataBufferUtils.release(join);
returncontent;
}
}
代码较长建议直接拷贝到编辑器,只要注意下面一个关键点:
getOrder()方法返回的值必须要<-1,否则标准的NettyWriteResponseFilter将在您的过滤器被调用的机会之前发送响应,即不会执行获取后端响应参数的方法
通过上面的两步我们已经可以获取到请求的输入输出参数了,在 writeAccessLog()中将其打印到日志文件,方便通过ELK进行收集。
在实际项目中,网关日志量一般会非常大,不建议使用数据库进行存储。
实际效果服务正常响应

服务异常响应

扫一扫,关注我们