Quarkus, MicroProfile and the wonderful world of metrics

February 2022
November 2020
TLDR: MicroProfile metrics are awesome and useful, here is the documentation.

Those who have already had the pleasure of developing an application with Quarkus are probably aware of its benefits when collecting metrics. The MicroProfile specification offers a range of possibilities for this, which I will briefly introduce here.

However, this article by no means covers all possibilities and aspects concerning MicroProfile metrics and serves rather as an introduction with useful examples. While it is intended for Quarkus, it should apply to other MicroProfile implementations as well. I will not go into the storage and display of metrics with e.g. Prometheus or Grafana here.

Update to MicroProfile 3.3

Quarkus (since version 1.3) now also supports the MicroProfile 3.3 specification, which includes MicroProfile metrics in version 2.3.

MicroProfile metrics – An Overview

The metrics in the MicroProfile – like many specifications in the MicroProfile – are designed for simple applications. Metrics are divided into three areas (scopes):  "base", "vendor" and "application" metrics.

"Base" metrics must be created for every MicroProfile implementation. "Vendor" metrics are intended for additional metrics of each implementation and "application" for the application itself.

Since I intend to discuss the possibilities within a Quarkus project here, I will limit myself to the "application" metrics.

More information about the specification can be found here for the current version 2.3 supported by Quarkus:

specification

See a brief introduction with examples of use in Quarkus:

introduction

MicroProfile metrics – An Example

Let’s begin with a minimal code example. This provides a REST interface that sorts a passed list of integers:

     
    @GET
    @Path("/test")
    @Produces({ "text/plain" })
    public Response getRandom(@QueryParam("numbers") List<Integer> values)
    { 
        ArrayList<Integer> numbers = new ArrayList<Integer>(values); 
        Collections.sort(numbers);
        return Response.status(Status.OK).entity(numbers).build(); 
    }
    

Annotated metrics

The easiest way to add metrics to the desired classes, methods or fields is to use annotation. Let’s have a look at some simple metrics.

Annotated metrics – Counter

If we want to know how often our resource is called, all we need is the following:

     
    @GET
    @Path("/test")
    @Produces({ "text/plain" })
    @Counted
    public Response getRandom(@QueryParam("numbers") List<Integer> values) 
    { 
        ArrayList<Integer> numbers = new ArrayList<Integer>(values); 
        Collections.sort(numbers); 
        return Response.status(Status.OK).entity(numbers).build(); 
    }
    

These and all other metrics can be conveniently queried via REST interface in OpenMetrics or Json format under "/metrics/application" for all application metrics.
As soon as we have called our interface once, we get the following output in OpenMetrics format for our example:

 
    application_test_RandomResource_getRandom_total 1.0
    

Not so fancy, is it? Okay, let’s try again:

     
    @Counted(name = "getRandomCount", absolute = true)
    

With this we get the following:

     
    application_getRandomCount_total 1.0
    

We can set the name of the metric (name) and specify that it is displayed as an absolute name (absolute) instead of the full class name.

Annotated metrics – Timer

With a timer we obtain execution times in addition to the number:

     
    @Timed(name = "getRandomTimer", absolute = true, unit = MetricUnits.MILLISECONDS)
    
    
    application_getRandomTimer_rate_per_second 0.03127755194344862 
    application_getRandomTimer_one_min_rate_per_second 0.04797284870696242 
    application_getRandomTimer_five_min_rate_per_second 0.012098841350039854
    application_getRandomTimer_fifteen_min_rate_per_second 0.004292367258391886 
    application_getRandomTimer_min_seconds 3.8485E-5 
    application_getRandomTimer_max_seconds 0.003196787 
    application_getRandomTimer_mean_seconds 2.473753023562506E-4 
    application_getRandomTimer_stddev_seconds 7.667319852774055E-4 
    application_getRandomTimer_seconds_count 4.0 
    application_getRandomTimer_seconds{quantile="0.5"} 4.8331E-5 
    application_getRandomTimer_seconds{quantile="0.75"} 5.6871E-5 
    application_getRandomTimer_seconds{quantile="0.95"} 0.003196787 
    application_getRandomTimer_seconds{quantile="0.98"} 0.003196787 
    application_getRandomTimer_seconds{quantile="0.99"} 0.003196787 
    application_getRandomTimer_seconds{quantile="0.999"} 0.003196787
    

Annotated metrics – SimpleTimer

A SimpleTimer is a simplified timer that only considers the number and elapsed time.

This is particularly useful if you evaluate the metrics with Prometheus. Since Prometheus can determine many values by itself, they do not have to be provided by the metric. See also the specification and this discussion on GitHub.

     
    @SimplyTimed(name = "getRandomTimer", absolute = true, unit = MetricUnits.MILLISECONDS)
    
    
    application_getRandomTimer_total 3.0 
    application_getRandomTimer_elapsedTime_seconds 0.001360847
    

Annotated metrics – Meters

A meter, on the other hand, indicates the number of calls in certain time periods:

     
    @Metered(name = "getRandomMeter", absolute = true, unit = MetricUnits.MILLISECONDS)
    
    
    application_getRandomMeter_total 1001.0
    application_getRandomMeter_rate_per_second 25.19547222937428
    application_getRandomMeter_one_min_rate_per_second 14.747915378636803
    application_getRandomMeter_five_min_rate_per_second 3.4269890149051405
    application_getRandomMeter_fifteen_min_rate_per_second 1.294740801690123
    

Annotated metrics – Gauge

With a gauge it is possible to use return values of methods to generate metrics. The value itself can thus be adjusted programmatically and at runtime. For a good example, see the Quarkus tutorial.

If this is not sufficient, you may need to create metrics dynamically.

Dynamic metrics

With the presented metrics per annotation, one can already cover a multitude of the scenarios that are relevant in a microservice. However, it is sometimes necessary to collect metrics dynamically and programmatically. Especially metrics for specific values of the business logic like numbers of objects, or metrics that should only be registered dynamically at runtime can thus be used flexibly.

Dynamic metrics – MetricRegistry

Metrics are stored in a MetricRegistry. A MetricRegistry manages all metrics including metadata, such as name and description. The unique assignment is done by the Metric-ID, which consists of the name and all tags of the metric. There are registries for each of the three areas Base, Vendor and Application. Later we will take a closer look at the use of tags.

The corresponding registry can be easily injected:

    
    @Inject
    @RegistryType(type = MetricRegistry.Type.APPLICATION) 
    MetricRegistry metricRegistry;
    

Since the Application Registry is the default, it can also be injected without @RegistryType .

Dynamic metrics – Counter

If you have integrated a registry, you can register metrics directly in it. For our example we want to introduce additional counters for the status codes 200 and 400 in addition to an annotated counter.

To do this, we specify the following:

    
    http200Count = metricRegistry.counter("http200Count"); 
    http400Count = metricRegistry.counter("http400Count");
    

These counters can now be called anywhere in the code. In our example it looks like this:

     
    @GET
    @Path("/test")
    @Produces({ "text/plain" })
    @Counted(name = "getRandomCount", absolute = true) 
    public Response getRandom(@QueryParam("numbers") List<Integer> values) 
    { 
        ArrayList<Integer> numbers = new ArrayList<Integer>(values);
        if (numbers.size() == 1) { 
            http400Count.inc(); 
            return Response.status(Status.BAD_REQUEST).build(); 
        }
        Collections.sort(numbers);
        http200Count.inc();
        return Response.status(Status.OK).entity(numbers).build(); 
    }
    

If only one number is passed, an HTTP response code 400 should be returned and the corresponding metric is incremented. Accordingly, if successful, the metric for the response code 200 is incremented.

When calling the interface, once with one number and once with three numbers, the following results when retrieving the metrics:

    
    application_http200Count_total 1.0 
    application_getRandomCount_total 2.0 
    application_http400Count_total 1.0
    

This is only a simple example. A metadata object can be used to add additional data such as name and description. For this see also the specification.

Dynamic metrics – Histogram

If you want to add a new metric to the application registry anyway, you can also do this more conveniently using @Metric Annotation. In our example, I would like to show this using a metric that is only available dynamically – the histogram.

We register our histogram as follows:

    
    @Inject
    @Metric(name = "getRandomHistoram", absolute = true) 
    Histogram getRandomHistogram;
    

In the code we use it to measure the amount of submitted numbers as frequencies:

     
    @GET
    @Path("/test")
    @Produces({ "text/plain" })
    @Counted(name = "getRandomCount", absolute = true) 
    public Response getRandom(@QueryParam("numbers") List<Integer> values) 
    { 
        ArrayList<Integer> numbers = new ArrayList<Integer>(values);
        if (numbers.size() == 1) { 
            http400Count.inc();
            return Response.status(Status.BAD_REQUEST).build(); 
        } 
        Collections.sort(numbers);
        http200Count.inc(); 
        getRandomHistoram.update(numbers.size());
        return Response.status(Status.OK).entity(numbers).build(); 
    }
    

A call with four times two numbers, three times three numbers, twice four numbers and once five numbers results in the following metrics:

    
    application_http200Count_total 10.0 
    application_getRandomCount_total 10.0 
    application_http400Count_total 0.0 
    application_getRandomHistoram_min 2.0 
    application_getRandomHistoram_max 5.0 
    application_getRandomHistoram_mean 3.316335141802363 
    application_getRandomHistoram_stddev 1.0588695413046927 
    application_getRandomHistoram_count 10.0 
    application_getRandomHistoram{quantile="0.5"} 3.0 
    application_getRandomHistoram{quantile="0.75"} 4.0 
    application_getRandomHistoram{quantile="0.95"} 5.0 
    application_getRandomHistoram{quantile="0.98"} 5.0 
    application_getRandomHistoram{quantile="0.99"} 5.0 
    application_getRandomHistoram{quantile="0.999"} 5.0
    

Tags

In addition to the name of the metric, tags can be defined as key, value pairs to better describe and distinguish the metric. Tags can be added to both annotated and dynamic metrics.

Here is an annotated metric with tags including the retrieval of the metric interface:

     
    @Counted(name = "getRandomCounter", absolute = true, tags = { "app=test", "method=get" })
    
     
    application_getRandomCounter_total{app="test",method="get"} 1.0
    

Reuse metrics

For annotated metrics, reuse under the same name and tags (Metrik-ID) is not possible by default to avoid hard-to-find errors. However, you can enable this explicitly, just set the field "reusable" to true. For dynamic metrics on the other hand, reusable is enabled by default to be able to address a metric at different places in the code.

Regarding the use of names, tags, metadata and types let me say the following:

If a metric is to be reused, the name, tags and type (e.g. counter) must match. More about this here.

External metrics

In addition to the self-generated metrics, there are also metrics that are added when other parts of the MicroProfile are used.

An example of this is the Fault Tolerance specification, which automatically registers corresponding metrics when using e.g. @Timeout or @Retry. This combination of the individual specifications allows you to benefit from automatic metrics without having to create them yourself.

Another example comes from Quarkus itself. There, you can activate metrics when your service communicates with databases, for instance. These are even delivered "for free". Not only is this practical, but it saves implementation effort as well and prevents every developer from having to come up with his or her own solution for the same metrics.

Outlook – How the MicroProfile metrics are progressing

With the new MicroProfile version 4.0 the metrics will also be available in a new version 3.0.

These changes are more extensive than between 2.2 and 2.3. In addition to adjustments to metrics such as SimpleTimer and Timer, the reusability of metrics has been expanded. Thus, this is now also possible by default for annotated metrics (except gauge). Further changes can be found in the changelog.

MicroProfile metrics – A Conclusion

All in all, the metric specification of the MicroProfile offers a great and easy way to spruce up your own microservice with metrics.

So far we have already been able to use them successfully in several microservices in various projects.

Naturally, the metrics and possibilities presented above are only a selection. The specification of the metrics and the link mentioned at the beginning gives a deeper insight for interested readers.

I hope this article is helpful for some of you! And respect to all who have made it this far 😉

You want to see more?

Featured posts

Show more
No spam, we promise
Get great insights from our expert team.