Adding customized metrics to track message flow

Having added the ability to comment on other people's posted images, it would be nice to start gathering metrics.

To do so, we can introduce metrics similar to those shown in Chapter 5, Developer Tools for Spring Boot Apps, as follows:

    @Controller 
    public class CommentController { 
 
      private final RabbitTemplate rabbitTemplate; 
 
      private final MeterRegistry meterRegistry; 
 
      public CommentController(RabbitTemplate rabbitTemplate, 
       MeterRegistry meterRegistry) { 
         this.rabbitTemplate = rabbitTemplate; 
         this.meterRegistry = meterRegistry; 
      } 
 
      @PostMapping("/comments") 
      public Mono<String> addComment(Mono<Comment> newComment) { 
        return newComment.flatMap(comment -> 
         Mono.fromRunnable(() -> 
          rabbitTemplate 
           .convertAndSend( 
             "learning-spring-boot", 
             "comments.new", 
           comment)) 
            .then(Mono.just(comment))) 
            .log("commentService-publish") 
            .flatMap(comment -> { 
              meterRegistry
.counter("comments.produced", "imageId", comment.getImageId())
.increment(); return Mono.just("redirect:/"); }); } }

This last code has these few changes compared to what we wrote earlier in this chapter:

  • A MeterRegistry is injected through the constructor and captured as a field.
  • It's used to increment a comments.produced metric with every comment.
  • Each metric is also "tagged" with the related imageId.
  • We have to tune the Mono wrapping our rabbitTemplate.convertAndSend(), and ensure that the comment is passed via then(). Then it must be unpacked via flatMap in the part of the flow that writes metrics.
Should the code talking to the meterRegistry also be wrapped in Mono.fromRunnable()? Perhaps. The code blocks when writing, but in this incarnation, the metrics are stored in memory, so the cost is low. Nevertheless, the cost could rise, meaning it should be properly managed. If the service became external, the odds would increase quickly in favor of wrapping with a separate Mono.

In a similar vein, if we inject MeterRegistry into CommentService, we can then use it there as well:

    @RabbitListener(bindings = @QueueBinding( 
      value = @Queue, 
      exchange = @Exchange(value = "learning-spring-boot"), 
      key = "comments.new" 
    )) 
    public void save(Comment newComment) { 
      repository 
       .save(newComment) 
       .log("commentService-save") 
       .subscribe(comment -> { 
         meterRegistry
.counter("comments.consumed", "imageId", comment.getImageId())
.increment(); }); }

This lines up with what we added to CommentController. The preceding code can be explained as follows:

  • Using the injected MeterRegistry, we increment a comments.consumed metric with every comment.
  • It's also tagged with the comment's related imageId.
  • The metrics are handled after the save is completed inside the subscribe method. This method grants us the ability to execute some code once the flow is complete.
Spring AMQP doesn't yet support Reactive Streams. That is why rabbitTemplate.convertAndSend() must be wrapped in Mono.fromRunnable. Blocking calls such as this subscribe() method should be red flags, but in this situation, it's a necessary evil until Spring AMQP is able to add support. There is no other way to signal for this Reactor flow to execute without it.

The thought of relaunching our app and manually entering a slew of comments doesn't sound very exciting. So why not write a simulator to do it for us!

@Profile("simulator")
@Component
public class CommentSimulator {

private final CommentController controller;
private final ImageRepository repository;

private final AtomicInteger counter;

public CommentSimulator(CommentController controller,
ImageRepository repository) {
this.controller = controller;
this.repository = repository;
this.counter = new AtomicInteger(1);
}

@EventListener
public void onApplicationReadyEvent(ApplicationReadyEvent event) {
Flux
.interval(Duration.ofMillis(1000))
.flatMap(tick -> repository.findAll())
.map(image -> {
Comment comment = new Comment();
comment.setImageId(image.getId());
comment.setComment(
"Comment #" + counter.getAndIncrement());
return Mono.just(comment);
})
.flatMap(newComment ->
Mono.defer(() ->
controller.addComment(newComment)))
.subscribe();
}
}

Let's take this simulator apart:

  • The @Profile annotation indicates that this only operates if spring.profiles.active=simulator is present when the app starts
  • The @Component annotation will allow this class to get picked up by Spring Boot automatically and activated
  • The class itself is located in the root package, com.greglturnquist.learningspring, given that it pulls bits from both subpackages
  • The @EventListener annotation signals Spring to pipe application events issued to the app context. In this case, the method is interested in ApplicationReadyEvents, fired when the application is up and operational
  • Flux.interval(Duration.ofMillis(1000)) causes a stream of lazy ticks to get fired every 1000 ms, lazily
  • By flatMapping over this Flux, each tick is transformed into all images using the ImageRepository
  • Each image is used to generate a new, related comment
  • Using the injected CommentController, it simulates the newly minted comment being sent in from the web

If we reconfigure our runner with spring.profiles.active=simulator, we can see it run. IntelliJ IDEA provides the means to set Spring profiles easily:

You can see the entry highlighted at the bottom of the previous screenshot.

If we kick things off after hearing our machine's fan move into high gear, we can check the metrics at http://localhost:8080/application/metrics/comments.consumed and http://localhost:8080/application/metrics/comments.produced, and expect to see tallies.


In this last screenshot, we can clearly see counter.comments.produced and counter.comments.consumed, and they happen to be the same, which means that none were lost.

We can also see the unique image IDs with an equal number of messages spread between them (as expected with our simulator).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.35.194