how to log Spring 5 WebClient call

3 min read 06-10-2024
how to log Spring 5 WebClient call


Logging Spring 5 WebClient Calls for Enhanced Debugging

Spring WebClient, a powerful tool for making HTTP requests, often requires insightful logging to pinpoint potential issues. This article guides you through effectively logging your WebClient calls for better debugging and monitoring.

The Scenario: Understanding the Challenge

Imagine you're working on a Spring Boot application that leverages WebClient to fetch data from an external API. During development or in production, you might encounter errors or performance bottlenecks. Without proper logging, pinpointing the root cause becomes a frustrating exercise.

Let's consider a simple example:

@RestController
public class MyController {

  private final WebClient webClient = WebClient.builder().baseUrl("https://api.example.com").build();

  @GetMapping("/data")
  public String getData() {
    return webClient.get()
      .uri("/users")
      .retrieve()
      .bodyToMono(String.class)
      .block();
  }
}

This code uses WebClient to fetch user data from an API endpoint. While functional, it lacks visibility into the underlying HTTP requests.

Leveraging the Power of Logging

Spring Boot's logging framework provides a robust mechanism for capturing detailed information about WebClient calls. To enhance debugging and monitoring, we'll employ the following techniques:

1. Log the Request and Response:

  • Utilize WebClient.mutate().filter(ExchangeFilterFunction.ofRequestProcessor(exchange -> { ... })) to intercept and log the request details.
  • Utilize WebClient.mutate().filter(ExchangeFilterFunction.ofResponseProcessor(exchange -> { ... })) to intercept and log the response details.
@RestController
public class MyController {

  private final WebClient webClient = WebClient.builder()
    .baseUrl("https://api.example.com")
    .filter(ExchangeFilterFunction.ofRequestProcessor(exchange -> {
      log.info("Request: {} {}", exchange.method(), exchange.url());
      return Mono.just(exchange);
    }))
    .filter(ExchangeFilterFunction.ofResponseProcessor(exchange -> {
      log.info("Response: {} {}", exchange.status(), exchange.statusCode());
      return Mono.just(exchange);
    }))
    .build();

  @GetMapping("/data")
  public String getData() {
    return webClient.get()
      .uri("/users")
      .retrieve()
      .bodyToMono(String.class)
      .block();
  }
}

2. Log Request and Response Body:

  • Use exchange.request().bodyToMono(String.class).block() to obtain the request body as a string.
  • Use exchange.response().bodyToMono(String.class).block() to obtain the response body as a string.
@RestController
public class MyController {

  private final WebClient webClient = WebClient.builder()
    .baseUrl("https://api.example.com")
    .filter(ExchangeFilterFunction.ofRequestProcessor(exchange -> {
      log.info("Request: {} {} - Body: {}", exchange.method(), exchange.url(), exchange.request().bodyToMono(String.class).block());
      return Mono.just(exchange);
    }))
    .filter(ExchangeFilterFunction.ofResponseProcessor(exchange -> {
      log.info("Response: {} {} - Body: {}", exchange.status(), exchange.statusCode(), exchange.response().bodyToMono(String.class).block());
      return Mono.just(exchange);
    }))
    .build();

  @GetMapping("/data")
  public String getData() {
    return webClient.get()
      .uri("/users")
      .retrieve()
      .bodyToMono(String.class)
      .block();
  }
}

3. Log Headers:

  • Access request and response headers using exchange.request().headers() and exchange.response().headers().
@RestController
public class MyController {

  private final WebClient webClient = WebClient.builder()
    .baseUrl("https://api.example.com")
    .filter(ExchangeFilterFunction.ofRequestProcessor(exchange -> {
      log.info("Request: {} {} - Headers: {}", exchange.method(), exchange.url(), exchange.request().headers());
      return Mono.just(exchange);
    }))
    .filter(ExchangeFilterFunction.ofResponseProcessor(exchange -> {
      log.info("Response: {} {} - Headers: {}", exchange.status(), exchange.statusCode(), exchange.response().headers());
      return Mono.just(exchange);
    }))
    .build();

  @GetMapping("/data")
  public String getData() {
    return webClient.get()
      .uri("/users")
      .retrieve()
      .bodyToMono(String.class)
      .block();
  }
}

4. Conditional Logging:

  • Use if statements or @ConditionalOnProperty to log only specific calls based on conditions.
  • Example: Log calls only if the request path starts with /sensitive/.
@ConditionalOnProperty(name = "logging.sensitive.enabled", havingValue = "true")
@Component
public class SensitiveRequestLogger implements ExchangeFilterFunction {

  @Override
  public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
    if (request.url().getPath().startsWith("/sensitive/")) {
      log.info("Sensitive Request: {} {}", request.method(), request.url());
    }
    return next.exchange(request);
  }
}

Key Takeaways and Best Practices

  • Use a structured logging format: Employ a standardized logging format (e.g., JSON) for easier parsing and analysis.
  • Avoid logging sensitive information: Mask or redact confidential data within your logs.
  • Enable debug logging selectively: Configure your application to log at the DEBUG level only when necessary to avoid log file bloat.
  • Use logging frameworks effectively: Leverage the power of Spring Boot's logging framework for convenient configuration and control.

Conclusion

Effective logging is crucial for understanding the behavior of your Spring WebClient calls. By applying the techniques outlined above, you can gain valuable insights into the communication between your application and external APIs, facilitating debugging and performance optimization. Remember to adapt these logging practices to your specific needs and prioritize security and performance considerations.