Spring Boot, Micrometer, Prometheus and Grafana — how to add custom metrics to your application
In this tutorial our Senior Engineer in Altimetrik Poland Aleksander Kołata will guide you how to use Micrometer, Prometheus and Grafana to add and present custom metrics from you application.
Components
First of all, let’s go over the components we are going to use:
- Micrometer — as we can read on its website , it’s a “vendor-neutral application observability facade”, which we are going to use with Prometheus
- Prometheus — an open-source systems monitoring and alerting toolkit
- Grafana — an open source data visualization and monitoring solution
With the usage of these components, we will be able to:
- define our own metrics using Micrometer
- gather them using Prometheus
- display them on a Grafana dashboard
Without further ado, let’s start coding !
Docker Compose
In docker
directory there is a Docker Compose file setting up Prometheus and Grafana containers.The only configuration you need to do is to provide your IP address in file docker/prometheus/prometheus.yml
under scrape_configs[].static_configs[].targets
key. After that, you are ready to run docker-compose up
in docker
directory.
Grafana will be available under http://localhost:3000 address. You can log in using admin/admin credentials
Used metric types
Full explanation of all available metrics are listed here , so I’ll keep it short
- counter — a counter is a cumulative metric that represents a single value that can only increase or be reset to zero on restart, e.g. how many times feature X has been used
- gauge — a gauge is a metric that represents a single numerical value that can arbitrarily go up and down, e.g. how many different books are in stock at the moment
- timer — used when you want to measure time. There are also histograms and summaries, but I will not use them in this tutorial, e.g. how much time does request to endpoint X take on average
Spring Boot application
Configuration
Code as usually will be available on GitHub, so I will keep it short. We are going to need the following dependencies:
- spring-boot-starter-actuator
- micrometer-registry-prometheus
Additionally, we will use these two dependencies, to simulate “a real API” usage, and we’ll add a simple endpoint:
- spring-boot-starter-web
- lombok
In application.properties we can configure the following properties:
spring.application.name=spring-boot-metrics
management.metrics.tags.application=${spring.application.name}
management.endpoints.web.exposure.include=prometheus
- spring.application.name — simply, the name of our application
- management.metrics.tags.application — we may monitor multiple applications on our Grafana dashboard, so we need to distinguish one application from another
- management.endpoints.web.exposure.include=prometheus — to enable /actuator/prometheus endpoint
Endpoint
I will skip the presentation of book model, repository and a service. Those are oversimplified, because designing a production-ready application is not a goal of this tutorial. Application has only one endpoint GET /api/books
which returns an array of books. Optionally, it is possible to filter those books by title.
For such API, these are the example metrics that we may want to monitor:
- how many times this API has been called
- how long does it take to return a list of books
- how many books are “in stock”
Let’s start with the counter — metric api_books_get
will tell us how many times endpoint /api/books
has been called. We will use tags concept to group them by title (if it has been provided).
package pl.akolata.metrics;
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import java.util.List;@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/api/books")
public class BooksController { private final BooksService booksService;
private final MeterRegistry meterRegistry; @GetMapping
public ResponseEntity<List<Book>> getBooks(@RequestParam(required = false) String title) {
Counter counter = Counter.builder("api_books_get")
.tag("title", StringUtils.isEmpty(title) ? "all" : title)
.description("a number of requests to /api/books endpoint")
.register(meterRegistry);
counter.increment(); return ResponseEntity.ok(booksService.findByTitle(title));
}}
Now, in BooksService we will use two metrics types. Firstly I have defined books_count
Gauge to answer the question “how much books do we have in stock”. You can image that it may represent current store’s offer.
I also used service_books_find
Timer to measure how long does it take to find books by title.
To simulate a situation in which for some specific criteria a request takes more time to complete than usual, I’ve added some extra code for Fundamental Algorithms
book.
package pl.akolata.metrics;
import io.micrometer.core.instrument.Gauge;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.Timer;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;@Service
public class BooksService { private final BooksRepository booksRepository;
private final MeterRegistry meterRegistry; public BooksService(BooksRepository booksRepository, MeterRegistry meterRegistry) {
this.booksRepository = booksRepository;
this.meterRegistry = meterRegistry; Gauge.builder("books_count", booksRepository::countBooks)
.description("A current number of books in the system")
.register(meterRegistry);
} @SneakyThrows
public List<Book> findByTitle(String title) {
Tag titleTag = Tag.of("title", StringUtils.isEmpty(title) ? "all" : title);
List<Book> books;
Timer.Sample timer = Timer.start(meterRegistry); if (StringUtils.isEmpty(title)) {
books = booksRepository.getBooks();
} else {
if ("Fundamental Algorithms".equalsIgnoreCase(title)) {
Thread.sleep(ThreadLocalRandom.current().nextInt(200, 400));
} books = booksRepository.getBooks()
.stream()
.filter(book -> book.getTitle().toLowerCase().contains(title.toLowerCase()))
.collect(Collectors.toList());
} timer.stop(Timer.builder("service_books_find")
.description("books searching timer")
.tags(List.of(titleTag))
.register(meterRegistry)); return books;
}}
To test it out, let’s execute some requests
### all books
GET http://localhost:8080/api/books
### present book
GET http://localhost:8080/api/books?title=Domain Driven Design### missing book
GET http://localhost:8080/api/books?title=Clean code### books which takes long time to find
GET http://localhost:8080/api/books?title=Fundamental Algorithms
Grafana
I won’t go into the details because Grafana has a nice documentation, and the UI might change in the future. That’s why I’ll just list the steps one by one.
You can find Grafana under http://localhost:3000/ address — use admin/admin to log in, after that you can either change the password or skip it for now.
After that, configure Prometheus as a data source.
Later, create a new dashboard and add a Gauge panel, which represents a number of different books available in stock — I am using books_count
metric.
Now let’s add a graph which will tell us how many requests have been made to our endpoint (grouped by title). From the Metrics
dropdown I’ve selected api_books_get_total
metric (suffix _total
) was added automatically. Sometimes the single metric can have different variants, like total, sum, count etc. In Legend
section I’ve provided {{title}}
, which is a custom tag provided in the BooksController
. That’s one usage of tags :)
And finally, let’s measure the average time required to search for a book by title. This definition will be a little bit more complex, as I am using rate
function.
That’s how dashboard may look like after some time :)
Summary
Now you know how to:
- add new metrics to your application
- add new panels to Grafana dashboard
You can find Github repository here.
We hope that you enjoy our new article! Big thanks to our talented Senior Engineer Aleksander Kołata for writing down his thoughts about the current topic! Please leave us some support!