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”開啟日志記錄。
輸出如下:
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。
官方文檔中有這么一段注解:
很多開發(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)文章
解決Java字符串JSON轉(zhuǎn)換異常:cn.hutool.json.JSONException:?Mismatched?
這篇文章主要給大家介紹了關(guān)于如何解決Java字符串JSON轉(zhuǎn)換異常:cn.hutool.json.JSONException:?Mismatched?hr?and?body的相關(guān)資料,文中將解決的辦法通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01基于Hutool的圖片驗(yàn)證碼功能模塊實(shí)現(xiàn)
為了提高系統(tǒng)的安全性,防止接口被暴力刷新,驗(yàn)證碼是個(gè)好的手段,圖片驗(yàn)證碼沒(méi)有短信驗(yàn)證碼的費(fèi)用,其是個(gè)人開發(fā)者學(xué)習(xí)的重點(diǎn),這篇文章主要介紹了基于Hutool的圖片驗(yàn)證碼功能模塊實(shí)現(xiàn),需要的朋友可以參考下2022-10-10Java實(shí)現(xiàn)樹形菜單的方法總結(jié)
當(dāng)我們想要展示層級(jí)結(jié)構(gòu),如文件目錄、組織結(jié)構(gòu)或分類目錄時(shí),樹形菜單是一個(gè)直觀且有效的解決方案,本文為大家整理了java中幾種常見(jiàn)方法,希望對(duì)大家有所幫助2023-08-08詳解SpringBoot如何實(shí)現(xiàn)統(tǒng)一后端返回格式
在前后端分離的項(xiàng)目中后端返回的格式一定要友好,不然會(huì)對(duì)前端的開發(fā)人員帶來(lái)很多的工作量。那么SpringBoot如何做到統(tǒng)一的后端返回格式呢?本文將為大家詳細(xì)講講2022-04-04Java轉(zhuǎn)換流(InputStreamReader/OutputStreamWriter)的使用
本文主要介紹了Java轉(zhuǎn)換流(InputStreamReader/OutputStreamWriter)的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01mybatis 如何返回list<String>類型數(shù)據(jù)
這篇文章主要介紹了mybatis 如何返回list<String>類型數(shù)據(jù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10Springboot mybatis plus druid多數(shù)據(jù)源解決方案 dynamic-datasource的使用詳
這篇文章主要介紹了Springboot mybatis plus druid多數(shù)據(jù)源解決方案 dynamic-datasource的使用,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11