springcloud gateway 自定义 accesslog elk

大家好,我是烤鸭:

​ 最近用 springcloud gateway 时,想使用类似 logback-access的功能,用来做数据统计和图表绘制等等,发现没有类似的功能,只能自己开发了。

环境:

        <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency>

整体思路

logback-access.jar 只需要在logback.xml 配置 LogstashAccessTcpSocketAppender 即可完成异步的日志上报。

如果采用相同的方式,考虑到同一个进程里异步上报占性能。(其实是开发太麻烦了)

这里采用的本地日志文件 + elk。

仿照 logback-access ,定义要收集的字段,开发过滤器收集字段,自定义 logstash.yml。

收集到的字段:
“User-Agent” : 请求头字段
“server_ip” :服务器ip
“Content-Length” : 请求参数长度
“request_uri” :请求路径(网关转发的路径)
“host” :本机ip
“client_ip” :请求ip
“method” :get/post
“Host” : 请求头字段
“params” :请求参数
“request_url” :请求全路径
“thread_name” :当前线程
“level” :日志级别
“cost_time” : 请求耗时
“logger_name” :日志类
“Protocol” : 请求头字段

代码实现

LoggingFilter

package com.xxx.gateway.filter;import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.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.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;import java.nio.charset.StandardCharsets;
import java.util.Map;@Slf4j
@Component
public class LoggingFilter implements GlobalFilter, Ordered {private static final String UNKNOWN = "unknown";private static final String METHOD = "method";private static final String PARAMS = "params";private static final String REQUEST_URI = "request_uri";private static final String REQUEST_URL = "request_url";private static final String CLIENT_IP = "client_ip";private static final String SERVER_IP = "server_ip";private static final String HOST = "Host";private static final String COST_TIME = "cost_time";private static final String CID = "cid";private static final String CONTENT_LENGTH = "Content-Length";private static final String PROTOCOL = "Protocol";private static final String REQID = "reqid";private static final String USER_AGENT = "User-Agent";private static final String START_TIME = "gw_start_time";private static final String LOGINFOCOLLECTOR = "logInfoCollector";/*** Process the Web request and (optionally) delegate to the next {@code WebFilter}* through the given {@link GatewayFilterChain}.** @param exchange the current server exchange* @param chain    provides a way to delegate to the next filter* @return {@code Mono<Void>} to indicate when request processing is complete*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();String requestUrl = request.getPath().toString();Map<String, Object> logInfoCollector = Maps.newLinkedHashMap();logInfoCollector.put(CLIENT_IP, getIpAddress(request));logInfoCollector.put(SERVER_IP, request.getURI().getHost());logInfoCollector.put(HOST, getHeaderValue(request, HOST));logInfoCollector.put(METHOD, request.getMethodValue());logInfoCollector.put(REQUEST_URI, request.getURI().getPath());logInfoCollector.put(REQUEST_URL, getRequestUrl(request));logInfoCollector.put(PARAMS, request.getURI().getQuery());logInfoCollector.put(CID, getHeaderValue(request, CID));logInfoCollector.put(CONTENT_LENGTH, request.getHeaders().getContentLength());logInfoCollector.put(PROTOCOL, getHeaderValue(request, PROTOCOL));logInfoCollector.put(REQID, getHeaderValue(request, REQID));logInfoCollector.put(USER_AGENT, getHeaderValue(request, USER_AGENT));exchange.getAttributes().put(START_TIME, System.currentTimeMillis());exchange.getAttributes().put(LOGINFOCOLLECTOR, logInfoCollector);String requestMethod = request.getMethodValue();String contentType = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);String contentLength = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_LENGTH);if (HttpMethod.POST.toString().equals(requestMethod) || HttpMethod.PUT.toString().equals(requestMethod)) {// 根据请求头,用不同的方式解析Bodyif ((Character.DIRECTIONALITY_LEFT_TO_RIGHT + "").equals(contentLength) || StringUtils.isEmpty(contentType)) {MultiValueMap<String, String> getRequestParams = request.getQueryParams();log.info("\n 请求url:`{}` \n 请求类型:{} \n 请求参数:{}", requestUrl, requestMethod, getRequestParams);return chain.filter(exchange);}Mono<DataBuffer> bufferMono = DataBufferUtils.join(exchange.getRequest().getBody());return bufferMono.flatMap(dataBuffer -> {byte[] bytes = new byte[dataBuffer.readableByteCount()];dataBuffer.read(bytes);String postRequestBodyStr = new String(bytes, StandardCharsets.UTF_8);if (contentType.startsWith("multipart/form-data")) {log.info("\n 请求url:`{}` \n 请求类型:{} \n 文件上传", requestMethod);} else {log.info("\n 请求url:`{}` \n 请求类型:{} \n 请求参数:{}", requestMethod, postRequestBodyStr);}// 后续需要用到参数的可以从这个地方获取exchange.getAttributes().put("POST_BODY", postRequestBodyStr);logInfoCollector.put(PARAMS, postRequestBodyStr);DataBufferUtils.release(dataBuffer);Flux<DataBuffer> cachedFlux = Flux.defer(() -> {DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);return Mono.just(buffer);});// 下面的将请求体再次封装写回到request里,传到下一级,否则,由于请求体已被消费,后续的服务将取不到值ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {@Overridepublic Flux<DataBuffer> getBody() {return cachedFlux;}};// 封装request,传给下一级return chain.filter(exchange.mutate().request(mutatedRequest).build()).then(Mono.fromRunnable(() -> {Long startTime = exchange.getAttribute(START_TIME);Map<String, Object> logInfo = exchange.getAttribute(LOGINFOCOLLECTOR);if (startTime != null && !CollectionUtils.isEmpty(logInfo)) {Long executeTime = (System.currentTimeMillis() - startTime);logInfo.put(COST_TIME, executeTime);log.info(JSONObject.toJSONString(logInfo));}}));});} else if (HttpMethod.GET.toString().equals(requestMethod)|| HttpMethod.DELETE.toString().equals(requestMethod)) {return chain.filter(exchange).then(Mono.fromRunnable(() -> {Long startTime = exchange.getAttribute(START_TIME);Map<String, Object> logInfo = exchange.getAttribute(LOGINFOCOLLECTOR);if (startTime != null && !CollectionUtils.isEmpty(logInfo)) {Long executeTime = (System.currentTimeMillis() - startTime);logInfo.put(COST_TIME, executeTime);log.info(JSONObject.toJSONString(logInfo));}}));}return chain.filter(exchange);}public String getIpAddress(ServerHttpRequest request) {HttpHeaders headers = request.getHeaders();String ip = headers.getFirst("x-forwarded-for");if (StringUtils.isNotBlank(ip) && !UNKNOWN.equalsIgnoreCase(ip)) {// 多次反向代理后会有多个ip值,第一个ip才是真实ipif (ip.indexOf(",") != -1) {ip = ip.split(",")[0];}}if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = headers.getFirst("Proxy-Client-IP");}if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = headers.getFirst("WL-Proxy-Client-IP");}if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = headers.getFirst("HTTP_CLIENT_IP");}if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = headers.getFirst("HTTP_X_FORWARDED_FOR");}if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = headers.getFirst("X-Real-IP");}if (StringUtils.isBlank(ip) || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getRemoteAddress().getAddress().getHostAddress();}return ip;}private String getRequestUrl(ServerHttpRequest request) {String url = request.getURI().toString();if (url.contains("?")) {url = url.substring(0, url.indexOf("?"));}return url;}private String getHeaderValue(ServerHttpRequest request, String key) {if (StringUtils.isEmpty(key)) {return "";}HttpHeaders headers = request.getHeaders();if (headers.containsKey(key)) {return headers.get(key).get(0);}return "";}/*** Get the order value of this object.* <p>Higher values are interpreted as lower priority. As a consequence,* the object with the lowest value has the highest priority (somewhat* analogous to Servlet {@code load-on-startup} values).* <p>Same order values will result in arbitrary sort positions for the* affected objects.** @return the order value* @see #HIGHEST_PRECEDENCE* @see #LOWEST_PRECEDENCE*/@Overridepublic int getOrder() {return HIGHEST_PRECEDENCE;}
}

logstash.yml

input {file {path => "D:/data/logs/ccc-gateway/*.log"type => "ccc-gateway"codec => json {charset => "UTF-8"}}
}filter {json {source => "message"skip_on_invalid_json => trueadd_field => { "@accessmes" => "%{message}" } remove_field => [ "@accessmes" ]}
}output {elasticsearch {hosts => "localhost:9200" index => "ccc-gateway_%{+YYYY.MM.dd}"}
}

上面的 logstash.yml 兼容 json和非json格式,loggingFilter 会保证数据打印为json格式,其他的地方log也可以是非json的。

效果如图

accesslog:
在这里插入图片描述

其他的log:
在这里插入图片描述

图表绘制

其实netty 作为容器本身也是有 acesslog的,可以开启。

-Dreactor.netty.http.server.accessLogEnabled=true

AccessLog的log方法直接通过logger输出日志,其日志格式为COMMON_LOG_FORMAT({} - {} [{}] "{} {} {}" {} {} {} {} ms),分别是address, user, zonedDateTime, method, uri, protocol, status, contentLength, port, duration
在这里插入图片描述

没有请求参数和自定义参数(一般链路id放在请求头里的)和响应参数(这次也没加),所以算是对accesslog做了改进。下图是访问量和平均耗时,后续还可以加tp99,请求路径等等

访问量:

在这里插入图片描述

平均耗时:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/412568.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第二阶段团队绩效考核报告

团队绩效考核 基于各种客观问题本次绩效考核采用和第一次冲刺不一样的标准&#xff0c;根据团队贡献事实打分如下 组员打分&#xff1a; 郭良 &#xff08;9.0&#xff09; 赵承龙 &#xff08;5.5&#xff09; &#xff08;根据组内之前定下的打分细则和本期冲刺过程的事实…

[css] 实现单行文本居中和多行文本左对齐并超出显示“...“

[css] 实现单行文本居中和多行文本左对齐并超出显示"…" .one {text-align: center }.multi {overflow: hiddentext-overflow: ellipsisdisplay: -webkit-box-webkit-line-clamp: 3-webkit-box-orient: vertical }可惜多行文本省略, 有严重的兼容性问题个人简介 我…

jenkins发布docker项目 harbor

大家好&#xff0c;我是烤鸭&#xff1a; ​ jenkins 部署k8s 项目还是比较流畅的&#xff0c;本身建立多流水线项目&#xff0c;在项目中添加jenkinsfile就好了&#xff0c;镜像需要额外的参数&#xff0c;还可以添加dokcerfile文件。由于我现在的问题是不能够修改原有的项…

[css] 不使用border画出1px高的线,在不同浏览器的标准和怪异模式下都能保持效果一样

[css] 不使用border画出1px高的线&#xff0c;在不同浏览器的标准和怪异模式下都能保持效果一样 <div style"width: 100%;height: 1px;"></div><hr size"1">个人简介 我是歌谣&#xff0c;欢迎和大家一起交流前后端知识。放弃很容易&am…

bzoj 3173 最长上升子序列

Written with StackEdit. Description 给定一个序列&#xff0c;初始为空。现在我们将\(1\)到\(N\)的数字插入到序列中&#xff0c;每次将一个数字插入到一个特定的位置。每插入一个数字&#xff0c;我们都想知道此时最长上升子序列长度是多少&#xff1f; Input 第一行一个整数…

java 调用linux 脚本并获取返回值

大家好&#xff0c;我是烤鸭&#xff1a; 今天分享下java 调用 shell脚本 并获取返回值。 代码实践 String cmd "df -h"; StringBuffer sb new StringBuffer(); Process process Runtime.getRuntime().exec(cmd); BufferedReader br new BufferedReader(new In…

[css] 写出主流浏览器内核私有属性的css前缀

[css] 写出主流浏览器内核私有属性的css前缀 完善一下&#xff1a; Chrome&#xff1a;Blink内核 -webkit-Safari&#xff1a;WebKit内核 -webkit-Firefox &#xff1a;Gecko内核 -moz-IE&#xff1a;Trident内核 -ms-Opera&#xff1a;Presto内核 …

补充小知识:文件句柄与文件标识符

#文件句柄 这是操作系统里的一个概念&#xff0c;句柄是WINDOWS用来标识被应用程序所建立或使用的对象的唯一整数&#xff0c;WINDOWS使用各种各样的句柄标识诸如应用程序实例&#xff0c;窗口&#xff0c;控制&#xff0c;位图&#xff0c;GDI对象等等。WINDOWS句柄有点象C语言…

[css] 使用flex实现三栏布局,两边固定,中间自适应

[css] 使用flex实现三栏布局&#xff0c;两边固定&#xff0c;中间自适应 同意里面的一个回答&#xff0c;现在有很多简单的实现方式&#xff0c;传统的这个也是一种hack的方式&#xff0c;真的没必要去追究了&#xff0c;但是核心知识点可以掌握。1.把 center 放在最前面&…

saltstack部署java应用失败无日志——CICD 部署

大家好&#xff0c;我是烤鸭&#xff1a; ​   最近在搞公司的CICD&#xff0c;遇到各种问题。复盘总结一下。 CICD 架构 这篇文章写得很详细&#xff0c;可以看一下 https://linux.cn/article-9926-1.html 而这里只是结合现在的情况分析下&#xff1a; CI 持续集成&…

day15 webUI自动化

一、webdriver的原理 driver webdriver.Chrome()创建浏览器&#xff0c;当做我们的服务端&#xff0c;代码就是客户端&#xff0c;和客户端进行ip绑定&#xff0c;基于http协议发送post请求 WebDriver webdriver是按照server – client的经典设计模式设计的。 webdriver的作用…

[css] 浏览器是怎样判断元素是否和某个CSS选择器匹配?

[css] 浏览器是怎样判断元素是否和某个CSS选择器匹配&#xff1f; 有选择器&#xff1a; div.ready #wrapper > .bg-red 先把所有元素 class 中有 bg-red 的元素拿出来组成一个集合&#xff0c;然后上一层&#xff0c;对每一个集合中的元素&#xff0c;如果元素的 parent i…

idea 插件开发 扫描sqlserver

大家好&#xff0c;我是烤鸭&#xff1a; 最近在搞sqlserver 升级 mysql/tidb&#xff0c;发现代码里的sql有很多地方需要改&#xff0c;想着能不能开发一个省点力。 官方的迁移指南&#xff1a; https://www.mysql.com/why-mysql/white-papers/sql-server-to-mysql-zh/ 方案…

VUE之文字跑马灯效果

1.效果演示 2.相关代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><script src"js/vue-2.4.0.js"></script> </head> <body> <div id&…

[css] 用CSS绘制一个三角形

[css] 用CSS绘制一个三角形 .triangle{width: 0;border-bottom: 35px solid lightgreen;border-left: 35px solid transparent;}wrong.triangle{width: 0;border: 35px solid transparent;border-bottom: 35px solid lightgreen; }个人简介 我是歌谣&#xff0c;欢迎和大家一起…

计算机从加电到系统(Linux)启动完成

0x0 背景 在我参加的面试和我面试别人、或者参加别人对别人的面试的事后经常遇到的一个问题就是&#xff1a;请从计算机加电开始描述一下计算机启动到操作系统正式启动起来的全过程。这是一个考验对计算机体系结构和基本知识了解程度的问题。今天也就特别针对这个问题做一个回答…

《少有人走的路——心智成熟的旅程》读书笔记

大家好&#xff0c;我是烤鸭&#xff1a; 《少有人走的路——心智成熟的旅程》&#xff0c;读书笔记。 第一部分 自律 规避问题和逃避痛苦的趋向&#xff0c;是人类心理疾病的根源。(正视自己是最重要的) 自律可以消除人的痛苦&#xff1a;延迟满足、承担责任、尊重事实…

[css] 说下line-height三种赋值方式有何区别?

[css] 说下line-height三种赋值方式有何区别&#xff1f; line-height 可以有带单位及不带单位的写法&#xff08;感觉其实是两种&#xff09;。div{line-height: 24px;line-height: 1.5;line-height: 1.5em;line-height: 150%; }对于应用在单个元素上&#xff0c;这几种写法的…

MySQL数据库select语句的使用方法

select语句可 以用回车分隔$sql"select * from article where id1"和 $sql"select * from article where id1" 都可以得到正确的结果&#xff0c;但有时分开写或许能 更明了一点&#xff0c;特别是当sql语句比较长时。批量查询数据可以用in 来实现 $sql&…

PMP 错题记录

PMP 错题记录 大家好&#xff0c;我是烤鸭&#xff1a; 这次的PMP错题集本来想考前发&#xff0c;临时能看看&#xff0c;还是耽搁了&#xff0c;补发一下吧&#xff0c;不知道以后用不用的上&#xff0c;据说改版了&#xff0c;可能也用不上了。 变更题错题记录 9、一项…