Spring Boot, Micrometer, Prometheus and Grafana — how to add custom metrics to your application

Altimetrik Poland Tech Blog
6 min readSep 12, 2023
Photo by patricia serna on Unsplash
Photo by patricia serna on Unsplash

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!

--

--

Altimetrik Poland Tech Blog

This is a Technical Blog of Altimetrik Poland team. We focus on subjects like: Java, Data, Mobile, Blockchain and Recruitment. Waiting for your feedback!