For a gateway, logging is not optional. Once requests start passing through a central entry point, you usually want a way to trace the full call chain, and adding something like a requestId at the gateway layer makes that much easier.
Logging the request
The key part of request logging is usually the request parameters and body. The problem is that in Spring Cloud Gateway, the RequestBody normally becomes unusable after it has been read once. If you read it directly for logging, downstream services may no longer be able to access the body correctly.
That means the request body has to be rewritten after it is read.
There are many ways to handle this. Here are two practical approaches.
Approach 1
Code:
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);
}
}
}
The core part is this:
// 重写原始请求
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));
If you do not need to operate on the session, this block alone is enough.
For request timing, the approach here is to put the start timestamp into the request headers, then read it back later when writing the log and calculate the duration there. If duration is measured independently inside a single filter, the timing can easily become less accurate.
This still is not perfectly precise, because Spring’s own filters and surrounding business logic also add some overhead. In practice, though, the difference is usually small—roughly a few to a dozen milliseconds.
Approach 2
The second option is to cache the request body yourself and read from the cached content afterward.
Code:
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));
}
}
The filter becomes much simpler:
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;
}
}
The helper class used by both approaches:
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);
}
}
The first approach is more direct: read the body, log it, then rebuild the request so downstream handlers can still consume it.
The second approach wraps ServerHttpRequest and caches the data on first read. After that, any later reads return the cached bytes instead of consuming the original stream again.
Logging the response body
Capturing the response is a little different. You can only access the response body after Spring has rewritten it, so filter ordering matters here.
The filter needs to run before NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER.
Code:
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);
}
};
}
}
A few details matter here:
- Only
httpandhttpsrequests are logged. - Upload and stream-like request bodies are excluded and recorded as
上传文件instead. - For
GET, the query string is used as the request payload in logs. - For non-
GETrequests with a body, the body is read first and then the request is rebuilt. - Response logging is done by decorating
ServerHttpResponseand interceptingwriteWith. - The response bytes are joined, converted to a string using the response charset, logged, and then wrapped again so the client still receives the original content.
This setup allows the gateway to record both request and response details while still preserving the normal reactive request flow. The important part is handling the request body carefully—once for logging, and again for downstream services—without breaking either side.