Home Java Spring Boot Observability: Micrometer, Prometheus, Grafana Guide for Production
Advanced 3 min · July 14, 2026

Spring Boot Observability: Micrometer, Prometheus, Grafana Guide for Production

Learn Spring Boot observability with Micrometer, Prometheus, and Grafana.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 20-25 min read
  • Java 17+ installed on your machine
  • Spring Boot 3.2.x project (we'll use 3.2.5 in examples)
  • Basic familiarity with REST APIs and Spring Boot Actuator
  • Docker installed for running Prometheus and Grafana locally
  • A payment-processing or similar domain example to follow along
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

• Micrometer acts as a metrics facade for Spring Boot, exposing JVM and custom metrics via Actuator endpoints • Prometheus scrapes those endpoints, storing time-series data with labels for multi-dimensional queries • Grafana visualizes Prometheus data with dashboards; use rate() and histogram_quantile() for real-world SLO tracking • Always add Micrometer registry in pom.xml: io.micrometer:micrometer-registry-prometheus:1.12.0 • Production gotcha: never expose /actuator/prometheus without authentication — use Spring Security or network policies

✦ Definition~90s read
What is Observability in Spring Boot?

Spring Boot observability is the practice of using Micrometer to instrument your application code and expose metrics via Actuator, which Prometheus scrapes and Grafana visualizes, giving you real-time insight into system health, performance, and business KPIs.

Think of observability like a car's dashboard.
Plain-English First

Think of observability like a car's dashboard. Micrometer is the sensor that measures engine temperature and speed. Prometheus is the data logger that records everything with timestamps. Grafana is the touchscreen display showing you real-time gauges and alerts. Without it, you're driving blind — your app could be overheating (high latency) and you'd only find out when users call support.

Observability isn't just buzzword bingo — it's the difference between knowing your app is dying at 3 AM and being woken up by a pager. I've been burned too many times by apps that looked healthy in logs but were silently leaking connections or burning CPU. Spring Boot 3.2.x with Micrometer 1.12.x gives you a first-class observability pipeline, but the official docs often skip the hard parts: what to actually measure, how to avoid metric explosion, and how to debug when Prometheus goes silent. In this guide, you'll move beyond the hello-world dashboards. We'll instrument a real payment-processing service, expose custom business metrics (like 'failed refunds per second'), and set up Prometheus rules that catch anomalies before they hit users. We'll also cover the production incident I had where a missing Maven dependency caused a silent metrics outage — and how we fixed it with a simple health check. By the end, you'll have a production-grade observability stack that actually helps you sleep at night.

Setting Up the Micrometer-Prometheus Pipeline

Let's start by instrumenting a Spring Boot 3.2.5 payment-processing service. First, add the required dependencies to your pom.xml. The key is spring-boot-starter-actuator plus the Micrometer Prometheus registry. Without the registry, /actuator/prometheus returns 404 — a silent failure we'll discuss later. The Micrometer core is auto-configured by Spring Boot, so you don't need explicit beans for basic JVM metrics. But for custom business metrics, you'll use MeterRegistry. Here's the minimal setup:

pom.xmlXML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
    <version>1.12.0</version>
</dependency>
<!-- For custom metrics if needed -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-core</artifactId>
</dependency>
Output
Dependencies added. After startup, hit GET /actuator/prometheus — you should see a plaintext response with metrics like jvm_memory_used_bytes and http_server_requests_seconds_sum.
⚠ Version Compatibility
📊 Production Insight
In production, never expose /actuator/prometheus publicly. Use Spring Security to restrict it to internal networks or a monitoring service account. I've seen too many startups accidentally leak JVM heap usage to the internet.
🎯 Key Takeaway
The Micrometer registry dependency is mandatory — without it, the Prometheus endpoint doesn't exist, but Actuator health still reports UP.

What the Official Docs Won't Tell You

The official Spring Boot docs show you how to enable Actuator and add the Prometheus dependency, but they skip the hard lessons. First, Micrometer uses dimensional metrics — every label combination creates a new time series. If you add a label like 'userId' to a counter, you'll create millions of series and blow up Prometheus storage. Second, the default http_server_requests_seconds metric includes a 'uri' label with path variables expanded (e.g., /api/orders/123). This is fine for low-cardinality paths, but if you have /api/orders/{id} with thousands of IDs, you'll get metric explosion. The fix is to use Spring's @Timed annotation with a custom tag that buckets the URI. Third, Prometheus scrape intervals matter — if your app has bursts of high-latency requests that last under 15 seconds, you'll miss them with a default 15s scrape. Set scrape_interval to 5s for critical services. Finally, the /actuator/prometheus endpoint returns metrics in Prometheus exposition format, which is text-based and easy to debug with curl. But the format is strict — any malformed metric name (e.g., starting with a digit) causes Prometheus to reject the entire scrape. I've debugged a production issue where a custom metric named '2xx_count' caused the whole endpoint to fail silently.

PrometheusScrapeConfig.ymlYAML
1
2
3
4
5
6
7
8
9
10
scrape_configs:
  - job_name: 'payment-service'
    scrape_interval: 5s
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['localhost:8080']
    # Add basic auth if needed
    basic_auth:
      username: 'monitoring'
      password: 'secret'
Output
Prometheus scrapes every 5 seconds. Use basic auth to protect the endpoint.
🔥Metric Naming Rules
📊 Production Insight
Always test your Prometheus scrape with curl before deploying: curl -u monitoring:secret http://localhost:8080/actuator/prometheus. If it fails, your dashboards will be blank.
🎯 Key Takeaway
Control label cardinality — never use high-cardinality values like user IDs as labels. Use buckets or aggregate at query time.

Instrumenting Business Metrics with Micrometer

Now let's add custom business metrics to a payment service. We'll track refund failures, payment processing time, and active transactions. Use MeterRegistry to create counters and timers. For the refund failure counter, we need to track by payment method (credit card, PayPal) and failure reason (insufficient funds, expired card). But remember the cardinality lesson — keep labels limited to 3-4 values. Here's a service class that records metrics:

PaymentService.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Service
public class PaymentService {
    private final MeterRegistry meterRegistry;
    private final Counter refundFailureCounter;
    private final Timer paymentTimer;

    public PaymentService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.refundFailureCounter = Counter.builder("payment.refund.failures")
            .description("Total refund failures")
            .tag("paymentMethod", "unknown")
            .register(meterRegistry);
        this.paymentTimer = Timer.builder("payment.processing.time")
            .description("Time to process payment")
            .publishPercentiles(0.5, 0.95, 0.99)
            .register(meterRegistry);
    }

    public void processRefund(String paymentMethod, String reason) {
        refundFailureCounter.increment();
        // Use tags for specific dimensions
        meterRegistry.counter("payment.refund.failures",
            "paymentMethod", paymentMethod,
            "reason", reason).increment();
    }

    public PaymentResult processPayment(PaymentRequest request) {
        return paymentTimer.record(() -> {
            // actual payment logic
            return new PaymentResult(true, "txn_123");
        });
    }
}
Output
After calling processRefund("credit_card", "insufficient_funds"), Prometheus shows: payment_refund_failures_total{paymentMethod="credit_card",reason="insufficient_funds"} 1.0
⚠ Timer Registration
📊 Production Insight
Add a custom health indicator that checks if your business metrics are incrementing. If refund failures stop incrementing, it might mean the metrics pipeline is broken — not that refunds are succeeding.
🎯 Key Takeaway
Use Micrometer's Counter and Timer for business metrics. Pre-register them to avoid runtime registration overhead.

Configuring Prometheus for Spring Boot Metrics

Prometheus needs a proper scrape configuration to discover your Spring Boot instances. In production with multiple replicas, use service discovery (e.g., Kubernetes pod annotations) instead of static targets. For local development, use docker-compose with a prometheus.yml file. Here's a production-grade configuration that includes relabeling for environment tags and rate limiting:

docker-compose.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
version: '3.8'
services:
  prometheus:
    image: prom/prometheus:v2.52.0
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.retention.time=30d'
  grafana:
    image: grafana/grafana:10.4.0
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
Output
Run 'docker-compose up'. Prometheus available at localhost:9090, Grafana at localhost:3000 (admin/admin).
🔥Retention Policy
📊 Production Insight
Monitor Prometheus's own metrics — if it runs out of disk space or memory, all monitoring stops. Set alerts on prometheus_tsdb_storage_blocks_bytes and go_memstats_alloc_bytes.
🎯 Key Takeaway
Use docker-compose for local dev. In production, use Kubernetes pod annotations or Consul for service discovery.

Building Grafana Dashboards for Real-Time Insights

Grafana dashboards are where observability becomes actionable. Start with a JVM dashboard using the Micrometer metrics. Use PromQL queries like rate(jvm_memory_used_bytes[5m]) for memory trends. For business metrics, create panels for refund failure rate per second and payment latency percentiles. Here's a key panel configuration for payment latency:

GrafanaPanelQuery.jsonJSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "targets": [
    {
      "expr": "histogram_quantile(0.99, rate(payment_processing_time_seconds_bucket[5m]))",
      "legendFormat": "p99",
      "refId": "A"
    },
    {
      "expr": "histogram_quantile(0.95, rate(payment_processing_time_seconds_bucket[5m]))",
      "legendFormat": "p95",
      "refId": "B"
    }
  ],
  "title": "Payment Processing Latency",
  "type": "timeseries"
}
Output
Grafana shows two lines: p99 and p95 latency over time. Spikes above 2 seconds trigger alerts.
⚠ Rate Function Importance
📊 Production Insight
Create a 'synthetic transaction' metric — a counter that increments every minute from a cron job. If it stops incrementing, you know the metrics pipeline is dead, not just your business metrics.
🎯 Key Takeaway
Use histogram_quantile for latency percentiles. Always combine with rate() to get per-second averages.

Alerting Rules for Production Readiness

Prometheus alerting rules turn metrics into actionable notifications. Define rules in YAML and load them via the rule_files directive. For the payment service, we need alerts for: high refund failure rate, elevated p99 latency, and JVM heap usage > 80%. Here's a rule file that triggers alerts for these conditions:

alerts.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
groups:
  - name: payment_service_alerts
    rules:
      - alert: HighRefundFailureRate
        expr: rate(payment_refund_failures_total[5m]) > 10
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Refund failure rate is high"
      - alert: HighLatencyP99
        expr: histogram_quantile(0.99, rate(payment_processing_time_seconds_bucket[5m])) > 2
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "P99 latency above 2 seconds"
      - alert: HighHeapUsage
        expr: jvm_memory_used_bytes{area="heap"} / jvm_memory_max_bytes{area="heap"} > 0.8
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Heap usage above 80%"
Output
When refund failures exceed 10 per second for 2 minutes, Prometheus fires an alert. Configure Alertmanager to send to PagerDuty or Slack.
🔥Alert Fatigue
📊 Production Insight
Add a 'dead man's switch' alert — a metric that must always increment. If Prometheus stops scraping, the alert fires. This catches metrics pipeline failures before they cause silent outages.
🎯 Key Takeaway
Alert on rate of change, not absolute values. Use 'for' to debounce. Always include a summary annotation for the on-call engineer.

Advanced: Custom Actuator Endpoint for Metrics Health

To prevent the 'silent metrics outage' scenario, create a custom Actuator health indicator that verifies Micrometer registries are active and metrics are being published. This runs alongside the default health check and gives you a separate signal. Here's an implementation:

MetricsHealthIndicator.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Component
public class MetricsHealthIndicator implements HealthIndicator {
    private final MeterRegistry meterRegistry;

    public MetricsHealthIndicator(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    @Override
    public Health health() {
        try {
            // Check if Prometheus registry is present
            PrometheusMeterRegistry promRegistry = meterRegistry
                .getRegistries()
                .stream()
                .filter(r -> r instanceof PrometheusMeterRegistry)
                .map(r -> (PrometheusMeterRegistry) r)
                .findFirst()
                .orElse(null);

            if (promRegistry == null) {
                return Health.down()
                    .withDetail("error", "Prometheus registry not found")
                    .build();
            }

            // Check if any metrics are registered
            long metricCount = promRegistry.getMeters().size();
            if (metricCount == 0) {
                return Health.down()
                    .withDetail("error", "No metrics registered")
                    .build();
            }

            return Health.up()
                .withDetail("registries", promRegistry.getClass().getSimpleName())
                .withDetail("metricCount", metricCount)
                .build();
        } catch (Exception e) {
            return Health.down(e).build();
        }
    }
}
Output
GET /actuator/health shows: {"status":"UP","components":{"metricsHealth":{"status":"UP","details":{"registries":"PrometheusMeterRegistry","metricCount":42}}}}
⚠ Health Indicator Naming
📊 Production Insight
In production, set up a separate Prometheus probe that scrapes /actuator/health and alerts if metricsHealth goes DOWN. This gives you a second layer of defense.
🎯 Key Takeaway
A custom health indicator for metrics catches pipeline failures before they cause silent outages. Always include it in your monitoring stack.

Debugging Common Observability Issues

Even with proper setup, things go wrong. Here are the most common issues I've seen: (1) Prometheus returns 'invalid metric type' — check that your custom metrics use correct types (Counter, Gauge, Timer). A common mistake is using a Counter for values that can decrease (use Gauge instead). (2) Grafana shows 'No data' — verify Prometheus is scraping by checking the target page at /targets. If it shows 'DOWN', check network connectivity and authentication. (3) Metrics appear but with wrong labels — use Prometheus's /api/v1/labels endpoint to debug label values. For example, curl http://localhost:9090/api/v1/labels shows all label names. (4) High cardinality causing Prometheus OOM — use metric_relabel_configs to drop high-cardinality labels at scrape time. Here's a relabel config that drops 'userId' labels:

prometheus.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
scrape_configs:
  - job_name: 'payment-service'
    scrape_interval: 5s
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['localhost:8080']
    metric_relabel_configs:
      - source_labels: [__name__]
        regex: '.*'
        action: keep
      - source_labels: [userId]
        regex: '.*'
        action: drop
        # Drops any metric with userId label to prevent explosion
Output
After relabeling, metrics with userId labels are dropped at scrape time, preventing cardinality explosion in Prometheus storage.
🔥Debugging Relabeling
📊 Production Insight
Set a Prometheus alert on scrape_duration_seconds > 1 to catch slow metrics endpoints. A slow scrape can block Prometheus from scraping other targets.
🎯 Key Takeaway
Use metric_relabel_configs to drop high-cardinality labels. Debug with /targets and /api/v1/labels endpoints.
● Production incidentPOST-MORTEMseverity: high

The Silent Metrics Outage: When Prometheus Stopped Scraping at 3 AM

Symptom
Grafana dashboards showed flat lines for all JVM metrics. No error logs. Actuator health endpoint returned UP. Users started complaining about slow checkout flow.
Assumption
We assumed Prometheus was down because we hadn't configured scrape intervals correctly. Restarted Prometheus twice with no effect.
Root cause
The pom.xml had spring-boot-starter-actuator but was missing the Micrometer Prometheus registry dependency (io.micrometer:micrometer-registry-prometheus). This meant /actuator/prometheus returned 404, but /actuator/health still worked, masking the issue.
Fix
Added the missing dependency and verified the endpoint returned valid Prometheus metrics. Also added a custom health indicator that checks if Micrometer registries are active.
Key lesson
  • Always verify that /actuator/prometheus returns data after deployment — not just that the endpoint exists.
  • Never assume the metrics pipeline is working because Actuator health says UP.
  • Add a Prometheus scrape health check in your CI/CD pipeline to catch missing dependencies early.
Production debug guideStep-by-step actions for common metrics pipeline failures4 entries
Symptom · 01
Grafana shows 'No data' for all panels
Fix
Check Prometheus targets at /targets. If target is DOWN, verify app is running and /actuator/prometheus returns 200. If target is UP but no data, check PromQL query syntax and metric names.
Symptom · 02
Prometheus scrape returns 'invalid metric type'
Fix
Check custom metric implementations. Ensure Counters only increment, Gauges can go up/down, and Timers use correct record() method. Look for malformed metric names (starting with digit).
Symptom · 03
High memory usage in Prometheus
Fix
Check for high-cardinality labels using /api/v1/labels. Look for unexpected labels like userId or requestId. Add metric_relabel_configs to drop them. Consider increasing Prometheus memory limit.
Symptom · 04
Actuator health is UP but no metrics
Fix
Check if micrometer-registry-prometheus dependency is present. Verify PrometheusMeterRegistry bean exists via /actuator/beans. Restart app if needed. Add custom health indicator to detect this.
★ Quick Debug Cheat Sheet for Spring Boot ObservabilityImmediate commands and fixes for common metrics issues in production
No metrics in Prometheus
Immediate action
Check app is running and endpoint responds
Commands
curl -u monitoring:secret http://localhost:8080/actuator/prometheus | head -20
curl http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | select(.labels.job=="payment-service") | .health'
Fix now
Add missing Micrometer registry dependency to pom.xml and redeploy
Grafana panel shows no data+
Immediate action
Verify metric exists in Prometheus expression browser
Commands
curl 'http://localhost:9090/api/v1/query?query=payment_refund_failures_total'
Check Grafana data source: curl http://localhost:3000/api/datasources
Fix now
Correct PromQL query — ensure metric name matches exactly (case-sensitive)
Prometheus running out of memory+
Immediate action
Identify high-cardinality metrics
Commands
curl 'http://localhost:9090/api/v1/labels' | jq '.data | length'
curl 'http://localhost:9090/api/v1/query?query=count({__name__=~".+"})'
Fix now
Add metric_relabel_configs to drop high-cardinality labels; increase Prometheus memory limit
Custom health indicator shows DOWN+
Immediate action
Check if Prometheus registry bean exists
Commands
curl http://localhost:8080/actuator/health/metricsHealth | jq
curl http://localhost:8080/actuator/beans | jq '.contexts.application.beans | keys | .[] | select(contains("Prometheus"))'
Fix now
Ensure micrometer-registry-prometheus is on classpath; restart app
FeatureMicrometerPrometheusGrafana
RoleMetrics facade and instrumentation libraryTime-series database and scraperVisualization and dashboarding tool
Data FormatExposes metrics in Prometheus exposition format (text)Stores metrics in custom TSDB formatQueries Prometheus via PromQL
ConfigurationSpring Boot auto-configuration + custom metersprometheus.yml scrape configsData source config + panel queries
AlertingNo built-in alertingAlerting rules in YAMLAlerting via Grafana alerts (optional)
Cardinality ControlUse @Timed with tagsmetric_relabel_configs to drop labelsN/A — query-time filtering
Production Use CaseInstrument business metrics (counters, timers)Store and query metrics with retentionReal-time dashboards and SLO tracking
⚙ Quick Reference
8 commands from this guide
FileCommand / CodePurpose
pom.xmlSetting Up the Micrometer-Prometheus Pipeline
PrometheusScrapeConfig.ymlscrape_configs:What the Official Docs Won't Tell You
PaymentService.java@ServiceInstrumenting Business Metrics with Micrometer
docker-compose.ymlversion: '3.8'Configuring Prometheus for Spring Boot Metrics
GrafanaPanelQuery.json{Building Grafana Dashboards for Real-Time Insights
alerts.ymlgroups:Alerting Rules for Production Readiness
MetricsHealthIndicator.java@ComponentAdvanced
prometheus.ymlscrape_configs:Debugging Common Observability Issues

Key takeaways

1
Micrometer + Prometheus + Grafana form a production-grade observability stack for Spring Boot. Always include the Micrometer registry dependency and verify the endpoint.
2
Control label cardinality
never use high-cardinality values like user IDs as labels. Use metric_relabel_configs to drop them at scrape time.
3
Create a custom health indicator for metrics to catch silent pipeline failures. Add a dead man's switch alert for the metrics pipeline itself.
4
Use rate() on counters and histogram buckets in PromQL. Use histogram_quantile for latency percentiles. Always test queries in Prometheus's expression browser.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does Micrometer integrate with Spring Boot Actuator to expose Promet...
Q02SENIOR
Explain the concept of metric cardinality and how to prevent explosion i...
Q03SENIOR
How would you debug a scenario where Prometheus is scraping successfully...
Q04JUNIOR
What is the purpose of the 'rate()' function in PromQL and when should y...
Q01 of 04SENIOR

How does Micrometer integrate with Spring Boot Actuator to expose Prometheus metrics?

ANSWER
Spring Boot Actuator auto-configures Micrometer's MeterRegistry when it detects micrometer-registry-prometheus on the classpath. It creates a PrometheusMeterRegistry bean that publishes metrics at /actuator/prometheus. The endpoint returns text in Prometheus exposition format, which Prometheus scrapes via its HTTP client.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between Micrometer and Prometheus?
02
How do I secure the /actuator/prometheus endpoint?
03
Why is my Grafana dashboard showing 'No data'?
04
What are the best practices for metric naming?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Everything here is grounded in real deployments.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

3 min read · try the examples if you haven't

Previous
Spring Events: Domain Events, Async Listeners, and Event Sourcing
36 / 121 · Spring Boot
Next
GraalVM Native Image with Spring Boot 3: AOT Compilation