Skip to content

How to Set Up Prometheus and Grafana for Spring Boot Monitoring

The Problem

My Spring Boot application was running in production, but I had no visibility into what was happening inside. When users reported slow responses, I was flying blind. I could check logs after the fact, but I needed real-time metrics to understand:

  • How much memory was my application using?
  • How many requests per second was it handling?
  • Which endpoints were slow?
  • Why did the CPU spike at 3 AM?

Spring Boot Actuator exposes metrics, but staring at JSON output in a browser isn’t monitoring. I needed graphs, dashboards, and alerts.

Why Prometheus and Grafana?

I asked around, and the answer was consistent: Prometheus + Grafana. This is the industry standard for Spring Boot monitoring.

Here’s why this combination works:

ComponentPurpose
Spring Boot ActuatorExposes application metrics
MicrometerBridges Spring Boot metrics to Prometheus format
PrometheusCollects and stores time-series metrics
GrafanaVisualizes metrics with dashboards
AlertmanagerSends alerts when something goes wrong
+------------------+ +------------+ +---------+ +----------+
| Spring Boot | | | | | | |
| Application |---->| Prometheus |---->| Grafana |---->| Alert |
| (Actuator) | | | | | | Manager |
+------------------+ +------------+ +---------+ +----------+
| | |
v v v
/actuator/prometheus Time-series DB Email/Slack/PagerDuty

Step 1: Add Dependencies

I started by adding two dependencies to my Spring Boot project.

Maven (pom.xml):

pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>

Gradle (build.gradle):

build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
}

That’s it. Spring Boot’s dependency management handles the versions. The micrometer-registry-prometheus dependency automatically creates a PrometheusMeterRegistry bean and enables the /actuator/prometheus endpoint.

Step 2: Configure Actuator

By default, Actuator endpoints are not exposed. I needed to explicitly enable the Prometheus endpoint.

application.yml
server:
port: 8080
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
prometheus:
enabled: true
metrics:
tags:
application: ${spring.application.name}
export:
prometheus:
enabled: true

The key settings:

  • exposure.include: Which endpoints to expose (security important here)
  • prometheus.enabled: Activate the Prometheus endpoint
  • tags.application: Add an application label to all metrics for filtering

Testing the Endpoint

I started my application and tested:

Terminal window
./mvnw spring-boot:run
curl http://localhost:8080/actuator/prometheus

The response looked like this:

Sample Prometheus output
# HELP jvm_memory_used_bytes The amount of used memory
# TYPE jvm_memory_used_bytes gauge
jvm_memory_used_bytes{application="my-app",area="heap",id="PS Eden Space"} 1.23456789E8
# HELP http_server_requests_seconds Total HTTP server requests
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{application="my-app",exception="None",method="GET",outcome="SUCCESS",status="200"} 15.0

This confirmed the endpoint was working. Spring Boot automatically exposes JVM metrics, HTTP request metrics, and system metrics.

Step 3: Deploy Prometheus

Now I needed Prometheus to scrape these metrics. I created a prometheus.yml configuration file:

prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
scrape_interval: 10s
static_configs:
- targets: ['host.docker.internal:8080']
labels:
application: 'my-spring-boot-app'
environment: 'development'

Important note: I used host.docker.internal:8080 because my Spring Boot app runs on the host, not in Docker. This works on macOS and Windows Docker Desktop. On Linux, use 172.17.0.1:8080 (the Docker bridge gateway).

Then I created a docker-compose.yml:

docker-compose.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.enable-lifecycle'
restart: unless-stopped
networks:
- monitoring
volumes:
prometheus_data:
networks:
monitoring:
driver: bridge

Started Prometheus:

Terminal window
docker-compose up -d prometheus

Verifying the Scrape

I opened the Prometheus UI at http://localhost:9090 and navigated to Status > Targets. The spring-boot-app job showed UP state.

I also tested a query:

Prometheus health query
up{job="spring-boot-app"}

This returned 1, confirming Prometheus was successfully scraping my application.

Step 4: Deploy Grafana

Prometheus collects metrics, but Grafana makes them visible. I added Grafana to my Docker Compose:

docker-compose.yml (updated)
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
restart: unless-stopped
networks:
- monitoring
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
restart: unless-stopped
networks:
- monitoring
depends_on:
- prometheus
volumes:
prometheus_data:
grafana_data:
networks:
monitoring:
driver: bridge

Automatic Data Source Configuration

I created a provisioning file so Grafana automatically connects to Prometheus:

grafana/provisioning/datasources/prometheus.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true

Started everything:

Terminal window
docker-compose up -d

Accessed Grafana at http://localhost:3000 with credentials admin/admin. The Prometheus data source was already configured and working.

Step 5: Create a Dashboard

I had two options: import a pre-built dashboard or create my own.

Option 1: Import Community Dashboard

The fastest approach is to import a community dashboard. Popular options:

Dashboard NameIDDescription
JVM (Micrometer)4701Comprehensive JVM metrics
Spring Boot Statistics12900Spring Boot specific metrics
Spring Boot 2.1 Statistics10280Older Spring Boot versions

Import steps:

  1. Navigate to Dashboards > Import
  2. Enter dashboard ID (e.g., 4701)
  3. Click Load
  4. Select Prometheus data source
  5. Click Import

Option 2: Create Custom Dashboard

I created my own dashboard with the metrics that matter most:

JVM Memory Usage:

Heap memory percentage
(jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}) * 100

HTTP Request Rate:

Requests per second
rate(http_server_requests_seconds_count[5m])

Average Response Time:

Average latency
rate(http_server_requests_seconds_sum[5m]) / rate(http_server_requests_seconds_count[5m])

95th Percentile Response Time:

P95 latency
histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m]))

Error Rate:

Error percentage
rate(http_server_requests_seconds_count{status=~"5.."}[5m]) / rate(http_server_requests_seconds_count[5m]) * 100

Step 6: Set Up Alerting

Metrics without alerts are just graphs. I needed to know when things go wrong. I added Alertmanager to the stack.

Alert Rules

I created alert.rules.yml:

alert.rules.yml
groups:
- name: spring_boot_alerts
interval: 30s
rules:
- alert: HighErrorRate
expr: |
rate(http_server_requests_seconds_count{status=~"5.."}[5m])
/
rate(http_server_requests_seconds_count[5m]) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate detected"
description: "Error rate is {{ $value | humanizePercentage }}"
- alert: HighMemoryUsage
expr: |
(jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}) > 0.85
for: 10m
labels:
severity: warning
annotations:
summary: "High JVM memory usage"
description: "JVM heap memory usage is {{ $value | humanizePercentage }}"
- alert: InstanceDown
expr: up{job="spring-boot-app"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Instance down"
description: "{{ $labels.instance }} has been down for more than 1 minute."

Updated Prometheus Configuration

I updated prometheus.yml to include alerting:

prometheus.yml (with alerting)
global:
scrape_interval: 15s
evaluation_interval: 15s
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
rule_files:
- /etc/prometheus/alert.rules.yml
scrape_configs:
- job_name: 'spring-boot-app'
metrics_path: '/actuator/prometheus'
scrape_interval: 10s
static_configs:
- targets: ['host.docker.internal:8080']

Alertmanager Configuration

I created alertmanager.yml for email notifications:

alertmanager.yml
global:
resolve_timeout: 5m
route:
group_by: ['alertname', 'severity']
group_wait: 10s
group_interval: 10s
repeat_interval: 1h
receiver: 'email-notifications'
receivers:
- name: 'email-notifications'
email_configs:
smarthost: 'smtp.example.com:587'
auth_username: '[email protected]'
auth_password: 'your-password'

Complete Docker Compose

docker-compose.yml (complete)
version: '3.8'
services:
prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./alert.rules.yml:/etc/prometheus/alert.rules.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
networks:
- monitoring
restart: unless-stopped
alertmanager:
image: prom/alertmanager:latest
container_name: alertmanager
ports:
- "9093:9093"
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
- alertmanager_data:/alertmanager
networks:
- monitoring
restart: unless-stopped
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
networks:
- monitoring
restart: unless-stopped
volumes:
prometheus_data:
alertmanager_data:
grafana_data:
networks:
monitoring:
driver: bridge

Adding Custom Metrics

The default metrics are useful, but I also needed application-specific metrics. Micrometer makes this easy.

OrderController.java
@RestController
public class OrderController {
private final Counter orderCounter;
private final Timer orderProcessingTimer;
public OrderController(MeterRegistry meterRegistry) {
// Counter: Increments on each order
this.orderCounter = meterRegistry.counter("orders.total",
"type", "online",
"region", "us-east");
// Timer: Measures order processing duration
this.orderProcessingTimer = Timer.builder("orders.processing.time")
.description("Time taken to process orders")
.tags("type", "online")
.register(meterRegistry);
}
@PostMapping("/orders")
public Order createOrder(@RequestBody OrderRequest request) {
return orderProcessingTimer.record(() -> {
orderCounter.increment();
return orderService.createOrder(request);
});
}
}

Micrometer metric types:

TypeBehaviorUse Case
CounterOnly increasesRequest count, orders placed
GaugeCan increase/decreaseCurrent memory, active threads
TimerMeasures durationRequest processing time
DistributionSummaryValue distributionRequest payload size

Common Issues I Encountered

Issue 1: Prometheus Can’t Scrape

Symptoms: Target shows “Connection refused” in Prometheus UI.

Fix:

  • Verify Spring Boot is running: curl http://localhost:8080/actuator/prometheus
  • Check Docker network: use host.docker.internal on macOS/Windows
  • On Linux, use 172.17.0.1:8080

Issue 2: Missing Metrics in Grafana

Symptoms: Dashboard shows “No data”.

Fix:

  • Test Prometheus data source connection in Grafana
  • Check metric names: {__name__=~".+"}
  • Verify application label matches your query
  • Wait for scrape interval to collect data

Issue 3: High Cardinality Metrics

Symptoms: Prometheus memory grows, slow queries.

Fix: Find high cardinality metrics:

Prometheus alert query
topk(10, count by (__name__) ({__name__=~".+"}))

Avoid using high-cardinality labels like user IDs or request IDs.

Issue 4: Alerts Not Firing

Symptoms: Alert shows as pending but never fires.

Fix:

  • Check the for duration in alert rule
  • Verify the expression returns expected values
  • Check Alertmanager logs: docker-compose logs alertmanager

Production Considerations

Resource Limits

docker-compose.yml (with limits)
services:
prometheus:
image: prom/prometheus:latest
deploy:
resources:
limits:
cpus: '2'
memory: 4G
grafana:
image: grafana/grafana:latest
deploy:
resources:
limits:
cpus: '1'
memory: 1G

Security

Restrict Actuator endpoints:

application.yml
management:
endpoints:
web:
exposure:
include: prometheus
server:
port: 8081

Change Grafana credentials immediately. Never use the default admin/admin in production.

Data Retention

Prometheus command arguments
command:
- '--storage.tsdb.retention.time=30d'
- '--storage.tsdb.retention.size=10GB'

Summary

In this post, I walked through setting up a complete monitoring stack for Spring Boot applications. I started with the problem: no visibility into production applications. Then I implemented the solution using Prometheus for metrics collection, Grafana for visualization, and Alertmanager for notifications.

The key steps were:

  1. Add Spring Boot Actuator and Micrometer dependencies
  2. Configure Actuator to expose the Prometheus endpoint
  3. Deploy Prometheus to scrape metrics
  4. Deploy Grafana to visualize metrics
  5. Set up alerts with Alertmanager
  6. Add custom metrics for application-specific monitoring

This stack scales from development to production. Start with the default metrics and community dashboards, then add custom metrics and alerts as your monitoring needs grow.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments