+1

HTTP Request/Response logging sử dụng Spring WebFlux

Mở đầu cho chuỗi bài viết về Java và Spring framework, hôm nay mình gửi tới mọi người một tutorial ngắn về cách hiện thực "HTTP Request/Response logging sử dụng Spring Webflux". Rất mong nhận được góp ý từ tất cả mọi người.

Setup Project

Phía dưới đây là cấu trúc project mà mình sẽ sử dụng trong bài viết hôm nay:

|- spring-webflux
    |- src
        |- main
            |- java
                |- io.github.ntduycs.springwebflux
                    |- config
                    |- controller
                        |- UserController.java
                    |- filter
                        |- LoggingFilter.java
                    |- util
                    |- WebFluxApplication.java
            |- resources
    |- pom.xml

Chắc hẳn với các bạn đã từng làm việc với Spring hoặc Java, cấu trúc này không còn quá xa lạ, thậm chí quá đỗi quen thuộc. Vì đó nên mình sẽ không đi quá sâu và việc giới thiệu cấu trúc project ở đây (tuy nhiên nếu các bạn muốn mình làm 1 bài viết giới thiệu về cách cấu trúc source code, đừng ngần ngại comment nhé ^^).

Depedencies

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-test-autoconfigure</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Trên đây là danh sách các dependencies mà mình sẽ sử dụng trong bài viết này, thực ra để có thể bắt đầu với Spring WebFlux, bạn có thể chỉ cần import spring-boot-starter-webflux thôi là đủ rồi. Tuy nhiên, do project này mình sẽ dùng cho các bài viết sau nữa nên có hơi boilerplate một tẹo ^^.

Bắt tay vào việc

Hướng đi

Về cơ bản, cách tiếp cận của mình khi hiện thực HTTP logging là tận dụng Filter (aka. middleware ở 1 số ngôn ngữ khác). Khi server nhận được request từ phía client, chúng sẽ được gửi đi qua 1 loạt các filter trước (và sau) khi được xử lí bởi controller đích. Chúng ta sẽ tạo 1 custom filter để có thể trích xuất thông tin về request và response tương ứng.

Hiện thực Filter

Spring WebFlux cho phép lập trình viên hiện thực các custom filter thông qua việc implements 1 interface có sẵn, đó là WebFilter.

@Slf4j
@Component
public class LoggingFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        return chain.filter(new LoggingWebExchange(exchange));
    }

}

Ở đoạn code phía trên, mình đã thêm 1 custom WebExchange với filter chain của Spring để thực hiện ý đồ logging của mình. Một lưu ý nhỏ đó là chúng ta sẽ cần thông báo với Spring container rằng chúng ta cần nó khởi tạo giúp custom filter này lúc ứng dụng khởi chạy thông qua việc sử dụng annotation @Component.

Hiện thực WebExchange

Nói sơ qua về khái niệm WebExchange trong Spring WebFlux, nó có thể được hiểu nôm na là một công cụ cho phép lập trình viên truy cập vào HTTP request, response cũng như các thuộc tính (properties) server-side trong quá trình xử lý.

static class LoggingWebExchange extends ServerWebExchangeDecorator {
    private final ServerHttpRequest request;
    private final ServerHttpResponse response;

    protected LoggingWebExchange(ServerWebExchange delegate) {
      super(delegate);
      this.request = new RequestLoggingDecorator(delegate.getRequest());
      this.response = new ResponseLoggingDecorator(delegate.getResponse());
    }

    @Override
    public ServerHttpRequest getRequest() {
      return request;
    }

    @Override
    public ServerHttpResponse getResponse() {
      return response;
    }
}

Nếu các bạn từng đọc qua hoặc sử dụng qua Decorator pattern thì ... nó đây. ServerWebExchangeDecorator cho phép chúng ta "wrap" 1 web exchange và qua đó, "add" thêm chức năng, nghiệp vụ.

Cụ thể, trong đoạn code phía trên, mình đã làm việc đó thông qua việc override lại 2 phương thức: getRequest()getResponse() bằng việc một lần nữa ... "wrap" request và response của bằng lần lượt 2 decorator khác là RequestLoggingDecoratorResponseLoggingDecorator =)).

Hiện thực Request Decorator

Với HTTP request, chúng ta có các thông tin quan trọng sau:

  • HTTP method (vd: GET, POST, ...)
  • HTTP headers
  • Request body
  • Request path (URI) (vd: /users, /products, ...)
  • Request query parameters
  • Cookies
  • Local và Remote addresses
  • SSL info

Trong bài viết lần này, mình sẽ tập trung vào các thông tin được in đậm

static class RequestLoggingDecorator extends ServerHttpRequestDecorator {
    private final Flux<DataBuffer> body;

    protected RequestLoggingDecorator(ServerHttpRequest request) {
      super(request);
      var path = request.getPath();
      var method = request.getMethod();
      var query = request.getQueryParams();
      var headers = request.getHeaders();

      log.info("Request: {} {}", method, path);
      log.info("Query: {}", JsonUtils.stringify(query.toSingleValueMap()));
      log.info("Headers: {}", JsonUtils.stringify(headers.toSingleValueMap()));

      this.body = super.getBody().doOnNext(this::logBody);
    }

    @Override
    public Flux<DataBuffer> getBody() {
      return body;
    }

    private void logBody(DataBuffer dataBuffer) {
      log.debug("Request body: {}", dataBuffer.toString(StandardCharsets.UTF_8));
    }
}

Chắc mình không cần giải thích nhiều về đoạn code ở trên nhỉ ^^.

Hiện thực Response Decorator

Tương tự với Request Decorator phía trên, Spring WebFlux cho phép lập trình việc 1 (trong nhiều) cách để truy cập vào HTTP response thông qua việc extends ServerHttpResponseDecorator.

static class ResponseLoggingDecorator extends ServerHttpResponseDecorator {
    public ResponseLoggingDecorator(ServerHttpResponse delegate) {
      super(delegate);
    }

    @Override
    public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
      return super.writeWith(
          Flux.from(body)
              .doOnNext(dataBuffer -> log.debug("Response: {}", dataBuffer.toString(StandardCharsets.UTF_8))));
    }
}

That's it! Ở HTTP response, còn 1 thông tin cũng khá quan trọng, đó là HTTP status trả về, mọi người có thể tùy chỉnh lại log structure để bao gồm thông tin này trong log message thông qua việc gọi tới delegate.getStatusCode().

Chạy thử

Để kiểm tra xem filter của chúng ta đã hoạt động như mong đợi hay chưa, mình sẽ viết 1 controller đơn giản và thực hiện gọi tới nó.

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    @GetMapping
    public Mono<ResponseEntity<ListUserResponse>> listUsers(@Valid ListUserRequest request) {
        return Mono.just(ResponseEntity.ok(new ListUserResponse()));
    }

    @PostMapping
    public Mono<ResponseEntity<Void>> createUser(@RequestBody CreateUserRequest request) {
        return Mono.just(ResponseEntity.noContent().build());
    }
}

OK!, we're good to go now. Cùng chạy thử ứng dụng vừa tạo và tiến hành gọi tới 2 API phía trên nhé.

Khởi chạy ứng dụng

Có nhiều cách để khởi chạy ứng dụng Spring, ở đây mình sử dụng dòng lệnh thông qua lệnh sau

mvn spring-boot run

(Lưu ý: bạn sẽ cần cài đặt sẵn Maven trên máy để có thể thực hiện câu lệnh trên)

Gọi tới API ListUsers

curl 'localhost:9990/users?name=foo'

Quan sát màn hình chạy ứng dụng, bạn sẽ thấy các dòng sau được in ra:

2024-10-19T19:00:06.705+07:00  INFO 32158 --- [spring-webflux] [ctor-http-nio-7] i.g.n.s.filter.LoggingFilter  : Request: GET /users
2024-10-19T19:00:06.706+07:00  INFO 32158 --- [spring-webflux] [ctor-http-nio-7] i.g.n.s.filter.LoggingFilter  : Query: {"name":"foo"}
2024-10-19T19:00:06.706+07:00  INFO 32158 --- [spring-webflux] [ctor-http-nio-7] i.g.n.s.filter.LoggingFilter  : Headers: {"Host":"localhost:9990","User-Agent":"curl/8.7.1","Accept":"*/*"}
2024-10-19T19:00:06.713+07:00 DEBUG 32158 --- [spring-webflux] [ctor-http-nio-7] i.g.n.s.filter.LoggingFilter  : Response: {"records":[]}

Như các bạn đã thấy, ngoài trừ request body, các thông tin được chọn để log đều đã được log ra. Một điểm khá thú vị ở Spring WebFlux khi mình hiện thực cơ chế logging request tương tự trên Spring MVC đó là chúng ta không cần phải thêm bất cứ 1 câu lệnh if..else nào để check xem request body có được gửi cùng hay không 🤭.

Gọi tới API CreateUser

curl -X POST localhost:9990/users --data '{"email": "foo@bar.com", "name": "Foo Bar"}' -H 'Content-Type:application/json'

Quan sát màn hình chạy ứng dụng, bạn sẽ thấy các dòng sau được in ra:

2024-10-19T18:58:22.314+07:00  INFO 32158 --- [spring-webflux] [ctor-http-nio-5] i.g.n.s.filter.LoggingFilter  : Request: POST /users
2024-10-19T18:58:22.315+07:00  INFO 32158 --- [spring-webflux] [ctor-http-nio-5] i.g.n.s.filter.LoggingFilter  : Query: {}
2024-10-19T18:58:22.316+07:00  INFO 32158 --- [spring-webflux] [ctor-http-nio-5] i.g.n.s.filter.LoggingFilter  : Headers: {"Host":"localhost:9990","User-Agent":"curl/8.7.1","Accept":"*/*","Content-Type":"application/json","Content-Length":"43"}
2024-10-19T18:58:22.373+07:00 DEBUG 32158 --- [spring-webflux] [ctor-http-nio-5] i.g.n.s.filter.LoggingFilter  : Request body: {"email":"foo@bar.com","name":"Foo Bar"}

Như các bạn thấy ở đây, request body đã được hiển thị đầy đủ. Và, ... well-done, filter của chúng ta đã hoạt động hoàn hảo 😎.

Kết bài

Như vậy chúng ta vừa cùng nhau đi qua cách hiện thực (ở mức độ đơn giản) 1 filter cho phép log các HTTP request/response đi tới APIs sử dụng Spring WebFlux. Tuy nhiên, vẫn còn 1 số điểm có thể được cải thiện trước khi các bạn có thể mang nó ra để "flex" với đồng nghiệp hoặc áp dụng vào dự án.

  1. Hiện thực cơ chế enable/disable logging: ở các môi trường DEV hoặc SIT, chúng ta sẽ thường cần log ra hầu hết các thông tin trên để thuận tiện cho việc debug. Tuy nhiên, khi launching PROD, việc log tất cả mọi thứ có thể sẽ làm chậm ứng dụng.
  2. Hiện thực cơ chế log masking/redacting: như ở ví dụ phía trên, việc log email của người dùng ở dạng plain text như trên có thể bị xem là illegal và rất dễ bị đội sờ-ciu-ra-ty (securiy) sờ gáy. Chúng ta sẽ cần 1 cơ chế để mask những thông tin này lại.
  3. Điều chỉnh log format: ví dụ phía trên, mình đã log thông tin dưới dạng plain text. Tuy nhiên, hầu hết các công cụ hỗ trợ phân tích và giám sát log ứng dụng hiện nay thường "prefer" sử dụng định dạng JSON.
  4. TBD. 😝

Rất cảm ơn các bạn đã đọc tới đoạn này, hẹn gặp các bạn vào bài viết tiếp theo trong tương lai không xa. Nếu như có bất cứ câu hỏi liên quan tới bài viết này hay yêu cầu về các chủ đề tiếp theo, mọi người đừng ngần ngại để lại comment nhé. Thân chào!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí