美文网首页Spring
Spring Cloud Gateway 代理日志记录 Filt

Spring Cloud Gateway 代理日志记录 Filt

作者: giafei | 来源:发表于2018-08-11 13:03 被阅读480次

    有时候客户端会有莫名其妙的问题需要服务端辅助定位,这时候有一份完全的请求的信息的日志会非常有帮助,这里提供一种基于过滤器的实现。


    过滤器

    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();
                    }));
        }
    }
    
    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;
    
    //解决request的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();
                    }));
        }
    }
    

    路由配置

    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
    Via:1.1 vegur
    ------------不记录body------------
    
    ------------ end at 1533963706808------------
    

    相关文章

      网友评论

      • 7ed3cffb08cf:RecorderServerHttpResponseDecorator 这个类 没有 ,大神
      • King_4ec0:兄弟,你重复贴了两次RecorderServerHttpRequestDecorator类,没有贴RecorderServerHttpResponseDecorator类的代码,麻烦重新编辑一下,谢谢!
        龙喆2025:老铁找到了吗

      本文标题:Spring Cloud Gateway 代理日志记录 Filt

      本文链接:https://www.haomeiwen.com/subject/jocdbftx.html