Spring

Spring Cloud Gateway 代理日志记录 Filt

2018-08-11  本文已影响480人  giafei

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


过滤器

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------------
上一篇下一篇

猜你喜欢

热点阅读