有时候客户端会有莫名其妙的问题需要服务端辅助定位,这时候有一份完全的请求的信息的日志会非常有帮助,这里提供一种基于过滤器的实现。代码见: https://github.com/giafei/gateway-request-recorder-starter
package net.giafei.gateway.filter.logger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.*; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.net.URI; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; @Component public class RequestRecorderGlobalFilter implements GlobalFilter, Ordered { private Logger logger = LoggerFactory.getLogger("requestRecorder"); private final static String REQUEST_RECORDER_LOG_BUFFER = "RequestRecorderGlobalFilter.request_recorder_log_buffer"; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest originalRequest = exchange.getRequest(); URI originalRequestUrl = originalRequest.getURI(); //只记录http的请求 String scheme = originalRequestUrl.getScheme(); if ((!"http".equals(scheme) && !"https".equals(scheme))) { return chain.filter(exchange); RecorderServerHttpResponseDecorator response = new RecorderServerHttpResponseDecorator(exchange.getResponse()); ServerWebExchange ex = exchange.mutate() .request(new RecorderServerHttpRequestDecorator(exchange.getRequest())) .response(response) .build(); response.subscribe( Mono.defer(() -> recorderRouteRequest(ex)).then( Mono.defer(() -> recorderResponse(ex)) return recorderOriginalRequest(ex) .then(chain.filter(ex)) .then(); private Mono<Void> writeLog(ServerWebExchange exchange) { StringBuffer logBuffer = exchange.getAttribute(REQUEST_RECORDER_LOG_BUFFER); logBuffer.append("\n------------ end at ") .append(System.currentTimeMillis()) .append("------------\n\n"); logger.info(logBuffer.toString()); return Mono.empty(); private Mono<Void> recorderOriginalRequest(ServerWebExchange exchange) { StringBuffer logBuffer = new StringBuffer("\n------------开始时间 ") .append(System.currentTimeMillis()) .append("------------"); exchange.getAttributes().put(REQUEST_RECORDER_LOG_BUFFER, logBuffer); ServerHttpRequest request = exchange.getRequest(); return recorderRequest(request, request.getURI(), logBuffer.append("\n原始请求:\n")); private Mono<Void> recorderRouteRequest(ServerWebExchange exchange) { URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); StringBuffer logBuffer = exchange.getAttribute(REQUEST_RECORDER_LOG_BUFFER); return recorderRequest(exchange.getRequest(), requestUrl, logBuffer.append("代理请求:\n")); private Mono<Void> recorderRequest(ServerHttpRequest request, URI uri, StringBuffer logBuffer) { if (uri == null) { uri = request.getURI(); HttpMethod method = request.getMethod(); HttpHeaders headers = request.getHeaders(); logBuffer .append(method.toString()).append(' ') .append(uri.toString()).append('\n'); logBuffer.append("------------请求头------------\n"); headers.forEach((name, values) -> { values.forEach(value -> { logBuffer.append(name).append(":").append(value).append('\n'); Charset bodyCharset = null; if (hasBody(method)) { long length = headers.getContentLength(); if (length <= 0) { logBuffer.append("------------无body------------\n"); } else { logBuffer.append("------------body 长度:").append(length).append(" contentType:"); MediaType contentType = headers.getContentType(); if (contentType == null) { logBuffer.append("null,不记录body------------\n"); } else if (!shouldRecordBody(contentType)) { logBuffer.append(contentType.toString()).append(",不记录body------------\n"); } else { bodyCharset = getMediaTypeCharset(contentType); logBuffer.append(contentType.toString()).append("------------\n"); if (bodyCharset != null) { return doRecordBody(logBuffer, request.getBody(), bodyCharset) .then(Mono.defer(() -> { logBuffer.append("\n------------ end ------------\n\n"); return Mono.empty(); } else { logBuffer.append("------------ end ------------\n\n"); return Mono.empty(); private Mono<Void> recorderResponse(ServerWebExchange exchange) { RecorderServerHttpResponseDecorator response = (RecorderServerHttpResponseDecorator)exchange.getResponse(); StringBuffer logBuffer = exchange.getAttribute(REQUEST_RECORDER_LOG_BUFFER); HttpStatus code = response.getStatusCode(); logBuffer.append("响应:").append(code.value()).append(" ").append(code.getReasonPhrase()).append('\n'); HttpHeaders headers = response.getHeaders(); logBuffer.append("------------响应头------------\n"); headers.forEach((name, values) -> { values.forEach(value -> { logBuffer.append(name).append(":").append(value).append('\n'); Charset bodyCharset = null; MediaType contentType = headers.getContentType(); if (contentType == null) { logBuffer.append("------------ contentType = null,不记录body------------\n"); } else if (!shouldRecordBody(contentType)) { logBuffer.append("------------不记录body------------\n"); } else { bodyCharset = getMediaTypeCharset(contentType); logBuffer.append("------------body------------\n"); if (bodyCharset != null) { return doRecordBody(logBuffer, response.copy(), bodyCharset) .then(Mono.defer(() -> writeLog(exchange))); } else { return writeLog(exchange); @Override public int getOrder() { //在GatewayFilter之前执行 return - 1; private boolean hasBody(HttpMethod method) { //只记录这3种谓词的body if (method == HttpMethod.POST || method == HttpMethod.PUT || method == HttpMethod.PATCH) return true; return false; //记录简单的常见的文本类型的request的body和response的body private boolean shouldRecordBody(MediaType contentType) { String type = contentType.getType(); String subType = contentType.getSubtype(); if ("application".equals(type)) { return "json".equals(subType) || "x-www-form-urlencoded".equals(subType) || "xml".equals(subType) || "atom+xml".equals(subType) || "rss+xml".equals(subType); } else if ("text".equals(type)) { return true; //暂时不记录form return false; private Mono<Void> doRecordBody(StringBuffer logBuffer, Flux<DataBuffer> body, Charset charset) { return DataBufferUtils.join(body).doOnNext(buffer -> { CharBuffer charBuffer = charset.decode(buffer.asByteBuffer()); logBuffer.append(charBuffer.toString()); DataBufferUtils.release(buffer); }).then(); private Charset getMediaTypeCharset(@Nullable MediaType mediaType) { if (mediaType != null && mediaType.getCharset() != null) { return mediaType.getCharset(); else { return StandardCharsets.UTF_8;
辅助类 RecorderServerHttpRequestDecorator
package net.giafei.gateway.filter.logger; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.LinkedList; import java.util.List; //解决response的body只能读一次的问题 public class RecorderServerHttpRequestDecorator extends ServerHttpRequestDecorator { private final List<DataBuffer> dataBuffers = new LinkedList<>(); private boolean bufferCached = false; private Mono<Void> progress = null; public RecorderServerHttpRequestDecorator(ServerHttpRequest delegate) { super(delegate); @Override public Flux<DataBuffer> getBody() { synchronized (dataBuffers) { if (bufferCached) return copy(); if (progress == null) { progress = cache(); return progress.thenMany(Flux.defer(this::copy)); private Flux<DataBuffer> copy() { return Flux.fromIterable(dataBuffers) .map(buf -> buf.factory().wrap(buf.asByteBuffer())); private Mono<Void> cache() { return super.getBody() .map(dataBuffers::add) .then(Mono.defer(()-> { bufferCached = true; progress = null; return Mono.empty();
import net.giafei.tools.filter.util.DataBufferUtilFix; import net.giafei.tools.filter.util.DataBufferWrapper; import org.reactivestreams.Publisher; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class RecorderServerHttpResponseDecorator extends ServerHttpResponseDecorator { private DataBufferWrapper data = null; public RecorderServerHttpResponseDecorator(ServerHttpResponse delegate) { super(delegate); @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { return DataBufferUtilFix.join(Flux.from(body)) .doOnNext(d -> this.data = d) .flatMap(d -> super.writeWith(copy())); @Override public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) { return writeWith(Flux.from(body) .flatMapSequential(p -> p)); public Flux<DataBuffer> copy() { //如果data为null 就出错了 正好可以调试 DataBuffer buffer = this.data.newDataBuffer(); if (buffer == null) return Flux.empty(); return Flux.just(buffer);
spring: cloud: gateway: routes: - id: gloabl_filter uri: http://localhost:4101 predicates: - Path=/filter/** filters: - StripPrefix=1 - id: no_filter uri: http://localhost:4101 predicates: - Path=/no-filter/{test} filters: - SetPath=/{test} - IgnoreTestGlobalFilter - id: img uri: http://httpbin.org:80 predicates: - Path=/image/* filters: - IgnoreTestGlobalFilter
------------开始时间 1533963520775------------ 原始请求: GET http://localhost:8080/filter/echo?a=1&b=2 ------------请求头------------ cache-control:no-cache Postman-Token:3ceae0d1-9f3f-42bc-85c1-ebea10950c46 User-Agent:PostmanRuntime/7.2.0 Accept:*/* Host:localhost:8080 accept-encoding:gzip, deflate Connection:keep-alive ------------ end ------------ 代理请求: GET http://localhost:4101/echo?a=1&b=2&throwFilter=true ------------请求头------------ cache-control:no-cache Postman-Token:3ceae0d1-9f3f-42bc-85c1-ebea10950c46 User-Agent:PostmanRuntime/7.2.0 Accept:*/* Host:localhost:8080 accept-encoding:gzip, deflate Connection:keep-alive ------------ end ------------ 响应:200 OK ------------响应头------------ Content-Type:application/json;charset=UTF-8 Date:Sat, 11 Aug 2018 04:58:40 GMT ------------body------------ {"a":["1"],"b":["2"],"throwFilter":["true"]} ------------ end at 1533963520873------------
------------开始时间 1533963577778------------ 原始请求: POST http://localhost:8080/filter/echo?a=1&b=2 ------------请求头------------ Content-Type:application/json cache-control:no-cache Postman-Token:69498eea-4270-4ed7-b374-5e15e760cd10 User-Agent:PostmanRuntime/7.2.0 Accept:*/* Host:localhost:8080 accept-encoding:gzip, deflate content-length:14 Connection:keep-alive ------------body 长度:14 contentType:application/json------------ {"a":1, "b":2} ------------ end ------------ 代理请求: POST http://localhost:4101/echo?a=1&b=2&throwFilter=true ------------请求头------------ Content-Type:application/json cache-control:no-cache Postman-Token:69498eea-4270-4ed7-b374-5e15e760cd10 User-Agent:PostmanRuntime/7.2.0 Accept:*/* Host:localhost:8080 accept-encoding:gzip, deflate content-length:14 Connection:keep-alive ------------body 长度:14 contentType:application/json------------ {"a":1, "b":2} ------------ end ------------ 响应:200 OK ------------响应头------------ Content-Type:text/plain;charset=UTF-8 Content-Length:14 Date:Sat, 11 Aug 2018 04:59:37 GMT ------------body------------ }2:"b" ,1:"a"{ ------------ end at 1533963577796------------
------------开始时间 1533963706176------------ 原始请求: GET http://localhost:8080/image/webp ------------请求头------------ cache-control:no-cache Postman-Token:01562f0b-9f28-4eda-8095-398991f7d537 User-Agent:PostmanRuntime/7.2.0 Accept:*/* Host:localhost:8080 accept-encoding:gzip, deflate Connection:keep-alive ------------ end ------------ 代理请求: GET http://httpbin.org:80/image/webp ------------请求头------------ cache-control:no-cache Postman-Token:01562f0b-9f28-4eda-8095-398991f7d537 User-Agent:PostmanRuntime/7.2.0 Accept:*/* Host:localhost:8080 accept-encoding:gzip, deflate Connection:keep-alive ------------ end ------------ 响应:200 OK ------------响应头------------ Server:gunicorn/19.9.0 Date:Sat, 11 Aug 2018 05:01:44 GMT Content-Type:image/webp Content-Length:10568 Access-Control-Allow-Origin:* Access-Control-Allow-Credentials:true