gateway、webflux、reactor-netty請(qǐng)求日志輸出方式
gateway、webflux、reactor-netty請(qǐng)求日志輸出
場(chǎng)景
在使用spring cloud gateway時(shí)想要輸出請(qǐng)求日志,考慮到兩種實(shí)現(xiàn)方案
方案一
官網(wǎng)中使用Reactor Netty Access Logs方案,配置“-Dreactor.netty.http.server.accessLogEnabled=true”開(kāi)啟日志記錄。
輸出如下:
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
- 優(yōu)點(diǎn):簡(jiǎn)單方便
- 缺點(diǎn):格式固定,信息量少
方案二
創(chuàng)建一個(gè)logfilter,在logfilter中解析request,并輸出請(qǐng)求信息
- 優(yōu)點(diǎn):可以自定義日志格式和內(nèi)容,可以獲取body信息
- 缺點(diǎn):返回信息需要再寫一個(gè)filter,沒(méi)有匹配到路由時(shí)無(wú)法進(jìn)入到logfilter中
思路
對(duì)方案一進(jìn)行改造,使其滿足需求。對(duì)reactor-netty源碼分析,主要涉及
AccessLog:日志工具,日志結(jié)構(gòu)體AccessLogHandler:http1.1協(xié)議日志控制,我們主要使用這個(gè)。AccessLogHandler2:http2協(xié)議日志控制
代碼如下:
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:日志控制
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);
?? ?}
}執(zhí)行順序
AccessLogHandler.channelRead > GlobalFilter.filter > AbstractLoadBalance.choose >response.writeWith >AccessLogHandler.write
解決方案
對(duì)AccessLog和AccessLogHandler進(jìn)行重寫,輸出自己想要的內(nèi)容和樣式。
AccessLogHandler中重寫了ChannelDuplexHandler中的channelRead和write方法,還可以對(duì)ChannelInboundHandler和ChannelOutboundHandler中的方法進(jìn)行重寫,覆蓋請(qǐng)求的整個(gè)生命周期。
spring-webflux、gateway、springboot-start-web問(wèn)題
Spring-webflux
當(dāng)兩者一起時(shí)配置的并不是webflux web application, 仍然時(shí)一個(gè)spring mvc web application。
官方文檔中有這么一段注解:
很多開(kāi)發(fā)者添加spring-boot-start-webflux到他們的spring mvc web applicaiton去是為了使用reactive WebClient. 如果希望更改webApplication 類型需要顯示的設(shè)置,如SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE).
<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>
結(jié)論一:
當(dāng)兩者一起時(shí)配置的并不是webflux web application, 仍然時(shí)一個(gè)spring mvc web application。但是啟動(dòng)不會(huì)報(bào)錯(cuò),可以正常使用,但是webflux功能失效

Spring-gateway
因?yàn)間ateway和zuul不一樣,gateway用的是長(zhǎng)連接,netty-webflux,zuul1.0用的就是同步webmvc。
所以你的非gateway子項(xiàng)目啟動(dòng)用的是webmvc,你的gateway啟動(dòng)用的是webflux. spring-boot-start-web和spring-boot-start-webflux相見(jiàn)分外眼紅。
不能配置在同一pom.xml,或者不能在同一項(xiàng)目中出現(xiàn),不然就會(huì)啟動(dòng)報(bào)錯(cuò)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
結(jié)論二:
當(dāng)spring-cloud-gateway和spring-boot-starer-web兩者一起時(shí)配置的時(shí)候, 啟動(dòng)直接報(bào)錯(cuò),依賴包沖突不兼容
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Springboot上傳excel并將表格數(shù)據(jù)導(dǎo)入或更新mySql數(shù)據(jù)庫(kù)的過(guò)程
這篇文章主要介紹了Springboot上傳excel并將表格數(shù)據(jù)導(dǎo)入或更新mySql數(shù)據(jù)庫(kù)的過(guò)程 ,本文以Controller開(kāi)始,從導(dǎo)入過(guò)程開(kāi)始講述,其中包括字典表的轉(zhuǎn)換,需要的朋友可以參考下2018-04-04
Spring的編程式事務(wù)TransactionTemplate的用法詳解
TransactionTemplate提供了一種在代碼中進(jìn)行編程式事務(wù)管理的方式,使開(kāi)發(fā)人員能夠在方法級(jí)別定義事務(wù)的開(kāi)始和結(jié)束點(diǎn),本文介紹了Spring框架中TransactionTemplate的用法,感興趣的朋友跟隨小編一起看看吧2023-07-07
Spring?Boot中的@EnableAutoConfiguration注解詳解
這篇文章主要介紹了Spring?Boot中的@EnableAutoConfiguration注解詳解,Spring?Boot是一個(gè)非常流行的Java框架,它可以快速創(chuàng)建基于Spring的應(yīng)用程序。Spring?Boot提供了許多自動(dòng)配置功能,使得開(kāi)發(fā)者可以非常容易地創(chuàng)建一個(gè)可運(yùn)行的應(yīng)用程序,需要的朋友可以參考下2023-08-08
rabbitmq basicReject/basicNack/basicRecover的區(qū)別及說(shuō)明
這篇文章主要介紹了rabbitmq basicReject/basicNack/basicRecover的區(qū)別及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
Springboot項(xiàng)目啟動(dòng)不加載resources目錄下的文件問(wèn)題
這篇文章主要介紹了Springboot項(xiàng)目啟動(dòng)不加載resources目錄下的文件問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08

