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:
| Component | Purpose |
|---|---|
| Spring Boot Actuator | Exposes application metrics |
| Micrometer | Bridges Spring Boot metrics to Prometheus format |
| Prometheus | Collects and stores time-series metrics |
| Grafana | Visualizes metrics with dashboards |
| Alertmanager | Sends alerts when something goes wrong |
+------------------+ +------------+ +---------+ +----------+| Spring Boot | | | | | | || Application |---->| Prometheus |---->| Grafana |---->| Alert || (Actuator) | | | | | | Manager |+------------------+ +------------+ +---------+ +----------+ | | | v v v /actuator/prometheus Time-series DB Email/Slack/PagerDutyStep 1: Add Dependencies
I started by adding two dependencies to my Spring Boot project.
Maven (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):
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.
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: trueThe key settings:
exposure.include: Which endpoints to expose (security important here)prometheus.enabled: Activate the Prometheus endpointtags.application: Add an application label to all metrics for filtering
Testing the Endpoint
I started my application and tested:
./mvnw spring-boot:runcurl http://localhost:8080/actuator/prometheusThe response looked like this:
# HELP jvm_memory_used_bytes The amount of used memory# TYPE jvm_memory_used_bytes gaugejvm_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 summaryhttp_server_requests_seconds_count{application="my-app",exception="None",method="GET",outcome="SUCCESS",status="200"} 15.0This 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:
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:
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: bridgeStarted Prometheus:
docker-compose up -d prometheusVerifying 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:
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:
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: bridgeAutomatic Data Source Configuration
I created a provisioning file so Grafana automatically connects to Prometheus:
apiVersion: 1
datasources: - name: Prometheus type: prometheus access: proxy url: http://prometheus:9090 isDefault: true editable: trueStarted everything:
docker-compose up -dAccessed 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 Name | ID | Description |
|---|---|---|
| JVM (Micrometer) | 4701 | Comprehensive JVM metrics |
| Spring Boot Statistics | 12900 | Spring Boot specific metrics |
| Spring Boot 2.1 Statistics | 10280 | Older Spring Boot versions |
Import steps:
- Navigate to Dashboards > Import
- Enter dashboard ID (e.g.,
4701) - Click Load
- Select Prometheus data source
- Click Import
Option 2: Create Custom Dashboard
I created my own dashboard with the metrics that matter most:
JVM Memory Usage:
(jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"}) * 100HTTP Request Rate:
rate(http_server_requests_seconds_count[5m])Average Response Time:
rate(http_server_requests_seconds_sum[5m]) / rate(http_server_requests_seconds_count[5m])95th Percentile Response Time:
histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m]))Error Rate:
rate(http_server_requests_seconds_count{status=~"5.."}[5m]) / rate(http_server_requests_seconds_count[5m]) * 100Step 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:
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:
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:
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_password: 'your-password'Complete Docker Compose
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: bridgeAdding Custom Metrics
The default metrics are useful, but I also needed application-specific metrics. Micrometer makes this easy.
@RestControllerpublic 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:
| Type | Behavior | Use Case |
|---|---|---|
| Counter | Only increases | Request count, orders placed |
| Gauge | Can increase/decrease | Current memory, active threads |
| Timer | Measures duration | Request processing time |
| DistributionSummary | Value distribution | Request 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.internalon 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
applicationlabel 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:
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
forduration in alert rule - Verify the expression returns expected values
- Check Alertmanager logs:
docker-compose logs alertmanager
Production Considerations
Resource 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: 1GSecurity
Restrict Actuator endpoints:
management: endpoints: web: exposure: include: prometheus server: port: 8081Change Grafana credentials immediately. Never use the default admin/admin in production.
Data Retention
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:
- Add Spring Boot Actuator and Micrometer dependencies
- Configure Actuator to expose the Prometheus endpoint
- Deploy Prometheus to scrape metrics
- Deploy Grafana to visualize metrics
- Set up alerts with Alertmanager
- 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:
- 👨💻 Spring Boot Actuator Documentation
- 👨💻 Prometheus Documentation
- 👨💻 Grafana Documentation
- 👨💻 Micrometer Documentation
- 👨💻 Prometheus Querying Basics
- 👨💻 Grafana Community Dashboards
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments