Monitoring your beer production with Spring Boot Metrics

Let’s take a small break from our Docker/Kubernetes series in which we try to optimize our beer-brewing and -selling throughput and focus a bit more on beer-brewing and -sales measurement.

Think about it! We need to keep our excel sheet managers busy, you don’t want those guys meddling with the overall brewing process!

In this blogpost we’re gonna build a Spring Boot REST Service from scratch using some Apache Camel REST DSL just for the fun of it. The gathering of metrics will be taken care of by Spring Boot’s metrics collector Micrometer. We’ll use Hawtio to browse the metrics of our service and, as an added bonus, we’ll also throw in a SpringFox Swagger UI.

As always the finished code can be found on GitHub here.

Micrometer

Let’s break down the final application starting with the most important part, i.e. Micrometer. Micrometer is Spring Boot’s preferred method of working with operational metrics. Our REST service is a simple service that exposes just one POST operation for processing a new Beer request. To keep it simple, the only thing the service does, is logging the Beer.

Beers can be of type lager or ale and with the help of Micrometer we’ll add counters that monitor the amount of lagers resp. ales posted. Working with Micrometer counters requires two steps. First you need to register the counter on startup and then you need to increment it wherever appropriate.

Maven dependencies

You need the following Maven dependency to make use of Micrometer:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-core</artifactId>
</dependency>

Registering Micrometer counters

The registration part is done in the @SpringBootApplication class:

@SpringBootApplication
@EnableSwagger2
public class Application {

    @Autowired
    private MeterRegistry meterRegistry;

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @EventListener(ApplicationReadyEvent.class)
    public void doSomethingAfterStartup() {
        Counter.builder("beer.orders")
            .tag("type", "ale")
            .description("The number of orders ever placed for Ale beers")
            .register(meterRegistry);

        Counter.builder("beer.orders")
            .tag("type", "lager")
            .description("The number of orders ever placed for Lager beers")
            .register(meterRegistry);
    }

}

The MeterRegistry instance to register the counters is injected into the Application class. After the ApplicationReadyEvent has fired, two counters are registered, i.e. the BeerOrders.type.ale counter for keeping track of ales posted and the BeerOrders.type.lager counter for posted lagers.

Incrementing Micrometer counters

Incrementing the counters is done in the Camel route that implements the POST operation. This metric logic is put in a Camel processor called BeerOrderProcessor:

@Component
public class BeerOrderProcessor implements Processor {

    private final MeterRegistry meterRegistry;

    public BeerOrderProcessor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    @Override
    public void process(Exchange exchange) {
        Beer beer = exchange.getIn().getBody(Beer.class);

        switch (beer.getType()) {
            case LAGER:
                meterRegistry.counter("beer.orders", "type", "lager").increment();
                break;
            case ALE:
                meterRegistry.counter("beer.orders", "type", "ale").increment();
                break;
            default:
        }

    }
}

Again the MeterRegistry is injected and is subsequently used to update one of the counters depending on the type of beer posted. To select the correct counter you need to supply its name (beer.orders) and optional tag(s) (eg. type=lager).

This is all there is to it! Exposing the actual metrics is done via the Hawtio console (see the Hawtio section in this blogpost).

For more information on Micrometer, check out its main documentation page Micrometer Application Monitoring and this Quick Guide to Micrometer.

Camel REST DSL

Exposing the service via REST is done with the help of the Camel REST DSL. For a good tutorial on setting up a REST service with Camel and Spring Boot check out this Apache Camel with Spring Boot guide.

Maven dependencies

You need the following Maven dependency to make use of the REST DSL:

<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-servlet-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-jackson-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-swagger-java-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.camel</groupId>
    <artifactId>camel-spring-boot-starter</artifactId>
</dependency>

The Swagger dependency takes care of automatically generating swagger api docs and exposing it via an endpoint.

REST configuration

The REST configuration can be done programmatically (see the aforementioned guide) but since we’re using Spring Boot, a cleaner way is to do it in the application.yml file:

camel:
    rest:
        context-path: /camel
        enable-cors: true
        api-context-path: /v2/api-docs
        api-property.api:
            title: Beer API with metrics
            version: v1

Most important properties here are the context-path – rest uris will start with it – and the api-context-path – this is where Camel will expose the Swagger docs.

REST DSL

First part of the Camel route is the REST DSL. It ends with a direct endpoint that delegates to the implementation:

rest("/api/").description("Beer Service")
    .id("api-route")
    .post("/beer")
    .produces(MediaType.APPLICATION_JSON)
    .consumes(MediaType.APPLICATION_JSON)
    .bindingMode(RestBindingMode.auto)
    .type(Beer.class)
    .enableCORS(true)
    .to("direct:remote-service");

The resource uri for the POST operation for the service according to our REST configuration and DSL will be camel/api/beer (remember that we set camel as the context path of the service).

The type operation here takes care of the unmarshalling of the input JSON to our Beer POJO.

Implementation

The second part of the route is the implementation of the POST operation.

from("direct:remote-service")
    .routeId("direct-route")
    .process(beerOrderProcessor)
    .log(INFO, "Beer ${body.name} of type ${body.type} posted")
    .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(201))
    .to("mock:post-beer");

The route logs the beer, sends a 201 response and ends in a mock endpoint. The most important part is the beerOrderProcessor, which we already saw takes care of updating the metrics.

SpringFox

SpringFox is a great plugin for generating Swagger documentation and adding a GUI on top.

The Swagger doc in this case will be generated by the camel-swagger-java-starter but we still use SpringFox for generating the UI.

Maven dependencies

We need the following dependencies to add the SpringFox GUI functionality:

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>${springfox-swagger2.version}</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>${springfox-swagger2.version}</version>
</dependency>

Configuration

Enabling the SpringFox Swagger functionality is done by adding just one annotation to the main application:

@SpringBootApplication
@EnableSwagger2
public class Application {
    ….
}

And last but not least, bridging the UI and the Swagger docs is done in the application.yml file:

springfox:
    documentation:
        swagger:
            v2:
                path: /camel/v2/api-docs

After starting up the application, you will have the SpringFox UI available at http://localhost:8080/swagger-ui.html:

Hawtio

With the SpringFox UI we can fire calls at our Beer Service. The Micrometer metrics code takes care of updating our counters. Next up is the addition of the Hawtio console so we can actually verify that our metrics are being updated. As an added bonus, the Hawtio console also gives us some insight into the Camel route we put in our service. For more information check out this Getting Started guide.

Maven dependencies

You need this dependency for the Hawtio inclusion:

<dependency>
    <groupId>io.hawt</groupId>
    <artifactId>hawtio-springboot</artifactId>
    <version>${hawtio-springboot.version}</version>
</dependency>

Configuration

Only one small other piece of configuration is needed for the Hawtio console. The property below, present in the application.yml file, disables the need for authentication when accessing the console.

hawtio:
    authenticationEnabled: false

JMX

We’ve been saving the best for last. This last piece of configuration glues it all together. Here we add the Spring Boot Actuator to the project and expose our metrics over JMX. After putting this last piece of configuration in place all the metrics will be available for viewing via the Hawtio Console. More information can be found on this Spring Boot reference doc.

Maven dependencies

We need the Spring Boot actuator dependency for exposing metrics over actuator endpoints. The micrometer-registry-jmx dependency will be the bridge between the Actuator endpoints and JMX.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-jmx</artifactId>
</dependency>

The great thing about Micrometer is that it can publish its metrics to a lot of different monitoring systems. All that’s needed is putting the corresponding bridge dependency in place and exposing the right management endpoints (see next section). Publishing to Prometheus for example is as easy as publishing to JMX, like we are doing.

Configuration

The place where it really comes together is the application.yml file. Here we configure the Actuator and Micrometer magic:

management:
    endpoints:
        web:
            exposure:
                include: hawtio,jolokia, metrics
    server:
        port: 8912

For Hawtio to work and manage published JMX metrics you need to expose the hawtio and jolokia endpoints. The metrics endpoint will publish OS, JVM as well as application level metrics.

With these settings (also take note of the management.server.port property) the Hawtio console will be available at http://localhost:8912/hawtio after startup (the metrics endpoint will be available at http://localhost:8912/metrics by the way).

Testing

Before firing up the service and checking if everything works, let’s put some attention to unit testing this beasty. We’ll use JUnit version 5 for this. Check out A Guide to JUnit 5 for some great examples.

@SpringBootTest
public class RestApiTest {

    @Autowired
    private ProducerTemplate producerTemplate;

    @Autowired
    private MeterRegistry meterRegistry;

    @EndpointInject(uri = "mock:post-beer")
    private MockEndpoint mockPostBeer;

    private final Beer lager = new Beer("Bavaria", BeerType.LAGER);
    private final Beer ale = new Beer("Bavaria", BeerType.ALE);

    @Test
    @DirtiesContext
    void whenPostDifferentBeerTypesExpectCorrectMetrics() throws InterruptedException {
        // given
        mockPostBeer.expectedMessageCount(5);

        // when
        producerTemplate.sendBody("direct:remote-service", lager);
        producerTemplate.sendBody("direct:remote-service", lager);
        producerTemplate.sendBody("direct:remote-service", ale);
        producerTemplate.sendBody("direct:remote-service", lager);
        producerTemplate.sendBody("direct:remote-service", ale);

        // then
        mockPostBeer.assertIsSatisfied();
        assertEquals(3.0, meterRegistry.get("beer.orders").tag("type", "lager").counter().count());
        assertEquals(2.0, meterRegistry.get("beer.orders").tag("type", "ale").counter().count());
    }
}

This unit test verifies whether the counters are updated to their correct values after posting some lagers and ales. Some pointers:

  • A ProducerTemplate is autowired to post some beers to the camel route;
  • The MeterRegistry is autowired to verify the counters;
  • We inject the Camel MockEndpoint present at the end of the Camel route and use it to verify its interactions (every POST operation should end up with a message on the mock);
  • With the help of the ProducerTemplate we POST 5 beers, three lagers and two ales (note that we can use POJOs here, cause when the route reaches the direct:remote-service endpoint, the unmarshalling from JSON to POJO is already done);
  • We check if 5 messages have arrived on the mock endpoint and we assert that the lager counter has a value of 3 and the ale counter has a value of 2.

Letting the beers flow

Alright. Time to fire up the service and reap the rewards of our hard work. After the service is up and running, fire some beers into it using the SpringFox GUI and check the Hawtio console for the beerOrders metrics:

And there you go! You can clearly see both metrics are there and after POSTing 3 lager beers, you can see the count has been updated accordingly. That’s all there is to it.

Summary

In this blog post we dived into the Micrometer framework to help us publish some much needed metrics regarding our fast expanding beer brewery business. We published the metrics via JMX and used the Hawtio console for monitoring.

In this blogpost we only looked at micrometer counters. There are two other types of metrics available: gauges and timers. A gauge exposes the current value of an object (contrary to counters that can only go up, gauges can also go down). A timer is used to measure latencies or frequency of events.

That’s it for now. See you in our next post. Until then, stay safe and grab a beer!

References

Application code

Spring Boot metrics

Micrometer

Hawtio

Camel Spring Boot

JUnit 5

Spring Boot Actuator