大家好,我是烤鸭:

​ 最近用 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,请求路径等等

访问量:

平均耗时:

springcloud gateway 自定义 accesslog elk相关推荐

  1. drools动态配置规则_微服务实战系列(八)-网关springcloud gateway自定义规则

    1. 场景描述 先说明下项目中使用的网关是:springcloud gateway, 因需要给各个网关服务系统提供自定义配置路由规则,实时生效,不用重启网关(重启风险大),目前已实现:动态加载自定义路 ...

  2. Springboot 集成Springcloud gateway的入门

    最近做项目使用到了springcloude gateway作为网关,因此在此记录下springcloud gateway的入门操作,后续再将源码解读写出来,先立个flag. 回归正题,Springcl ...

  3. 使用springcloud gateway搭建网关(分流,限流,熔断)

    Spring Cloud Gateway Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 ...

  4. springcloud gateway 使用nacos 动态过滤器 记一次线上网关升级cpu升高的问题

    大家好,我是烤鸭: ​ 网关升级,想使用 springcloud gateway nacos 动态过滤器配置(原来是硬编码的方式),升级之后出了一些问题(cpu升高,ygc频繁),记录一下. 关于 s ...

  5. SpringCloud Gateway 集成 oauth2 实现统一认证授权_03

    文章目录 一.网关搭建 1. 引入依赖 2. 配置文件 3. 增加权限管理器 4. 自定义认证接口管理类 5. 增加网关层的安全配置 6. 搭建授权认证中心 二.搭建产品服务 2.1. 创建boot项 ...

  6. SpringCloud Gateway 服务网关,过滤器

    SpringCloud Gateway 过滤器有 pre 和 post 两种方式,请求首先经过 pre 过滤器处理,成功后被转发到对应的服务,然后经过 post 过滤器处理,将结果返回客户端.这种机制 ...

  7. SpringCloud - GateWay服务网关

    文章目录 一. Gateway概述 1.Gateway是什么 2. Gateway作用 3. 微服务架构中网关的位置 4. SpringCloud Gateway概念 5. SpringCloud G ...

  8. SpringCloud gateway (史上最全)

    前言 疯狂创客圈(笔者尼恩创建的高并发研习社群)Springcloud 高并发系列文章,将为大家介绍三个版本的 高并发秒杀: 一.版本1 :springcloud + zookeeper 秒杀 二.版 ...

  9. 微服务网关:SpringCloud Gateway保姆级入门教程

    什么是微服务网关 SpringCloud Gateway是Spring全家桶中一个比较新的项目,Spring社区是这么介绍它的: 该项目借助Spring WebFlux的能力,打造了一个API网关.旨 ...

最新文章

  1. node.js(node.js+mongoose小案例)_实现简单的注册登录退出
  2. 【bzoj3524】Couriers——主席树
  3. Apache24 + wsgi + Flask on Python3.8部署实践
  4. c语言程序连接后扩展名为,C语言程序经过编译、连接后生成的可执行文件的扩展名是...
  5. oracle客户端没有deinstall.bat,Oracle数据库11gR2的卸载 - deinstall
  6. fork join框架_Fork / Join框架vs.并行流vs.ExecutorService:最终的Fork / Join基准
  7. AUTOSAR从入门到精通100讲(三十二)-AutoSar之实时环境RTE
  8. java 判断对象为控制_Java流程控制
  9. 订餐系统jsp模板_java|web|jsp网上订餐系统|餐饮管理|在线点餐外卖网站|源码代码...
  10. linux自己写摄像头驱动,详解linux 摄像头驱动编写
  11. 【非标自动化】2017年的最NB的非标自动化内容都在这了
  12. Eth-Trunk 链路聚合
  13. 【力扣-动态规划入门】【第 4 天】45. 跳跃游戏 II
  14. oracle exadata x7发布,没有对比就没有伤害 QData T5完虐Oracle Exadata X7
  15. 修复cdn服务器连接异,cdn服务器连接异常怎么处理
  16. sudo -s sudo su
  17. 学计算机用商务本还是游戏本,工作学习游戏?这 8 款最具性价比的笔记本电脑,总有一款适合你...
  18. python根据图片网址下载图片
  19. css 超链接样式设置
  20. SCU(System Control Units)

热门文章

  1. [css] 移动页面底部工具条有3个图标,如何平分?在设置边框后最后一个图标掉下去了怎么办?
  2. 工作339:pc父组件通过props传值给子组件,如何避免子组件改变props的属性值报错问题
  3. 前端学习(2512):组件注册
  4. 前端学习(2248)git是怎么运作的
  5. 前端学习(1981)vue之电商管理系统电商系统之完成可选项的添加操作
  6. 前端学习(1801):前端调试之清除浮动练习3
  7. 前端学习(1350):用户的增删改查操作7增删改查
  8. 前端学习(868):dom重点核心
  9. 前端学习(571):margin负值下的两栏自适应
  10. java学习(87):Interage包装类进制转换