从零搭建Spring Cloud Gateway网关(二)—— 打印请求响应日志

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

从零搭建Spring Cloud Gateway网关(二)—— 打印请求响应日志

李锋镝   2020-03-19 我要评论
作为网关,日志记录是必不可少的功能,可以在网关出增加requestId来查询整个请求链的调用执行情况等等。 # 打印请求日志 打印请求日志最重要的就是打印请求参数这些东西,不过RequestBody通常情况下在被读取一次之后就会失效,这样的话,下游的服务就不能正常获取到请求参数了。所以我们需要重写下请求体。 具体方法呢有很多,这里说一下我用的两种: ## 第一种 代码如下: ```java 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); } } } ``` 上面的核心代码是: ```java // 重写原始请求 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等业务逻辑,不过时间相差不是很大,大概十几毫秒的样子。 ## 第二种 第二种就是自己缓存下请求体,读取的时候读取缓存内容。 代码如下: ```java 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这里就简单写下: ```java 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; } } ``` 工具类也贴下: ```java 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`之前执行。代码如下: ```java 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://github.com/lifengdi/spring-cloud-gateway-demo) 原文地址:[https://www.lifengdi.com/archives/article/1778](https://www.lifengdi.com/archives/article/1778)

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们