作为网关,日志记录是必不可少的功能,可以在网关出增加requestId来查询整个请求链的调用执行情况等等。
打印请求日志最重要的就是打印请求参数这些东西,不过RequestBody通常情况下在被读取一次之后就会失效,这样的话,下游的服务就不能正常获取到请求参数了。所以我们需要重写下请求体。
具体方法呢有很多,这里说一下我用的两种:
代码如下:
package com.lifengdi.gateway.filter;
import com.lifengdi.gateway.constant.HeaderConstant;
import com.lifengdi.gateway.constant.OrderedConstant;
import com.lifengdi.gateway.log.Log;
import com.lifengdi.gateway.log.LogHelper;
import com.lifengdi.gateway.utils.GenerateIdUtils;
import com.lifengdi.gateway.utils.IpUtils;
import io.netty.buffer.UnpooledByteBufAllocator;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
/**
* 请求日志打印
*/
@Component
@Slf4j
public class RequestLogFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return OrderedConstant.REQUEST_FILTER;
}
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
long startTime = System.currentTimeMillis();
try {
ServerHttpRequest request = exchange.getRequest();
// 设置X-Request-Id
AtomicReference requestId = new AtomicReference<>(GenerateIdUtils.requestIdWithUUID());
Consumer httpHeadersConsumer = httpHeaders -> {
String headerRequestId = request.getHeaders().getFirst(HeaderConstant.REQUEST_ID);
if (StringUtils.isBlank(headerRequestId)) {
httpHeaders.set(HeaderConstant.REQUEST_ID, requestId.get());
} else {
requestId.set(headerRequestId);
}
httpHeaders.set(HeaderConstant.START_TIME_KEY, String.valueOf(startTime));
};
ServerRequest serverRequest = ServerRequest.create(exchange,
HandlerStrategies.withDefaults().messageReaders());
URI requestUri = request.getURI();
String uriQuery = requestUri.getQuery();
String url = requestUri.getPath() + (StringUtils.isNotBlank(uriQuery) ? "?" + uriQuery : "");
HttpHeaders headers = request.getHeaders();
MediaType mediaType = headers.getContentType();
String method = request.getMethodValue().toUpperCase();
// 原始请求体
final AtomicReference requestBody = new AtomicReference<>();
final AtomicBoolean newBody = new AtomicBoolean(false);
if (Objects.nonNull(mediaType) && LogHelper.isUploadFile(mediaType)) {
requestBody.set("上传文件");
} else {
if (method.equals("GET")) {
if (StringUtils.isNotBlank(uriQuery)) {
requestBody.set(uriQuery);
}
} else {
newBody.set(true);
}
}
final Log logDTO = new Log();
logDTO.setLevel(Log.LEVEL.INFO);
logDTO.setRequestUrl(url);
logDTO.setRequestBody(requestBody.get());
logDTO.setRequestMethod(method);
logDTO.setRequestId(requestId.get());
logDTO.setIp(IpUtils.getClientIp(request));
ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate().headers(httpHeadersConsumer).build();
ServerWebExchange build = exchange.mutate().request(serverHttpRequest).build();
return build.getSession().flatMap(webSession -> {
logDTO.setSessionId(webSession.getId());
if (newBody.get() && headers.getContentLength() > 0) {
Mono bodyToMono = serverRequest.bodyToMono(String.class);
return bodyToMono.flatMap(reqBody -> {
logDTO.setRequestBody(reqBody);
// 重写原始请求
ServerHttpRequestDecorator requestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux getBody() {
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
DataBuffer bodyDataBuffer = nettyDataBufferFactory.wrap(reqBody.getBytes());
return Flux.just(bodyDataBuffer);
}
};
return chain.filter(exchange.mutate()
.request(requestDecorator)
.build()).then(LogHelper.doRecord(logDTO));
});
} else {
return chain.filter(exchange).then(LogHelper.doRecord(logDTO));
}
});
} catch (Exception e) {
log.error("请求日志打印出现异常", e);
return chain.filter(exchange);
}
}
}
上面的核心代码是:
// 重写原始请求
ServerHttpRequestDecorator requestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux getBody() {
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
DataBuffer bodyDataBuffer = nettyDataBufferFactory.wrap(reqBody.getBytes());
return Flux.just(bodyDataBuffer);
}
};
return chain.filter(exchange.mutate()
.request(requestDecorator)
.build()).then(LogHelper.doRecord(logDTO));
如果不需要对session进行操作,可以直接调用这块就行。
关于请求时间,我这里采用的是将时间戳放进请求头中,等到打印日志的时候再从请求头中读取然后计算出时间。否则如果单独在某个filter中计算请求时间,会造成时间不太准确。当然这样时间也不是很准确,毕竟还有Spring本身的filter等业务逻辑,不过时间相差不是很大,大概十几毫秒的样子。
第二种就是自己缓存下请求体,读取的时候读取缓存内容。
代码如下:
package com.lifengdi.gateway.log;
import com.lifengdi.gateway.constant.HeaderConstant;
import com.lifengdi.gateway.utils.IpUtils;
import io.netty.buffer.UnpooledByteBufAllocator;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Objects;
/**
* 对ServerHttpRequest进行二次封装,解决requestBody只能读取一次的问题
* @author: Li Fengdi
* @date: 2020-03-17 18:02
*/
@Slf4j
public class CacheServerHttpRequestDecorator extends ServerHttpRequestDecorator {
private DataBuffer bodyDataBuffer;
private int getBufferTime = 0;
private byte[] bytes;
public CacheServerHttpRequestDecorator(ServerHttpRequest delegate) {
super(delegate);
}
@Override
public Flux getBody() {
if (getBufferTime == 0) {
getBufferTime++;
Flux flux = super.getBody();
return flux.publishOn(Schedulers.single())
.map(this::cache)
.doOnComplete(() -> trace(getDelegate()));
} else {
return Flux.just(getBodyMore());
}
}
private DataBuffer getBodyMore() {
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
bodyDataBuffer = nettyDataBufferFactory.wrap(bytes);
return bodyDataBuffer;
}
private DataBuffer cache(DataBuffer buffer) {
try {
InputStream dataBuffer = buffer.asInputStream();
bytes = IOUtils.toByteArray(dataBuffer);
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
bodyDataBuffer = nettyDataBufferFactory.wrap(bytes);
return bodyDataBuffer;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private void trace(ServerHttpRequest request) {
URI requestUri = request.getURI();
String uriQuery = requestUri.getQuery();
String url = requestUri.getPath() + (StringUtils.isNotBlank(uriQuery) ? "?" + uriQuery : "");
HttpHeaders headers = request.getHeaders();
MediaType mediaType = headers.getContentType();
String schema = requestUri.getScheme();
String method = request.getMethodValue().toUpperCase();
if ((!"http".equals(schema) && !"https".equals(schema))) {
return;
}
String reqBody = null;
if (Objects.nonNull(mediaType) && LogHelper.isUploadFile(mediaType)) {
reqBody = "上传文件";
} else {
if (method.equals("GET")) {
if (StringUtils.isNotBlank(uriQuery)) {
reqBody = uriQuery;
}
} else if (headers.getContentLength() > 0) {
reqBody = LogHelper.readRequestBody(request);
}
}
final Log logDTO = new Log();
logDTO.setLevel(Log.LEVEL.INFO);
logDTO.setRequestUrl(url);
logDTO.setRequestBody(reqBody);
logDTO.setRequestMethod(method);
logDTO.setRequestId(headers.getFirst(HeaderConstant.REQUEST_ID));
logDTO.setIp(IpUtils.getClientIp(request));
log.info(LogHelper.toJsonString(logDTO));
}
}
filter这里就简单写下:
package com.lifengdi.gateway.filter;
import com.lifengdi.gateway.constant.OrderedConstant;
import com.lifengdi.gateway.log.CacheServerHttpRequestDecorator;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author: Li Fengdi
* @date: 2020-03-17 18:17
*/
//@Component
public class LogFilter implements GlobalFilter, Ordered {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
CacheServerHttpRequestDecorator cacheServerHttpRequestDecorator = new CacheServerHttpRequestDecorator(exchange.getRequest());
return chain.filter(exchange.mutate().request(cacheServerHttpRequestDecorator).build());
}
@Override
public int getOrder() {
return OrderedConstant.LOGGING_FILTER;
}
}
工具类也贴下:
package com.lifengdi.gateway.log;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lifengdi.gateway.constant.HeaderConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
public class LogHelper {
private final static ObjectMapper objectMapper = new ObjectMapper();
/**
* Log转JSON
* @param dto Log
* @return JSON字符串
*/
public static String toJsonString(@NonNull Log dto) {
try {
return objectMapper.writeValueAsString(dto);
} catch (JsonProcessingException e) {
log.error("Log转换JSON异常", e);
return null;
}
}
/**
* 根据MediaType获取字符集,如果获取不到,则默认返回UTF_8
* @param mediaType MediaType
* @return Charset
*/
public static Charset getMediaTypeCharset(@Nullable MediaType mediaType) {
if (Objects.nonNull(mediaType) && mediaType.getCharset() != null) {
return mediaType.getCharset();
} else {
return StandardCharsets.UTF_8;
}
}
/**
* 记录日志(后期可扩展为通过MQ将日志发送到ELK系统)
* @param dto Log
* @return Mono.empty()
*/
public static Mono doRecord(Log dto) {
log.info(toJsonString(dto));
return Mono.empty();
}
/**
* 从HttpHeaders获取请求开始时间
*
* 要求请求头中必须要有参数{@link HeaderConstant#START_TIME_KEY},否则将返回当前时间戳
*
* @param headers HttpHeaders请求头
* @return 开始时间时间戳(Mills)
*/
public static long getStartTime(HttpHeaders headers) {
String startTimeStr = headers.getFirst(HeaderConstant.START_TIME_KEY);
return StringUtils.isNotBlank(startTimeStr) ? Long.parseLong(startTimeStr) : System.currentTimeMillis();
}
/**
* 根据HttpHeaders请求头获取请求执行时间
*
* 要求请求头中必须要有参数{@link HeaderConstant#START_TIME_KEY}
*
* @param headers HttpHeaders请求头
* @return 请求执行时间
*/
public static long getHandleTime(HttpHeaders headers) {
String startTimeStr = headers.getFirst(HeaderConstant.START_TIME_KEY);
long startTime = StringUtils.isNotBlank(startTimeStr) ? Long.parseLong(startTimeStr) : System.currentTimeMillis();
return System.currentTimeMillis() - startTime;
}
/**
* 读取请求体内容
* @param request ServerHttpRequest
* @return 请求体
*/
public static String readRequestBody(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
MediaType mediaType = headers.getContentType();
String method = request.getMethodValue().toUpperCase();
if (Objects.nonNull(mediaType) && mediaType.equals(MediaType.MULTIPART_FORM_DATA)) {
return "上传文件";
} else {
if (method.equals("GET")) {
if (!request.getQueryParams().isEmpty()) {
return request.getQueryParams().toString();
}
return null;
} else {
AtomicReference bodyString = new AtomicReference<>();
request.getBody().subscribe(buffer -> {
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
DataBufferUtils.release(buffer);
bodyString.set(new String(bytes, getMediaTypeCharset(mediaType)));
});
return bodyString.get();
}
}
}
/**
* 判断是否是上传文件
* @param mediaType MediaType
* @return Boolean
*/
public static boolean isUploadFile(@Nullable MediaType mediaType) {
if (Objects.isNull(mediaType)) {
return false;
}
return mediaType.equals(MediaType.MULTIPART_FORM_DATA)
|| mediaType.equals(MediaType.IMAGE_GIF)
|| mediaType.equals(MediaType.IMAGE_JPEG)
|| mediaType.equals(MediaType.IMAGE_PNG)
|| mediaType.equals(MediaType.MULTIPART_MIXED);
}
}
响应报文需要在Spring重写了响应体之后才能获取到,所以对filter的执行顺序有要求,需要在NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER
之前执行。代码如下:
package com.lifengdi.gateway.filter;
import com.lifengdi.gateway.constant.HeaderConstant;
import com.lifengdi.gateway.constant.OrderedConstant;
import com.lifengdi.gateway.log.Log;
import com.lifengdi.gateway.log.LogHelper;
import com.lifengdi.gateway.utils.IpUtils;
import io.netty.buffer.UnpooledByteBufAllocator;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
/**
* 请求响应日志打印
*/
@Component
@Slf4j
public class ResponseLogFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return OrderedConstant.LOGGING_FILTER;
}
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
try {
ServerHttpRequest request = exchange.getRequest();
ServerRequest serverRequest = ServerRequest.create(exchange,
HandlerStrategies.withDefaults().messageReaders());
URI requestUri = request.getURI();
String uriQuery = requestUri.getQuery();
HttpHeaders headers = request.getHeaders();
MediaType mediaType = headers.getContentType();
String schema = requestUri.getScheme();
String method = request.getMethodValue().toUpperCase();
// 只记录http、https请求
if ((!"http".equals(schema) && !"https".equals(schema))) {
return chain.filter(exchange);
}
final AtomicReference requestBody = new AtomicReference<>();// 原始请求体
// 排除流文件类型,比如上传的文件contentType.contains("multipart/form-data")
if (Objects.nonNull(mediaType) && LogHelper.isUploadFile(mediaType)) {
requestBody.set("上传文件");
return chain.filter(exchange);
} else {
if (method.equals("GET")) {
if (StringUtils.isNotBlank(uriQuery)) {
requestBody.set(uriQuery);
}
} else if (headers.getContentLength() > 0){
return serverRequest.bodyToMono(String.class).flatMap(reqBody -> {
requestBody.set(reqBody);
// 重写原始请求
ServerHttpRequestDecorator requestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
return httpHeaders;
}
@Override
public Flux getBody() {
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(new UnpooledByteBufAllocator(false));
DataBuffer bodyDataBuffer = nettyDataBufferFactory.wrap(reqBody.getBytes());
return Flux.just(bodyDataBuffer);
// return Flux.just(reqBody).map(bx -> exchange.getRequest().bufferFactory().wrap(bx.getBytes()));
}
};
ServerHttpResponseDecorator responseDecorator = getServerHttpResponseDecorator(exchange,
requestBody);
return chain.filter(exchange.mutate()
.request(requestDecorator)
.response(responseDecorator)
.build());
});
}
ServerHttpResponseDecorator decoratedResponse = getServerHttpResponseDecorator(exchange,
requestBody);
return chain.filter(exchange.mutate()
.response(decoratedResponse)
.build());
}
} catch (Exception e) {
log.error("请求响应日志打印出现异常", e);
return chain.filter(exchange);
}
}
private ServerHttpResponseDecorator getServerHttpResponseDecorator(ServerWebExchange exchange,
AtomicReference requestBody) {
// 获取response的返回数据
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
HttpStatus httpStatus = originalResponse.getStatusCode();
ServerHttpRequest request = exchange.getRequest();
URI requestUri = request.getURI();
String uriQuery = requestUri.getQuery();
String url = requestUri.getPath() + (StringUtils.isNotBlank(uriQuery) ? "?" + uriQuery : "");
HttpHeaders headers = request.getHeaders();
String method = request.getMethodValue().toUpperCase();
String requestId = headers.getFirst(HeaderConstant.REQUEST_ID);
// 封装返回体
return new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono writeWith(Publisher extends DataBuffer> body) {
if (body instanceof Flux) {
Flux extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
DataBuffer join = bufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);
Charset charset = LogHelper.getMediaTypeCharset(originalResponse.getHeaders().getContentType());
String responseBody = new String(content, charset);
long handleTime = LogHelper.getHandleTime(headers);
Log logDTO = new Log(Log.TYPE.RESPONSE);
logDTO.setLevel(Log.LEVEL.INFO);
logDTO.setRequestUrl(url);
logDTO.setRequestBody(requestBody.get());
logDTO.setResponseBody(responseBody);
logDTO.setRequestMethod(method);
if (Objects.nonNull(httpStatus)) {
logDTO.setStatus(httpStatus.value());
}
logDTO.setHandleTime(handleTime);
logDTO.setRequestId(requestId);
logDTO.setIp(IpUtils.getClientIp(request));
exchange.getSession().subscribe(webSession -> {
logDTO.setSessionId(webSession.getId());
});
log.info("url:{},method:{},请求内容:{},响应内容:{},status:{},handleTime:{},requestId:{}",
url, method, requestBody.get(), responseBody, httpStatus,
handleTime, requestId);
log.info(LogHelper.toJsonString(logDTO));
return bufferFactory.wrap(content);
}));
}
return super.writeWith(body);
}
};
}
}
代码已上传到git上,需要的可以去看看。
git代码地址:https://github.com/lifengdi/spring-cloud-gateway-demo
原文地址:https://www.lifengdi.com/archives/article/1778