Instrumenting Java Apps with Prometheus Metrics
This article provides a detailed guide on integrating Prometheus metrics into your Java application.
It explores key concepts, including instrumenting your application with various metric types, monitoring HTTP request activity, and exposing metrics for Prometheus to scrape.
Let's get started!
Prerequisites
- Prior experience with Java and Spring Boot, along with a recent JDK installed
- Maven or Gradle for dependency management
- Familiarity with Docker and Docker Compose
- Basic understanding of how Prometheus works
Step 1 — Setting up the demo project
To demonstrate Prometheus instrumentation in Java applications, let's set up a simple "Hello World" Spring Boot application along with the Prometheus server.
First, create a new Spring Boot project. The easiest way is to use Spring Initializr. Select:
- Maven or Gradle (we'll use Maven for this tutorial)
- Java 17
- Spring Boot 3.4.2
- Dependencies: Spring Web
Click Generate to download the project, then extract it and open it in your preferred IDE.
Here's the initial application class:
This app exposes two endpoints: / returns a simple "Hello world!" message, and
/metrics endpoint that will eventually expose the instrumented metrics.
Create a docker-compose.yml file in your project root:
Create a Dockerfile for the Spring Boot application:
Create a prometheus.yml configuration file:
Before starting the services, create an .env file:
Launch both services with:
You should see output similar to:
To confirm that the Spring Boot application is running, send a request to the root endpoint:
This should return:
To verify that Prometheus can access the exposed /metrics endpoint, visit
http://localhost:9090/targets in your browser:
Step 2 — Installing the Prometheus client
Before instrumenting your Spring Boot application with Prometheus, you need to install the Micrometer and Prometheus dependencies. Micrometer provides a vendor-neutral metrics facade that Spring Boot uses for its metrics system.
Update your pom.xml to include these dependencies:
Next, configure Spring Boot Actuator to expose the Prometheus endpoint. Add
these settings to your application.properties file:
With these dependencies and configurations in place, Spring Boot will
automatically expose a /actuator/prometheus endpoint that Prometheus can
scrape. Update your prometheus.yml configuration to use this new endpoint:
Rebuild your application:
Now when you visit http://localhost:8080/actuator/prometheus, you'll see the
default metrics that Spring Boot automatically collects:
These default metrics include important JVM statistics like:
- Memory usage (heap and non-heap)
- Garbage collection statistics
- Thread counts
- CPU usage
- HTTP request statistics
While these built-in metrics are valuable, let's explore how to create custom metrics for your application's specific needs. In the following sections, we'll implement different types of metrics:
- Counters for tracking cumulative values
- Gauges for fluctuating measurements
- Timers for measuring durations
- Distributions for analyzing value ranges
Each metric type serves different monitoring needs, and understanding them will help you choose the right one for your specific requirements.
Step 3 — Implementing a Counter metric
Let's start with a fundamental metric that tracks the total number of HTTP requests made to your Spring Boot application. Since this value always increases, it is best represented as a Counter.
A Counter in Prometheus is a cumulative metric that represents a single monotonically increasing counter. It can only increase or be reset to zero on restart. Think of it like an odometer in a car - it only goes up.
Create a metrics configuration class to define your Counter:
This configuration:
- Creates a Counter named
http.requests.total - Adds a description that will appear in Prometheus
- Adds tags (labels in Prometheus terms) to help categorize the metric
- Registers the Counter with Spring's metric registry
Next, create a web filter that will increment the counter for each request:
The filter:
- Extends
OncePerRequestFilterto ensure it runs exactly once per request - Receives the Counter through dependency injection
- Increments the counter before passing the request along
After making several requests to your application, visiting the
/actuator/prometheus endpoint will show:
For each counter metric, Prometheus creates two data points:
- The actual counter (
http_requests_total) - A creation timestamp gauge (
http_requests_created)
You can visualize your counter data in the Prometheus UI at
http://localhost:9090. Enter http_requests_total in the query box and click
Execute:
Switch to the Graph tab to see the counter increasing over time:
Counters are ideal for tracking:
- Total number of requests processed
- Number of errors encountered
- Bytes of data transferred
- Number of items completed
- Any other value that only increases
In the next section, we'll explore Gauge metrics, which are better suited for values that can both increase and decrease.
Step 4 — Implementing a Gauge metric
A Gauge represents a value that can fluctuate up or down. Unlike Counters that only increase, Gauges are perfect for metrics like current memory usage, active requests, or queue size.
Let's modify our MetricsConfig class to include a gauge that tracks the number
of active requests:
Update the filter to track active requests:
To observe the gauge in action, let's add some random delay to the root endpoint:
Now use a load testing tool like wrk to generate concurrent requests:
Visit the /actuator/prometheus endpoint to see your gauge metric:
This indicates that there are currently 42 active requests being processed by your application. Unlike the counter metric that keeps increasing, this gauge value will fluctuate up and down as requests start and complete.
You can observe the changing gauge values over time in Prometheus's Graph view
at http://localhost:9090:
Tracking Static Values
If you need a gauge that tracks absolute but fluctuating values, you can set the value directly instead of incrementing or decrementing it. For example, to track the current memory usage of the JVM:
This will produce metrics like:
Gauges are perfect for metrics like:
- Current memory usage
- Current CPU utilization
- Active connections
- Queue size
- Temperature readings
- Any value that can increase or decrease
In the next section, we'll explore how to use a Histogram metric to track the distribution of request durations.
Step 5 — Implementing a Histogram metric
Histograms are useful for tracking the distribution of measurements, such as request durations. A histogram samples observations and counts them in configurable buckets, making it ideal for analyzing patterns in your metrics.
Let's add a histogram to track HTTP request durations. Update your
MetricsConfig:
Update the metrics filter to record request durations:
After generating some traffic to your application, you'll see histogram data like this:
Let's understand what this output means:
- Each
_bucketline represents the number of requests that took less than or equal to a specific duration - For example,
le="0.5"} 45means 45 requests completed within 0.5 seconds - The
_sumvalue (47.423) is the total of all observed durations - The
_countvalue (100) is the total number of observations
You can calculate useful statistics from histogram data. For example, to find the 95th percentile latency over a 5-minute window, use this PromQL query:
This tells you the response time that 95% of requests fall under, which is more useful than averages for understanding real user experience:
Histograms are particularly useful for:
- Request latencies
- Response sizes
- Queue processing times
- Any measurement where understanding the distribution is important
In the next section, we'll explore Summary metrics, which provide an alternative way to track quantiles.
Step 6 — Implementing a Summary metric
A Summary metric in Prometheus, like a histogram, captures size or duration measurements. However, while histograms calculate quantiles on the server side, summaries compute them on the client. Let's use a Summary to track external API call latencies.
Create a service to demonstrate summary metrics:
Add a controller to use this service:
After making several requests to the /posts endpoint, you'll see summary
metrics like:
This output tells us:
- The median (50th percentile) request time is 341 milliseconds
- 95% of requests complete within 465 milliseconds
- 99% of requests complete within 591 milliseconds
- We've made 32 requests with a total duration of 12.423 seconds
In Prometheus, enter the metric name to see these values:
When to Use Summary vs Histogram
While both Summaries and Histograms can track distributions of values, they serve different purposes:
Use Summaries when:
- You need accurate quantiles for a single instance
- The client can compute quantiles efficiently
- You don't need to aggregate quantiles across instances
Use Histograms when:
- You need to aggregate quantiles across multiple instances
- You want to calculate different quantiles at query time
- You're tracking latencies that might change dramatically
Final Thoughts
In this tutorial, we've explored how to integrate Prometheus metrics into a Spring Boot application. We've covered:
Spring Boot and Micrometer provide a robust foundation for monitoring, making it straightforward to expose both built-in metrics about your JVM and application-specific metrics that matter to your business.
Consider these next steps:
- Set up Prometheus Alertmanager to create alerts based on your metrics.
- Connect your metrics to Grafana for powerful visualization and dashboarding.
- Explore PromQL to write more sophisticated queries for analyzing your metrics.
When implementing metrics in your Spring Boot applications, remember to use dependency injection for metric instances, keep metric collection code isolated in services and filters, choose meaningful metric names and labels, and document your metrics for team visibility.
Thanks for reading, and happy monitoring!