有时候客户端会有莫名其妙的问题需要服务端辅助定位,这时候有一份完全的请求的信息的日志会非常有帮助,这里提供一种基于过滤器的实现。代码见: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
Via:1.1 vegur
------------不记录body------------
------------ end at 1533963706808------------