gateway 、 webflux 、reactor-netty请求 日志输出
场景
在使用spring cloud gateway时想要输出请求日志,考虑到两种实现方案
方案一
官网中使用 Reactor Netty Access Logs 方案,配置[-Dreactor.netty.http.server.accessLogEnabled=true]开启日志记录。
输出如下:
reactor.netty.http.server.AccessLog :
10.2.20.177 - - [02/Dec/2020:16:41:57 +0800] "GET /fapi/gw/hi/login HTTP/1.1" 200 319 8080 626 ms
方案二
创建一个logfilter,在logfilter中解析request,并输出请求信息
优点:可以自定义日志格式和内容,可以获取body信息 缺点:返回信息需要再写一个filter,没有匹配到路由时无法进入到logfilter中
思路
对方案一进行改造,使其满足需求。对reactor-netty源码分析,主要涉及
AccessLog :日志工具,日志结构体 AccessLogHandler :http1.1协议日志控制,我们主要使用这个。 AccessLogHandler2 :http2协议日志控制代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
package reactor.netty.http.server; import reactor.util.Logger; import reactor.util.Loggers;
import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Locale; import java.util.Objects;
final class AccessLog { static final Logger log = Loggers.getLogger( "reactor.netty.http.server.AccessLog" ); static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern( "dd/MMM/yyyy:HH:mm:ss Z" , Locale.US); static final String COMMON_LOG_FORMAT = "{} - {} [{}] \"{} {} {}\" {} {} {} {} ms" ; static final String MISSING = "-" ; final String zonedDateTime;
String address; CharSequence method; CharSequence uri; String protocol; String user = MISSING; CharSequence status; long contentLength; boolean chunked; long startTime = System.currentTimeMillis(); int port;
AccessLog() { this .zonedDateTime = ZonedDateTime.now().format(DATE_TIME_FORMATTER); }
AccessLog address(String address) { this .address = Objects.requireNonNull(address, "address" ); return this ; }
AccessLog port( int port) { this .port = port; return this ; }
AccessLog method(CharSequence method) { this .method = Objects.requireNonNull(method, "method" ); return this ; }
AccessLog uri(CharSequence uri) { this .uri = Objects.requireNonNull(uri, "uri" ); return this ; }
AccessLog protocol(String protocol) { this .protocol = Objects.requireNonNull(protocol, "protocol" ); return this ; }
AccessLog status(CharSequence status) { this .status = Objects.requireNonNull(status, "status" ); return this ; }
AccessLog contentLength( long contentLength) { this .contentLength = contentLength; return this ; }
AccessLog increaseContentLength( long contentLength) { if (chunked) { this .contentLength += contentLength; } return this ; }
AccessLog chunked( boolean chunked) { this .chunked = chunked; return this ; }
long duration() { return System.currentTimeMillis() - startTime; }
void log() { if (log.isInfoEnabled()) { log.info(COMMON_LOG_FORMAT, address, user, zonedDateTime, method, uri, protocol, status, (contentLength > - 1 ? contentLength : MISSING), port, duration()); } } } |
AccessLogHandler :日志控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
package reactor.netty.http.server; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpUtil; import io.netty.handler.codec.http.LastHttpContent;
/** * @author Violeta Georgieva */ final class AccessLogHandler extends ChannelDuplexHandler {
AccessLog accessLog = new AccessLog();
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof HttpRequest) { final HttpRequest request = (HttpRequest) msg; final SocketChannel channel = (SocketChannel) ctx.channel();
accessLog = new AccessLog() .address(channel.remoteAddress().getHostString()) .port(channel.localAddress().getPort()) .method(request.method().name()) .uri(request.uri()) .protocol(request.protocolVersion().text()); } ctx.fireChannelRead(msg); }
@Override @SuppressWarnings ( "FutureReturnValueIgnored" ) public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { if (msg instanceof HttpResponse) { final HttpResponse response = (HttpResponse) msg; final HttpResponseStatus status = response.status();
if (status.equals(HttpResponseStatus.CONTINUE)) { //"FutureReturnValueIgnored" this is deliberate ctx.write(msg, promise); return ; }
final boolean chunked = HttpUtil.isTransferEncodingChunked(response); accessLog.status(status.codeAsText()) .chunked(chunked); if (!chunked) { accessLog.contentLength(HttpUtil.getContentLength(response, - 1 )); } } if (msg instanceof LastHttpContent) { accessLog.increaseContentLength(((LastHttpContent) msg).content().readableBytes()); ctx.write(msg, promise.unvoid()) .addListener(future -> { if (future.isSuccess()) { accessLog.log(); } }); return ; } if (msg instanceof ByteBuf) { accessLog.increaseContentLength(((ByteBuf) msg).readableBytes()); } if (msg instanceof ByteBufHolder) { accessLog.increaseContentLength(((ByteBufHolder) msg).content().readableBytes()); } //"FutureReturnValueIgnored" this is deliberate ctx.write(msg, promise); } } |
执行顺序
AccessLogHandler.channelRead > GlobalFilter.filter > AbstractLoadBalance.choose >response.writeWith >AccessLogHandler.write
解决方案
对AccessLog和AccessLogHandler进行重写,输出自己想要的内容和样式。
AccessLogHandler中重写了ChannelDuplexHandler中的channelRead和write方法,还可以对ChannelInboundHandler和ChannelOutboundHandler中的方法进行重写,覆盖请求的整个生命周期。
spring-webflux、gateway、springboot-start-web问题
Spring-webflux
当两者一起时配置的并不是webflux web application, 仍然时一个spring mvc web application。
官方文档中有这么一段注解:
很多开发者添加spring-boot-start-webflux到他们的spring mvc web applicaiton去是为了使用reactive WebClient. 如果希望更改webApplication 类型需要显示的设置,如SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE).
1 2 3 4 5 6 7 8 |
< dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-webflux</ artifactId > </ dependency > < dependency > < groupId >org.springframework.boot</ groupId > < artifactId >spring-boot-starter-web</ artifactId > </ dependency > |
结论一:
当两者一起时配置的并不是webflux web application, 仍然时一个spring mvc web application。但是启动不会报错,可以正常使用,但是webflux功能失效
Spring-gateway
因为gateway和zuul不一样,gateway用的是长连接,netty-webflux,zuul1.0用的就是同步webmvc。
所以你的非gateway子项目启动用的是webmvc,你的gateway启动用的是webflux. spring-boot-start-web和spring-boot-start-webflux相见分外眼红。
不能配置在同一pom.xml,或者不能在同一项目中出现,不然就会启动报错
1 2 3 4 |
< dependency > < groupId >org.springframework.cloud</ groupId > < artifactId >spring-cloud-starter-gateway</ artifactId > </ dependency > |
结论二:
当spring-cloud-gateway和spring-boot-starer-web两者一起时配置的时候, 启动直接报错,依赖包冲突不兼容
以上为个人经验,希望能给大家一个参考,也希望大家多多支持。
原文链接:https://lizz6.blog.csdn.net/article/details/110489737
查看更多关于gateway、webflux、reactor-netty请求日志输出方式的详细内容...