请求响应日志是日常开发调试定位问题的重要手段,在微服务中引入SpringCloud Gateway后我们希望在网关层统一进行日志的收集。 本节内容将实现以下两个功能: 获取请求的输入输出参数,封装成。..

请求响应日志是日常开发调试定位问题的重要手段,在微服务中引入SpringCloud Gateway后我们希望在网关层统一进行日志的收集。
本节内容将实现以下两个功能:

获取请求的输入输出参数,封装成自定义日志
将日志发送到MongoDB进行存储

一、SpringCloud Gateway日志收集

1.1 获取输入输出参数

首先我们先定义一个日志体

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
  @Data
public class GatewayLog {
    /**访问实例*/
    private String targetServer;
    /**请求路径*/
    private String requestPath;
    /**请求方法*/
    private String requestMethod;
    /**协议 */
    private String schema;
    /**请求体*/
    private String requestBody;
    /**响应体*/
    private String responseData;
    /**请求ip*/
    private String ip;
 /**请求时间*/
    private Date requestTime;
 /**响应时间*/
    private Date responseTime;
    /**执行时间*/
    private long executeTime;
}

`【关键】在网关定义日志过滤器,获取输入输出参数```java
/**
 * 日志过滤器,用于记录日志
 * @author jianzh5
 * @date 2020/3/24 17:17
 */
@Slf4j
@Component
public class AccessLogFilter implements GlobalFilter, Ordered {
    @Autowired
    private AccessLogService accessLogService;

    private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();

    @Override
    public int getOrder() {
        return -100;
    }

    @SuppressWarnings("unchecked")
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

        ServerHttpRequest request = exchange.getRequest();

        // 请求路径
        String requestPath = request.getPath().pathWithinApplication().value();

        Route route = getGatewayRoute(exchange);


        String ipAddress = WebUtils.getServerHttpRequestIpAddress(request);

        GatewayLog gatewayLog = new GatewayLog();
        gatewayLog.setSchema(request.getURI().getScheme());
        gatewayLog.setRequestMethod(request.getMethodValue());
        gatewayLog.setRequestPath(requestPath);
        gatewayLog.setTargetServer(route.getId());
        gatewayLog.setRequestTime(new Date());
        gatewayLog.setIp(ipAddress);

        MediaType mediaType = request.getHeaders().getContentType();

        if(MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)){
            return writeBodyLog(exchange, chain, gatewayLog);
        }else{
            return writeBasicLog(exchange, chain, gatewayLog);
        }

    private Mono<Void> writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog accessLog) {
        StringBuilder builder = new StringBuilder();
        MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
        for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
            builder.append(entry.getKey()).append("=").append(StringUtils.join(entry.getValue(), ","));
        }
        accessLog.setRequestBody(builder.toString());

        //获取响应体
        ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);

        return chain.filter(exchange.mutate().response(decoratedResponse).build())
                .then(Mono.fromRunnable(() -> {
                    // 打印日志
                    writeAccessLog(accessLog);
                }));


    /**
     * 解决 request body 只能读取一次问题,
     * @param exchange
     * @param chain
     * @param gatewayLog
     * @return
     */
    private Mono writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) {
        ServerRequest serverRequest = ServerRequest.create(exchange,messageReaders);

        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
                .flatMap(body ->{
                    gatewayLog.setRequestBody(body);
                    return Mono.just(body);
                });

        // 通过 BodyInserter 插入 body(支持修改body), 避免 request body 只能获取一次
        BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());
        // the new content type will be computed by bodyInserter
        // and then set in the request decorator
        headers.remove(HttpHeaders.CONTENT_LENGTH);

        CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);

        return bodyInserter.insert(outputMessage,new BodyInserterContext())
                .then(Mono.defer(() -> {
                    // 重新封装请求
                    ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);

                    // 记录响应日志
                    ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);

                    // 记录普通的
                    return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build())
                                writeAccessLog(gatewayLog);
                            }));

    /**
     * 打印日志
     * @author javadaily
     * @date 2021/3/24 14:53
     * @param gatewayLog 网关日志
     */
    private void writeAccessLog(GatewayLog gatewayLog) {
        log.info(gatewayLog.toString());  
    }


    private Route getGatewayRoute(ServerWebExchange exchange) {
        return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
    }


    /**
     * 请求装饰器,重新计算 headers
     * @param headers
     * @param outputMessage
     */
    private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers,
                                                       CachedBodyOutputMessage outputMessage) {
        return new ServerHttpRequestDecorator(exchange.getRequest()) {
            public HttpHeaders getHeaders() {
                long contentLength = headers.getContentLength();
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                if (contentLength > 0) {
                    httpHeaders.setContentLength(contentLength);
                } else {
                    // TODO: this causes a 'HTTP/1.1 411 Length Required' // on
                    // httpbin.org
                    httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                }
                return httpHeaders;
            }

            public Flux<DataBuffer> getBody() {
                return outputMessage.getBody();
            }
        };


    /**
     * 记录响应日志
     * 通过 DataBufferFactory 解决响应体分段传输问题。
     */
    private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLog gatewayLog) {
        ServerHttpResponse response = exchange.getResponse();
        DataBufferFactory bufferFactory = response.bufferFactory();

        return new ServerHttpResponseDecorator(response) {
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Date responseTime = new Date();
                    gatewayLog.setResponseTime(responseTime);
                    // 计算执行时间
                    long executeTime = (responseTime.getTime() - gatewayLog.getRequestTime().getTime());

                    gatewayLog.setExecuteTime(executeTime);

                    // 获取响应类型,如果是 json 就打印
                    String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);


                    if (ObjectUtil.equal(this.getStatusCode(), HttpStatus.OK)
                            && StringUtil.isNotBlank(originalResponseContentType)
                            && originalResponseContentType.contains("application/json")) {

                        Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                        return super.writeWith(fluxBody.buffer().map(dataBuffers -> {

                            // 合并多个流集合,解决返回体分段传输
                            DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                            DataBuffer join = dataBufferFactory.join(dataBuffers);
                            byte[] content = new byte[join.readableByteCount()];
                            join.read(content);

                            // 释放掉内存
                            DataBufferUtils.release(join);
                            String responseResult = new String(content, StandardCharsets.UTF_8);


                            gatewayLog.setResponseData(responseResult);

                            return bufferFactory.wrap(content);
                        }));
                // if body is not a flux. never got there.
                return super.writeWith(body);
            }
        };

`代码较长建议直接拷贝到编辑器,只要注意下面一个关键点:```java
getOrder()
1
2
方法返回的值必须要<-1,「否则标准的NettyWriteResponseFilter将在您的过滤器被调用的机会之前发送响应,即不会执行获取后端响应参数的方法」 
通过上面的两步我们已经可以获取到请求的输入输出参数了,在 `writeAccessLog()` 中将其输出到了日志文件,大家可以在Postman发送请求观察日志。

1.2 存储日志

引入MongoDB

1
2
3
4
5
6
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>


由于gateway是基于webflux,所以我们需要选择reactive版本。

在GatewayLog上添加对应的注解

1
2
3
4
  @Data
@Document
    @Id
    private String id;

 …
}

`建立AccessLogRepository```xml

1
2
  @Repository
public interface AccessLogRepository extends ReactiveMongoRepository<GatewayLog,String> {

  
}

`建立Service```xml

1
public interface AccessLogService {
1
2
3
4
    /**
     * 保存AccessLog
     * @param gatewayLog 请求响应日志
     * @return 响应日志

     */

1
    Mono<GatewayLog> saveAccessLog(GatewayLog gatewayLog);

}

`建立实现类```java

1
2
3
  @Service
public class AccessLogServiceImpl implements AccessLogService {
    private AccessLogRepository accessLogRepository;
1
2
    public Mono<GatewayLog> saveAccessLog(GatewayLog gatewayLog) {
        return accessLogRepository.insert(gatewayLog);

    }

`在Nacos配置中心添加MongoDB对应配置```text
spring:
  data:
    mongodb:

1
2
3
4
5
      host: xxx.xx.x.xx
      port: 27017
      database: accesslog
      username: accesslog
      password: xxxxxx


 执行请求,打开MongoDB客户端,查看日志结果  

 ![Test](https://oscimg.oschina.net/oscnet/5d1b13e8-31f4-44f1-8fcb-ece41f90bac6.png  'SpringCloud Alibaba实战二十九 - SpringCloud Gateway 请求响应日志') 
 
 
 以上,希望对你有所帮助! 
 
 
 
 

本文标题: SpringCloudAlibaba实战二十九SpringC

发布时间: 2024年01月28日 00:00

最后更新: 2025年12月30日 08:54

原始链接: https://haoxiang.eu.org/c8d00d53/

版权声明: 本文著作权归作者所有,均采用CC BY-NC-SA 4.0许可协议,转载请注明出处!

× 喜欢就赞赏一下呗!
打赏二维码